diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 66028fb5..64c7bed0 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -2,11 +2,11 @@ import { GetPhotosOptions, - sqlDeletePhoto, - sqlInsertPhoto, - sqlDeletePhotoTagGlobally, - sqlUpdatePhoto, - sqlRenamePhotoTagGlobally, + deletePhoto, + insertPhoto, + deletePhotoTagGlobally, + updatePhoto, + renamePhotoTagGlobally, getPhoto, getPhotos, } from '@/photo/db'; @@ -53,7 +53,7 @@ export const createPhotoAction = async (formData: FormData) => if (updatedUrl) { photo.url = updatedUrl; - await sqlInsertPhoto(photo); + await insertPhoto(photo); revalidateAllKeysAndPaths(); redirect(PATH_ADMIN_PHOTOS); } @@ -71,7 +71,7 @@ export const updatePhotoAction = async (formData: FormData) => if (url) { photo.url = url; } } - await sqlUpdatePhoto(photo); + await updatePhoto(photo); revalidatePhoto(photo.id); @@ -89,7 +89,7 @@ export const toggleFavoritePhotoAction = async ( photo.tags = tags.some(tag => tag === TAG_FAVS) ? tags.filter(tag => !isTagFavs(tag)) : [...tags, TAG_FAVS]; - await sqlUpdatePhoto(convertPhotoToPhotoDbInsert(photo)); + await updatePhoto(convertPhotoToPhotoDbInsert(photo)); revalidateAllKeysAndPaths(); if (shouldRedirect) { redirect(pathForPhoto(photoId)); @@ -103,7 +103,7 @@ export const deletePhotoAction = async ( shouldRedirect?: boolean, ) => runAuthenticatedAdminServerAction(async () => { - await sqlDeletePhoto(photoId).then(() => deleteStorageUrl(photoUrl)); + await deletePhoto(photoId).then(() => deleteStorageUrl(photoUrl)); revalidateAllKeysAndPaths(); if (shouldRedirect) { redirect(PATH_ROOT); @@ -122,7 +122,7 @@ export const deletePhotoTagGloballyAction = async (formData: FormData) => runAuthenticatedAdminServerAction(async () => { const tag = formData.get('tag') as string; - await sqlDeletePhotoTagGlobally(tag); + await deletePhotoTagGlobally(tag); revalidatePhotosKey(); revalidateAdminPaths(); @@ -134,7 +134,7 @@ export const renamePhotoTagGloballyAction = async (formData: FormData) => const updatedTag = formData.get('updatedTag') as string; if (tag && updatedTag && tag !== updatedTag) { - await sqlRenamePhotoTagGlobally(tag, updatedTag); + await renamePhotoTagGlobally(tag, updatedTag); revalidatePhotosKey(); revalidateTagsKey(); redirect(PATH_ADMIN_TAGS); @@ -185,7 +185,7 @@ export const syncPhotoExifDataAction = async (formData: FormData) => ...convertPhotoToFormData(photo), ...photoFormExif, }); - await sqlUpdatePhoto(photoFormDbInsert); + await updatePhoto(photoFormDbInsert); revalidatePhotosKey(); } } diff --git a/src/photo/db.ts b/src/photo/db.ts index aa27cac7..949d085e 100644 --- a/src/photo/db.ts +++ b/src/photo/db.ts @@ -21,7 +21,20 @@ export const GENERATE_STATIC_PARAMS_LIMIT = 1000; const PHOTO_DEFAULT_LIMIT = 100; -const sqlCreatePhotosTable = () => +export type GetPhotosOptions = { + sortBy?: 'createdAt' | 'takenAt' | 'priority' + limit?: number + offset?: number + query?: string + tag?: string + camera?: Camera + simulation?: FilmSimulation + takenBefore?: Date + takenAfterInclusive?: Date + hidden?: 'exclude' | 'include' | 'only' +} + +const createPhotosTable = () => sql` CREATE TABLE IF NOT EXISTS photos ( id VARCHAR(8) PRIMARY KEY, @@ -56,7 +69,7 @@ const sqlCreatePhotosTable = () => // Migration 01 const MIGRATION_FIELDS_01 = ['caption', 'semantic_description']; -const sqlRunMigration01 = () => +const runMigration01 = () => sql` ALTER TABLE photos ADD COLUMN IF NOT EXISTS caption TEXT, @@ -64,7 +77,7 @@ const sqlRunMigration01 = () => `; // Must provide id as 8-character nanoid -export const sqlInsertPhoto = (photo: PhotoDbInsert) => +export const insertPhoto = (photo: PhotoDbInsert) => safelyQueryPhotos(() => sql` INSERT INTO photos ( id, @@ -120,9 +133,9 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => ${photo.takenAt}, ${photo.takenAtNaive} ) - `, 'sqlInsertPhoto'); + `, 'insertPhoto'); -export const sqlUpdatePhoto = (photo: PhotoDbInsert) => +export const updatePhoto = (photo: PhotoDbInsert) => safelyQueryPhotos(() => sql` UPDATE photos SET url=${photo.url}, @@ -151,36 +164,33 @@ export const sqlUpdatePhoto = (photo: PhotoDbInsert) => taken_at_naive=${photo.takenAtNaive}, updated_at=${(new Date()).toISOString()} WHERE id=${photo.id} - `, 'sqlUpdatePhoto'); + `, 'updatePhoto'); -export const sqlDeletePhotoTagGlobally = (tag: string) => +export const deletePhotoTagGlobally = (tag: string) => safelyQueryPhotos(() => sql` UPDATE photos SET tags=ARRAY_REMOVE(tags, ${tag}) WHERE ${tag}=ANY(tags) - `, 'sqlDeletePhotoTagGlobally'); + `, 'deletePhotoTagGlobally'); -export const sqlRenamePhotoTagGlobally = (tag: string, updatedTag: string) => +export const renamePhotoTagGlobally = (tag: string, updatedTag: string) => safelyQueryPhotos(() => sql` UPDATE photos SET tags=ARRAY_REPLACE(tags, ${tag}, ${updatedTag}) WHERE ${tag}=ANY(tags) - `, 'sqlRenamePhotoTagGlobally'); + `, 'renamePhotoTagGlobally'); -export const sqlDeletePhoto = (id: string) => +export const deletePhoto = (id: string) => safelyQueryPhotos( () => sql`DELETE FROM photos WHERE id=${id}`, - 'sqlDeletePhoto', + 'deletePhoto', ); -const sqlGetPhoto = (id: string, includeHidden?: boolean) => includeHidden - ? sql`SELECT * FROM photos WHERE id=${id} LIMIT 1` - // eslint-disable-next-line max-len - : sql`SELECT * FROM photos WHERE id=${id} AND hidden IS NOT TRUE LIMIT 1`; - -const sqlGetPhotosMostRecentUpdate = async () => sql` - SELECT updated_at FROM photos ORDER BY updated_at DESC LIMIT 1 -`.then(({ rows }) => rows[0] ? rows[0].updated_at as Date : undefined); +export const getPhotosMostRecentUpdate = async () => + safelyQueryPhotos(() => sql` + SELECT updated_at FROM photos ORDER BY updated_at DESC LIMIT 1 + `.then(({ rows }) => rows[0] ? rows[0].updated_at as Date : undefined), + 'getPhotosMostRecentUpdate'); export const getUniqueTags = async () => safelyQueryPhotos(() => sql` @@ -193,8 +203,7 @@ export const getUniqueTags = async () => tag: tag as string, count: parseInt(count, 10), }))), - 'getUniqueTags', - ); + 'getUniqueTags'); export const getUniqueTagsHidden = async () => safelyQueryPhotos(() => sql` @@ -206,8 +215,7 @@ export const getUniqueTagsHidden = async () => tag: tag as string, count: parseInt(count, 10), }))), - 'getUniqueTagsHidden', - ); + 'getUniqueTagsHidden'); export const getUniqueCameras = async () => safelyQueryPhotos(() => sql` @@ -223,8 +231,7 @@ export const getUniqueCameras = async () => camera: { make, model }, count: parseInt(count, 10), }))), - 'getUniqueCameras', - ); + 'getUniqueCameras'); export const getUniqueFilmSimulations = async () => safelyQueryPhotos(() => sql` @@ -238,21 +245,7 @@ export const getUniqueFilmSimulations = async () => simulation: film_simulation as FilmSimulation, count: parseInt(count, 10), }))), - 'getUniqueFilmSimulations', - ); - -export type GetPhotosOptions = { - sortBy?: 'createdAt' | 'takenAt' | 'priority' - limit?: number - offset?: number - query?: string - tag?: string - camera?: Camera - simulation?: FilmSimulation - takenBefore?: Date - takenAfterInclusive?: Date - hidden?: 'exclude' | 'include' | 'only' -} + 'getUniqueFilmSimulations'); const safelyQueryPhotos = async ( callback: () => Promise, @@ -270,12 +263,12 @@ const safelyQueryPhotos = async ( 'i', ).test(e.message))) { console.log('Running migration 01 ...'); - await sqlRunMigration01(); + await runMigration01(); result = await callback(); } else if (/relation "photos" does not exist/i.test(e.message)) { // If the table does not exist, create it console.log('Creating photos table ...'); - await sqlCreatePhotosTable(); + await createPhotosTable(); result = await callback(); } else if (/endpoint is in transition/i.test(e.message)) { console.log('sql get error: endpoint is in transition (setting timeout)'); @@ -426,9 +419,9 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => sql.push(limitAndOffset); values.push(...limitAndOffsetValues); - return query(sql.join(' '), values); - }, 'getPhotos') - .then(({ rows }) => rows.map(parsePhotoFromDb)); + return query(sql.join(' '), values) + .then(({ rows }) => rows.map(parsePhotoFromDb)); + }, 'getPhotos'); export const getPhotosNearId = async ( photoId: string, @@ -460,16 +453,16 @@ export const getPhotosNearId = async ( LIMIT $${valuesIndex++} `, [...wheresValues, photoId, limit] - ); - }, `getPhotosNearId: ${photoId}`) - .then(({ rows }) => { - const photo = rows.find(({ id }) => id === photoId); - const indexNumber = photo ? parseInt(photo.row_number) : undefined; - return { - photos: rows.map(parsePhotoFromDb), - indexNumber, - }; - }); + ) + .then(({ rows }) => { + const photo = rows.find(({ id }) => id === photoId); + const indexNumber = photo ? parseInt(photo.row_number) : undefined; + return { + photos: rows.map(parsePhotoFromDb), + indexNumber, + }; + }); + }, `getPhotosNearId: ${photoId}`); export const getPhotosMeta = (options: GetPhotosOptions = {}) => safelyQueryPhotos(async () => { @@ -477,36 +470,33 @@ export const getPhotosMeta = (options: GetPhotosOptions = {}) => let sql = 'SELECT COUNT(*), MIN(taken_at_naive) as start, MAX(taken_at_naive) as end FROM photos'; const { wheres, wheresValues } = getWheresFromOptions(options); if (wheres) { sql += ` ${wheres}`; } - return query(sql, wheresValues); - }, 'getPhotosMeta') - .then(({ rows }) => ({ - count: parseInt(rows[0].count, 10), - ...rows[0]?.start && rows[0]?.end - ? { dateRange: rows[0] as PhotoDateRange } - : undefined, - })); + return query(sql, wheresValues) + .then(({ rows }) => ({ + count: parseInt(rows[0].count, 10), + ...rows[0]?.start && rows[0]?.end + ? { dateRange: rows[0] as PhotoDateRange } + : undefined, + })); + }, 'getPhotosMeta'); export const getPhotoIds = async ({ limit }: { limit?: number }) => - safelyQueryPhotos(() => limit + safelyQueryPhotos(() => (limit ? sql`SELECT id FROM photos LIMIT ${limit}` - : sql`SELECT id FROM photos`, - 'getPhotoIds') - .then(({ rows }) => rows.map(({ id }) => id as string)); + : sql`SELECT id FROM photos`) + .then(({ rows }) => rows.map(({ id }) => id as string)), + 'getPhotoIds'); export const getPhoto = async ( id: string, includeHidden?: boolean, ): Promise => - safelyQueryPhotos(() => { + safelyQueryPhotos(async () => { // Check for photo id forwarding and convert short ids to uuids const photoId = translatePhotoId(id); - return sqlGetPhoto(photoId, includeHidden); - }, 'sqlGetPhoto') - .then(({ rows }) => rows.map(parsePhotoFromDb)) - .then(photos => photos.length > 0 ? photos[0] : undefined); - -export const getPhotosMostRecentUpdate = () => - safelyQueryPhotos( - sqlGetPhotosMostRecentUpdate, - 'getPhotosMostRecentUpdate', - ); + return (includeHidden + ? sql`SELECT * FROM photos WHERE id=${photoId} LIMIT 1` + // eslint-disable-next-line max-len + : sql`SELECT * FROM photos WHERE id=${photoId} AND hidden IS NOT TRUE LIMIT 1`) + .then(({ rows }) => rows.map(parsePhotoFromDb)) + .then(photos => photos.length > 0 ? photos[0] : undefined); + }, 'getPhoto');