From 296eb23d97bb7f9e282c5aa3be3a3699901ba057 Mon Sep 17 00:00:00 2001 From: Kyle Corbitt Date: Thu, 17 Aug 2023 23:28:56 -0700 Subject: [PATCH] Use shorter experiment IDs Because https://app.openpipe.ai/experiments/B1EtN6oHeXMele2 is a cooler URL than https://app.openpipe.ai/experiments/3692942c-6f1b-4bef-83b1-c11f00a3fbdd --- app/@types/nextjs-routes.d.ts | 2 +- .../migration.sql | 88 +++++++++++++++++++ app/prisma/schema.prisma | 58 ++++++------ .../components/experiments/ExperimentCard.tsx | 20 ++--- .../useOnForkButtonPressed.tsx | 7 +- .../{[id].tsx => [experimentSlug].tsx} | 6 +- .../server/api/routers/experiments.router.ts | 12 ++- app/src/utils/hooks.ts | 4 +- pnpm-lock.yaml | 2 +- 9 files changed, 143 insertions(+), 56 deletions(-) create mode 100644 app/prisma/migrations/20230818055303_add_slug_to_experiment/migration.sql rename app/src/pages/experiments/{[id].tsx => [experimentSlug].tsx} (96%) diff --git a/app/@types/nextjs-routes.d.ts b/app/@types/nextjs-routes.d.ts index d573c23..3c9bf4d 100644 --- a/app/@types/nextjs-routes.d.ts +++ b/app/@types/nextjs-routes.d.ts @@ -21,7 +21,7 @@ declare module "nextjs-routes" { | StaticRoute<"/dashboard"> | DynamicRoute<"/data/[id]", { "id": string }> | StaticRoute<"/data"> - | DynamicRoute<"/experiments/[id]", { "id": string }> + | DynamicRoute<"/experiments/[experimentSlug]", { "experimentSlug": string }> | StaticRoute<"/experiments"> | StaticRoute<"/"> | DynamicRoute<"/invitations/[invitationToken]", { "invitationToken": string }> diff --git a/app/prisma/migrations/20230818055303_add_slug_to_experiment/migration.sql b/app/prisma/migrations/20230818055303_add_slug_to_experiment/migration.sql new file mode 100644 index 0000000..171f194 --- /dev/null +++ b/app/prisma/migrations/20230818055303_add_slug_to_experiment/migration.sql @@ -0,0 +1,88 @@ +/* + * Copyright 2023 Viascom Ltd liab. Co + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +CREATE OR REPLACE FUNCTION nanoid( + size int DEFAULT 21, + alphabet text DEFAULT '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' +) + RETURNS text + LANGUAGE plpgsql + volatile +AS +$$ +DECLARE + idBuilder text := ''; + counter int := 0; + bytes bytea; + alphabetIndex int; + alphabetArray text[]; + alphabetLength int; + mask int; + step int; +BEGIN + alphabetArray := regexp_split_to_array(alphabet, ''); + alphabetLength := array_length(alphabetArray, 1); + mask := (2 << cast(floor(log(alphabetLength - 1) / log(2)) as int)) - 1; + step := cast(ceil(1.6 * mask * size / alphabetLength) AS int); + + while true + loop + bytes := gen_random_bytes(step); + while counter < step + loop + alphabetIndex := (get_byte(bytes, counter) & mask) + 1; + if alphabetIndex <= alphabetLength then + idBuilder := idBuilder || alphabetArray[alphabetIndex]; + if length(idBuilder) = size then + return idBuilder; + end if; + end if; + counter := counter + 1; + end loop; + + counter := 0; + end loop; +END +$$; + + +-- Make a short_nanoid function that uses the default alphabet and length of 15 +CREATE OR REPLACE FUNCTION short_nanoid() + RETURNS text + LANGUAGE plpgsql + volatile +AS +$$ +BEGIN + RETURN nanoid(15, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'); +END +$$; + +-- AlterTable +ALTER TABLE "Experiment" ADD COLUMN "slug" TEXT NOT NULL DEFAULT short_nanoid(); + +-- For existing experiments, keep the existing id as the slug for backwards compatibility +UPDATE "Experiment" SET "slug" = "id"; + +-- CreateIndex +CREATE UNIQUE INDEX "Experiment_slug_key" ON "Experiment"("slug"); diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index ed25d5e..dd12489 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -11,7 +11,9 @@ datasource db { } model Experiment { - id String @id @default(uuid()) @db.Uuid + id String @id @default(uuid()) @db.Uuid + + slug String @unique @default(dbgenerated("short_nanoid()")) label String sortIndex Int @default(0) @@ -207,14 +209,14 @@ model Project { personalProjectUserId String? @unique @db.Uuid personalProjectUser User? @relation(fields: [personalProjectUserId], references: [id], onDelete: Cascade) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - projectUsers ProjectUser[] - projectUserInvitations UserInvitation[] - experiments Experiment[] - datasets Dataset[] - loggedCalls LoggedCall[] - apiKeys ApiKey[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + projectUsers ProjectUser[] + projectUserInvitations UserInvitation[] + experiments Experiment[] + datasets Dataset[] + loggedCalls LoggedCall[] + apiKeys ApiKey[] } enum ProjectUserRole { @@ -324,10 +326,10 @@ model LoggedCallModelResponse { } model LoggedCallTag { - id String @id @default(uuid()) @db.Uuid - name String - value String? - projectId String @db.Uuid + id String @id @default(uuid()) @db.Uuid + name String + value String? + projectId String @db.Uuid loggedCallId String @db.Uuid loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade) @@ -391,12 +393,12 @@ model User { role UserRole @default(USER) - accounts Account[] - sessions Session[] - projectUsers ProjectUser[] - projects Project[] - worldChampEntrant WorldChampEntrant? - sentUserInvitations UserInvitation[] + accounts Account[] + sessions Session[] + projectUsers ProjectUser[] + projects Project[] + worldChampEntrant WorldChampEntrant? + sentUserInvitations UserInvitation[] createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt @@ -405,17 +407,17 @@ model User { model UserInvitation { id String @id @default(uuid()) @db.Uuid - projectId String @db.Uuid - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) - email String - role ProjectUserRole - invitationToken String @unique - senderId String @db.Uuid - sender User @relation(fields: [senderId], references: [id], onDelete: Cascade) + projectId String @db.Uuid + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + email String + role ProjectUserRole + invitationToken String @unique + senderId String @db.Uuid + sender User @relation(fields: [senderId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@unique([projectId, email]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt } model VerificationToken { diff --git a/app/src/components/experiments/ExperimentCard.tsx b/app/src/components/experiments/ExperimentCard.tsx index 4e36cc6..56db7a6 100644 --- a/app/src/components/experiments/ExperimentCard.tsx +++ b/app/src/components/experiments/ExperimentCard.tsx @@ -14,21 +14,11 @@ import { formatTimePast } from "~/utils/dayjs"; import Link from "next/link"; import { useRouter } from "next/router"; import { BsPlusSquare } from "react-icons/bs"; -import { api } from "~/utils/api"; +import { RouterOutputs, api } from "~/utils/api"; import { useHandledAsyncCallback } from "~/utils/hooks"; import { useAppStore } from "~/state/store"; -type ExperimentData = { - testScenarioCount: number; - promptVariantCount: number; - id: string; - label: string; - sortIndex: number; - createdAt: Date; - updatedAt: Date; -}; - -export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => { +export const ExperimentCard = ({ exp }: { exp: RouterOutputs["experiments"]["list"][0] }) => { return ( { as={Link} w="full" h="full" - href={{ pathname: "/experiments/[id]", query: { id: exp.id } }} + href={{ pathname: "/experiments/[experimentSlug]", query: { experimentSlug: exp.slug } }} justify="space-between" > @@ -89,8 +79,8 @@ export const NewExperimentCard = () => { projectId: selectedProjectId ?? "", }); await router.push({ - pathname: "/experiments/[id]", - query: { id: newExperiment.id }, + pathname: "/experiments/[experimentSlug]", + query: { experimentSlug: newExperiment.slug }, }); }, [createMutation, router, selectedProjectId]); diff --git a/app/src/components/experiments/ExperimentHeaderButtons/useOnForkButtonPressed.tsx b/app/src/components/experiments/ExperimentHeaderButtons/useOnForkButtonPressed.tsx index b207b10..37391a9 100644 --- a/app/src/components/experiments/ExperimentHeaderButtons/useOnForkButtonPressed.tsx +++ b/app/src/components/experiments/ExperimentHeaderButtons/useOnForkButtonPressed.tsx @@ -16,11 +16,14 @@ export const useOnForkButtonPressed = () => { const [onFork, isForking] = useHandledAsyncCallback(async () => { if (!experiment.data?.id || !selectedProjectId) return; - const forkedExperimentId = await forkMutation.mutateAsync({ + const newExperiment = await forkMutation.mutateAsync({ id: experiment.data.id, projectId: selectedProjectId, }); - await router.push({ pathname: "/experiments/[id]", query: { id: forkedExperimentId } }); + await router.push({ + pathname: "/experiments/[experimentSlug]", + query: { experimentSlug: newExperiment.slug }, + }); }, [forkMutation, experiment.data?.id, router]); const onForkButtonPressed = useCallback(() => { diff --git a/app/src/pages/experiments/[id].tsx b/app/src/pages/experiments/[experimentSlug].tsx similarity index 96% rename from app/src/pages/experiments/[id].tsx rename to app/src/pages/experiments/[experimentSlug].tsx index 9828eb1..2713605 100644 --- a/app/src/pages/experiments/[id].tsx +++ b/app/src/pages/experiments/[experimentSlug].tsx @@ -33,9 +33,9 @@ export default function Experiment() { const experiment = useExperiment(); const experimentStats = api.experiments.stats.useQuery( - { id: router.query.id as string }, + { id: experiment.data?.id as string }, { - enabled: !!router.query.id, + enabled: !!experiment.data?.id, }, ); const stats = experimentStats.data; @@ -125,7 +125,7 @@ export default function Experiment() { - + diff --git a/app/src/server/api/routers/experiments.router.ts b/app/src/server/api/routers/experiments.router.ts index 58bb7c0..1be0275 100644 --- a/app/src/server/api/routers/experiments.router.ts +++ b/app/src/server/api/routers/experiments.router.ts @@ -85,15 +85,16 @@ export const experimentsRouter = createTRPCRouter({ return experimentsWithCounts; }), - get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => { - await requireCanViewExperiment(input.id, ctx); + get: publicProcedure.input(z.object({ slug: z.string() })).query(async ({ input, ctx }) => { const experiment = await prisma.experiment.findFirstOrThrow({ - where: { id: input.id }, + where: { slug: input.slug }, include: { project: true, }, }); + await requireCanViewExperiment(experiment.id, ctx); + const canModify = ctx.session?.user.id ? await canModifyExperiment(experiment.id, ctx.session?.user.id) : false; @@ -290,7 +291,10 @@ export const experimentsRouter = createTRPCRouter({ }), ]); - return newExperimentId; + const newExperiment = await prisma.experiment.findUniqueOrThrow({ + where: { id: newExperimentId }, + }); + return newExperiment; }), create: protectedProcedure diff --git a/app/src/utils/hooks.ts b/app/src/utils/hooks.ts index ab7b282..23d1777 100644 --- a/app/src/utils/hooks.ts +++ b/app/src/utils/hooks.ts @@ -15,8 +15,8 @@ export const useExperiments = () => { export const useExperiment = () => { const router = useRouter(); const experiment = api.experiments.get.useQuery( - { id: router.query.id as string }, - { enabled: !!router.query.id }, + { slug: router.query.experimentSlug as string }, + { enabled: !!router.query.experimentSlug }, ); return experiment; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fad4937..e065f05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6259,7 +6259,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.4.10 + '@types/node': 18.16.0 merge-stream: 2.0.0 supports-color: 8.1.1