diff --git a/README.md b/README.md index 64007c83..86a59c1f 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Installation - `NEXT_PUBLIC_PRO_MODE = 1` enables higher quality image storage - `NEXT_PUBLIC_GEO_PRIVACY = 1` disables collection/display of location-based data +- `NEXT_PUBLIC_IGNORE_PRIORITY_ORDER = 1` prevents `priority_order` field affecting photo order - `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api` - `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo - `NEXT_PUBLIC_HIDE_FILM_SIMULATIONS = 1` prevents Fujifilm simulations showing up in `/grid` sidebar diff --git a/src/cache/index.ts b/src/cache/index.ts index 0bdd40eb..7673c577 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -48,6 +48,7 @@ const getPhotosCacheKeyForOption = ( // Primitive keys case 'sortBy': case 'limit': + case 'offset': case 'tag': case 'simulation': case 'includeHidden': { diff --git a/src/services/vercel-postgres.ts b/src/services/vercel-postgres.ts index 80fd6f81..7ab19c6f 100644 --- a/src/services/vercel-postgres.ts +++ b/src/services/vercel-postgres.ts @@ -1,4 +1,4 @@ -import { sql } from '@vercel/postgres'; +import { db, sql } from '@vercel/postgres'; import { PhotoDb, PhotoDbInsert, @@ -11,6 +11,7 @@ import { Camera, Cameras, createCameraKey } from '@/camera'; import { parameterize } from '@/utility/string'; import { Tags } from '@/tag'; import { FilmSimulation, FilmSimulations } from '@/simulation'; +import { PRIORITY_ORDER_ENABLED } from '@/site/config'; const PHOTO_DEFAULT_LIMIT = 100; @@ -151,109 +152,6 @@ export const sqlRenamePhotoTagGlobally = (tag: string, updatedTag: string) => export const sqlDeletePhoto = (id: string) => sql`DELETE FROM photos WHERE id=${id}`; -const sqlGetPhotos = ( - limit = PHOTO_DEFAULT_LIMIT, - offset = 0, -) => - sql` - SELECT * FROM photos - WHERE hidden IS NOT TRUE - ORDER BY taken_at DESC - LIMIT ${limit} OFFSET ${offset} - `; - -const sqlGetPhotosIncludingHidden = ( - limit = PHOTO_DEFAULT_LIMIT, - offset = 0, -) => - sql` - SELECT * FROM photos - ORDER BY created_at DESC - LIMIT ${limit} OFFSET ${offset} - `; - -const sqlGetPhotosSortedByCreatedAt = ( - limit = PHOTO_DEFAULT_LIMIT, - offset = 0, -) => - sql` - SELECT * FROM photos - WHERE hidden IS NOT TRUE - ORDER BY created_at DESC - LIMIT ${limit} OFFSET ${offset} - `; - -const sqlGetPhotosSortedByPriority = ( - limit = PHOTO_DEFAULT_LIMIT, - offset = 0, -) => - sql` - SELECT * FROM photos - WHERE hidden IS NOT TRUE - ORDER BY priority_order ASC, taken_at DESC - LIMIT ${limit} OFFSET ${offset} - `; - -const sqlGetPhotosByTag = ( - limit = PHOTO_DEFAULT_LIMIT, - tag: string, -) => - sql` - SELECT * FROM photos - WHERE ${tag}=ANY(tags) - AND hidden IS NOT TRUE - ORDER BY taken_at DESC - LIMIT ${limit} - `; - -const sqlGetPhotosByCamera = async ( - limit = PHOTO_DEFAULT_LIMIT, - make: string, - model: string, -) => sql` - SELECT * FROM photos - WHERE - LOWER(make)=${parameterize(make)} AND - LOWER(REPLACE(model, ' ', '-'))=${parameterize(model)} - ORDER BY taken_at DESC - LIMIT ${limit} -`; - -const sqlGetPhotosBySimulation = async ( - limit = PHOTO_DEFAULT_LIMIT, - simulation: FilmSimulation, -) => sql` - SELECT * FROM photos - WHERE film_simulation=${simulation} - AND hidden IS NOT TRUE - ORDER BY taken_at DESC - LIMIT ${limit} -`; - -const sqlGetPhotosTakenAfterDateInclusive = ( - takenAt: Date, - limit?: number, -) => - sql` - SELECT * FROM photos - WHERE taken_at <= ${takenAt.toISOString()} - AND hidden IS NOT TRUE - ORDER BY taken_at DESC - LIMIT ${limit} - `; - -const sqlGetPhotosTakenBeforeDate = ( - takenAt: Date, - limit?: number, -) => - sql` - SELECT * FROM photos - WHERE taken_at > ${takenAt.toISOString()} - AND hidden IS NOT TRUE - ORDER BY taken_at ASC - LIMIT ${limit} - `; - const sqlGetPhoto = (id: string) => sql`SELECT * FROM photos WHERE id=${id} LIMIT 1`; @@ -367,6 +265,7 @@ const sqlGetUniqueFilmSimulations = async () => sql` export type GetPhotosOptions = { sortBy?: 'createdAt' | 'takenAt' | 'priority' limit?: number + offset?: number tag?: string camera?: Camera simulation?: FilmSimulation @@ -403,11 +302,11 @@ const safelyQueryPhotos = async (callback: () => Promise): Promise => { return result; }; -// PHOTOS export const getPhotos = async (options: GetPhotosOptions = {}) => { const { - sortBy = 'takenAt', - limit, + sortBy = PRIORITY_ORDER_ENABLED ? 'priority' : 'takenAt', + limit = PHOTO_DEFAULT_LIMIT, + offset = 0, tag, camera, simulation, @@ -416,30 +315,69 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => { includeHidden, } = options; - let getPhotosSql = () => sqlGetPhotos(limit); + let sql = 'SELECT * FROM photos'; - if (includeHidden) { - getPhotosSql = () => sqlGetPhotosIncludingHidden(limit); - } else if (takenBefore) { - getPhotosSql = () => sqlGetPhotosTakenBeforeDate(takenBefore, limit); - } else if (takenAfterInclusive) { - // eslint-disable-next-line max-len - getPhotosSql = () => sqlGetPhotosTakenAfterDateInclusive(takenAfterInclusive, limit); - } else if (tag) { - getPhotosSql = () => sqlGetPhotosByTag(limit, tag); - } else if (camera) { - getPhotosSql = () => sqlGetPhotosByCamera(limit, camera.make, camera.model); - } else if (simulation) { - getPhotosSql = () => sqlGetPhotosBySimulation(limit, simulation); - } else if (sortBy === 'createdAt') { - getPhotosSql = () => sqlGetPhotosSortedByCreatedAt(limit); - } else if (sortBy === 'priority') { - getPhotosSql = () => sqlGetPhotosSortedByPriority(limit); + // WHERE + let wheres = [] as string[]; + let values = [] as (string | number)[]; + let valueNumber = 1; + if (!includeHidden) { + wheres.push('hidden IS NOT TRUE'); + } + if (takenBefore) { + wheres.push(`taken_at > $${valueNumber++}`); + values.push(takenBefore.toISOString()); + } + if (takenAfterInclusive) { + wheres.push(`taken_at <= $${valueNumber++}`); + values.push(takenAfterInclusive.toISOString()); + } + if (tag) { + wheres.push(`$${valueNumber++}=ANY(tags)`); + values.push(tag); + } + if (camera) { + wheres.push(`LOWER(make)=$${valueNumber++}`); + wheres.push(`LOWER(REPLACE(model, ' ', '-'))=$${valueNumber++}`); + values.push(parameterize(camera.make)); + values.push(parameterize(camera.model)); + } + if (simulation) { + wheres.push(`film_simulation=$${valueNumber++}`); + values.push(simulation); + } + if (wheres.length > 0) { + sql += ` WHERE ${wheres.join(' AND ')}`; } - return safelyQueryPhotos(getPhotosSql) + // ORDER BY + switch (sortBy) { + case 'createdAt': + sql += ' ORDER BY created_at DESC'; + break; + case 'takenAt': + sql += ' ORDER BY taken_at DESC'; + break; + case 'priority': + sql += ' ORDER BY priority_order ASC, taken_at DESC'; + break; + } + + // LIMIT + OFFSET + sql += ` LIMIT $${valueNumber++} OFFSET $${valueNumber++}`; + values.push(limit, offset); + + const client = await db.connect(); + + console.log({ + sql, + values, + }); + + return safelyQueryPhotos(() => client.query(sql, values)) .then(({ rows }) => rows.map(parsePhotoFromDb)); }; + export const getPhoto = async (id: string): Promise => { // Check for photo id forwarding // and convert short ids to uuids diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index 11c22341..9d571f37 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -33,6 +33,7 @@ export default function SiteChecklistClient({ showFilmSimulations, isProModeEnabled, isGeoPrivacyEnabled, + isPriorityOrderEnabled, isPublicApiEnabled, isOgTextBottomAligned, showRefreshButton, @@ -256,6 +257,16 @@ export default function SiteChecklistClient({ collection/display of location-based data {renderEnvVars(['NEXT_PUBLIC_GEO_PRIVACY'])} + + Set environment variable to {'"1"'} to prevent + priority order photo field affecting photo order + {renderEnvVars(['NEXT_PUBLIC_IGNORE_PRIORITY_ORDER'])} +