From 6ff2b16d097f24babdf7bf0935d21e0707bf365b Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 19 Sep 2023 16:26:49 -0500 Subject: [PATCH] Switch photo ids to nanoids --- .vscode/settings.json | 1 + package.json | 2 +- pnpm-lock.yaml | 27 ++++++++------------------- src/photo/PhotoOGTile.tsx | 2 +- src/photo/form.ts | 16 +++------------- src/photo/index.ts | 12 ++---------- src/services/postgres.ts | 10 ++++------ src/site/paths.ts | 6 +++--- src/utility/nanoid.ts | 9 +++++++++ src/utility/string.ts | 3 --- 10 files changed, 32 insertions(+), 56 deletions(-) create mode 100644 src/utility/nanoid.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 7ce3345f..e25b8baa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "exif", "hgetall", "hset", + "nanoids", "nextjs", "qaub", "skippable", diff --git a/package.json b/package.json index 45805db0..8a9320dc 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "eslint": "8.49.0", "eslint-config-next": "13.4.19", "framer-motion": "^10.16.4", + "nanoid": "^5.0.1", "next": "^13.4.19", "next-auth": "0.0.0-manual.ffd05533", "next-themes": "^0.2.1", @@ -30,7 +31,6 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.11.0", - "short-uuid": "^4.2.2", "sonner": "^0.7.4", "tailwindcss": "3.3.3", "ts-exif-parser": "^0.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcfe3104..a0f37f45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,7 @@ specifiers: eslint: 8.49.0 eslint-config-next: 13.4.19 framer-motion: ^10.16.4 + nanoid: ^5.0.1 next: ^13.4.19 next-auth: 0.0.0-manual.ffd05533 next-themes: ^0.2.1 @@ -24,7 +25,6 @@ specifiers: react: 18.2.0 react-dom: 18.2.0 react-icons: ^4.11.0 - short-uuid: ^4.2.2 sonner: ^0.7.4 tailwindcss: 3.3.3 ts-exif-parser: ^0.2.2 @@ -47,6 +47,7 @@ dependencies: eslint: 8.49.0 eslint-config-next: 13.4.19_rngtr6f3b25lvetpihwplgecf4 framer-motion: 10.16.4_biqbaboplfbrettd7655fr4n2y + nanoid: 5.0.1 next: 13.4.19_biqbaboplfbrettd7655fr4n2y next-auth: 0.0.0-manual.ffd05533_next@13.4.19+react@18.2.0 next-themes: 0.2.1_m2l4knchnqa6bzxbbtfxlzoxdy @@ -54,7 +55,6 @@ dependencies: react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-icons: 4.11.0_react@18.2.0 - short-uuid: 4.2.2 sonner: 0.7.4_biqbaboplfbrettd7655fr4n2y tailwindcss: 3.3.3 ts-exif-parser: 0.2.2 @@ -761,10 +761,6 @@ packages: engines: {node: '>=10'} dev: false - /any-base/1.1.0: - resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} - dev: false - /any-promise/1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} dev: false @@ -2524,6 +2520,12 @@ packages: hasBin: true dev: false + /nanoid/5.0.1: + resolution: {integrity: sha512-vWeVtV5Cw68aML/QaZvqN/3QQXc6fBfIieAlu05m7FZW2Dgb+3f0xc0TTxuJW+7u30t7iSDTV/j3kVI0oJqIfQ==} + engines: {node: ^18 || >=20} + hasBin: true + dev: false + /natural-compare/1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: false @@ -3184,14 +3186,6 @@ packages: engines: {node: '>=8'} dev: false - /short-uuid/4.2.2: - resolution: {integrity: sha512-IE7hDSGV2U/VZoCsjctKX6l5t5ak2jE0+aeGJi3KtvjIUNuZVmHVYUjNBhmo369FIWGDtaieRaO8A83Lvwfpqw==} - engines: {node: '>=8'} - dependencies: - any-base: 1.1.0 - uuid: 8.3.2 - dev: false - /side-channel/1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -3596,11 +3590,6 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false - /uuid/8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - dev: false - /w3c-xmlserializer/4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} diff --git a/src/photo/PhotoOGTile.tsx b/src/photo/PhotoOGTile.tsx index a1b68ecc..20c39525 100644 --- a/src/photo/PhotoOGTile.tsx +++ b/src/photo/PhotoOGTile.tsx @@ -79,7 +79,7 @@ export default function PhotoOGTile({ } {(loadingState === 'loading' || loadingState === 'loaded') && {`OG; @@ -19,21 +20,10 @@ type FormMeta = { hideTemporarily?: boolean }; -const FORM_METADATA: Record< - // Display short ids as a convenience - // even though they're not part of form data - keyof PhotoFormData & { idShort?: string }, - FormMeta -> = { +const FORM_METADATA: Record = { title: { label: 'title' }, tags: { label: 'tags', note: 'comma-separated values' }, id: { label: 'id', readOnly: true, hideIfEmpty: true }, - idShort: { - label: 'short id', - readOnly: true, - hideIfEmpty: true, - note: 'Auto-generated by id', - }, url: { label: 'url', readOnly: true }, extension: { label: 'extension', readOnly: true }, aspectRatio: { label: 'aspect ratio', readOnly: true }, @@ -122,7 +112,7 @@ export const convertFormDataToPhoto = ( return { ...photoForm, - ...(generateId && !photoForm.id) && { id: crypto.randomUUID() }, + ...(generateId && !photoForm.id) && { id: generateNanoid() }, // convert form strings to arrays tags: convertStringToArray(photoForm.tags), // Convert form strings to numbers diff --git a/src/photo/index.ts b/src/photo/index.ts index 2798ba1e..e1d86e49 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -12,9 +12,6 @@ import { } from '@/utility/exif'; import camelcaseKeys from 'camelcase-keys'; import { Metadata } from 'next'; -import short from 'short-uuid'; - -const translator = short(); export const GRID_THUMBNAILS_TO_SHOW_MAX = 12; @@ -58,7 +55,6 @@ export interface PhotoDb extends Omit { // Parsed db response export interface Photo extends PhotoDb { - idShort?: string focalLengthFormatted?: string focalLengthIn35MmFormatFormatted?: string fNumberFormatted?: string @@ -75,8 +71,6 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => { ) as unknown as PhotoDb; return { ...photoDb, - idShort: - translator.fromUUID(photoDb.id), tags: photoDb.tags ?? [], focalLengthFormatted: formatFocalLength(photoDb.focalLength), @@ -173,10 +167,8 @@ const PHOTO_ID_FORWARDING_TABLE: Record = JSON.parse( process.env.PHOTO_ID_FORWARDING_TABLE || '{}' ); -export const translatePhotoId = (shortId: string) => { - const id = PHOTO_ID_FORWARDING_TABLE[shortId] || shortId; - return id.length === 22 ? translator.toUUID(id) : id; -}; +export const translatePhotoId = (id: string) => + PHOTO_ID_FORWARDING_TABLE[id] || id; export const titleForPhoto = (photo: Photo) => photo.title || 'Untitled'; diff --git a/src/services/postgres.ts b/src/services/postgres.ts index 09f19f7b..edb0117c 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -6,7 +6,6 @@ import { parsePhotoFromDb, Photo, } from '@/photo'; -import { isValidUUID } from '@/utility/string'; const PHOTO_DEFAULT_LIMIT = 100; @@ -17,7 +16,7 @@ export const convertArrayToPostgresString = (array?: string[]) => array const sqlCreatePhotosTable = () => sql` CREATE TABLE IF NOT EXISTS photos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id VARCHAR(8) PRIMARY KEY, url VARCHAR(255) NOT NULL, extension VARCHAR(255) NOT NULL, aspect_ratio REAL DEFAULT 1.5, @@ -44,6 +43,7 @@ const sqlCreatePhotosTable = () => ) `; +// Must provide id as 8-character nanoid export const sqlInsertPhotoIntoDb = (photo: PhotoDbInsert) => { return sql` INSERT INTO photos ( @@ -256,8 +256,6 @@ export const getPhoto = async (id: string): Promise => { // Check for photo id forwarding // and convert short ids to uuids const photoId = translatePhotoId(id); - return isValidUUID(photoId) - ? sqlGetPhotoFromDb(photoId) - .then(photos => photos.length > 0 ? photos[0] : undefined) - : Promise.resolve(undefined); + return sqlGetPhotoFromDb(photoId) + .then(photos => photos.length > 0 ? photos[0] : undefined); }; diff --git a/src/site/paths.ts b/src/site/paths.ts index a264aa65..9d5a69ee 100644 --- a/src/site/paths.ts +++ b/src/site/paths.ts @@ -13,14 +13,14 @@ export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`; export const pathForPhoto = (photo: Photo, tag?: string) => tag - ? `${pathForTag(tag)}/${photo.idShort}` - : `${PREFIX_PHOTO}/${photo.idShort}`; + ? `${pathForTag(tag)}/${photo.id}` + : `${PREFIX_PHOTO}/${photo.id}`; export const pathForPhotoShare = (photo: Photo, tag?: string) => `${pathForPhoto(photo, tag)}/share`; export const pathForPhotoEdit = (photo: Photo) => - `${PATH_ADMIN_PHOTOS}/${photo.idShort}/edit`; + `${PATH_ADMIN_PHOTOS}/${photo.id}/edit`; export const pathForTag = (tag: string) => `${PREFIX_TAG}/${tag}`; diff --git a/src/utility/nanoid.ts b/src/utility/nanoid.ts new file mode 100644 index 00000000..cb4e73cc --- /dev/null +++ b/src/utility/nanoid.ts @@ -0,0 +1,9 @@ +import { customAlphabet } from 'nanoid'; + +const NANOID_LENGTH = 8; + +const NANOID_ALPHABET = + '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + +export const generateNanoid = + customAlphabet(NANOID_ALPHABET, NANOID_LENGTH); diff --git a/src/utility/string.ts b/src/utility/string.ts index e5fbfb91..ed8e8ab3 100644 --- a/src/utility/string.ts +++ b/src/utility/string.ts @@ -1,6 +1,3 @@ -export const isValidUUID = (id: string): boolean => - /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/i.test(id); - export const convertStringToArray = ( string?: string, parameterize = true,