Rebuild photo query engine, preferring priority order

This commit is contained in:
Sam Becker 2023-12-18 00:28:46 -06:00
parent 4438d0e1ee
commit 32c6260a3b
5 changed files with 80 additions and 126 deletions

View File

@ -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

1
src/cache/index.ts vendored
View File

@ -48,6 +48,7 @@ const getPhotosCacheKeyForOption = (
// Primitive keys
case 'sortBy':
case 'limit':
case 'offset':
case 'tag':
case 'simulation':
case 'includeHidden': {

View File

@ -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<PhotoDb>`
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<PhotoDb>`
SELECT * FROM photos
ORDER BY created_at DESC
LIMIT ${limit} OFFSET ${offset}
`;
const sqlGetPhotosSortedByCreatedAt = (
limit = PHOTO_DEFAULT_LIMIT,
offset = 0,
) =>
sql<PhotoDb>`
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<PhotoDb>`
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<PhotoDb>`
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<PhotoDb>`
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<PhotoDb>`
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<PhotoDb>`
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<PhotoDb>`
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<PhotoDb>`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 <T>(callback: () => Promise<T>): Promise<T> => {
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<Photo | undefined> => {
// Check for photo id forwarding
// and convert short ids to uuids

View File

@ -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'])}
</ChecklistRow>
<ChecklistRow
title="Priority Order"
status={isPriorityOrderEnabled}
isPending={isPendingPage}
optional
>
Set environment variable to {'"1"'} to prevent
priority order photo field affecting photo order
{renderEnvVars(['NEXT_PUBLIC_IGNORE_PRIORITY_ORDER'])}
</ChecklistRow>
<ChecklistRow
title="Public API"
status={isPublicApiEnabled}

View File

@ -49,6 +49,8 @@ export const HAS_AWS_S3_STORAGE =
export const PRO_MODE_ENABLED = process.env.NEXT_PUBLIC_PRO_MODE === '1';
export const GEO_PRIVACY_ENABLED = process.env.NEXT_PUBLIC_GEO_PRIVACY === '1';
export const PRIORITY_ORDER_ENABLED =
process.env.NEXT_PUBLIC_IGNORE_PRIORITY_ORDER !== '1';
export const PUBLIC_API_ENABLED = process.env.NEXT_PUBLIC_PUBLIC_API === '1';
export const SHOW_REPO_LINK = process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1';
export const SHOW_FILM_SIMULATIONS =
@ -77,6 +79,7 @@ export const CONFIG_CHECKLIST_STATUS = {
showFilmSimulations: SHOW_FILM_SIMULATIONS,
isProModeEnabled: PRO_MODE_ENABLED,
isGeoPrivacyEnabled: GEO_PRIVACY_ENABLED,
isPriorityOrderEnabled: PRIORITY_ORDER_ENABLED,
isPublicApiEnabled: PUBLIC_API_ENABLED,
isOgTextBottomAligned: OG_TEXT_BOTTOM_ALIGNMENT,
gridAspectRatio: GRID_ASPECT_RATIO,