Generalize photo meta queries, apply to tags

This commit is contained in:
Sam Becker 2024-05-20 10:48:33 -05:00
parent bc87d2ec0f
commit 14ee9b30c9
9 changed files with 150 additions and 140 deletions

View File

@ -4,7 +4,7 @@ import { getPhotosCached } from '@/photo/cache';
import TagForm from '@/tag/TagForm';
import { PATH_ADMIN, PATH_ADMIN_TAGS, pathForTag } from '@/site/paths';
import PhotoLightbox from '@/photo/PhotoLightbox';
import { getPhotosTagMeta } from '@/photo/db';
import { getPhotosMeta } from '@/photo/db';
import AdminTagBadge from '@/admin/AdminTagBadge';
const MAX_PHOTO_TO_SHOW = 6;
@ -22,7 +22,7 @@ export default async function PhotoPageEdit({
{ count },
photos,
] = await Promise.all([
getPhotosTagMeta(tag),
getPhotosMeta({ tag }),
getPhotosCached({ tag, limit: MAX_PHOTO_TO_SHOW }),
]);

View File

@ -11,11 +11,9 @@ import {
absolutePathForPhotoImage,
} from '@/site/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import {
getPhotosNearIdCached,
getPhotosTagMetaCached,
} from '@/photo/cache';
import { getPhotosNearIdCached } from '@/photo/cache';
import { ReactNode, cache } from 'react';
import { getPhotosMeta } from '@/photo/db';
const getPhotosNearIdCachedCached = cache((photoId: string, tag: string) =>
getPhotosNearIdCached(
@ -66,7 +64,7 @@ export default async function PhotoTagPage({
if (!photo) { redirect(PATH_ROOT); }
const { count, dateRange } = await getPhotosTagMetaCached(tag);
const { count, dateRange } = await getPhotosMeta({ tag });
return <>
{children}

View File

@ -6,8 +6,8 @@ import {
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import {
getPhotosNearIdCached,
getPhotosTagHiddenMetaCached,
} from '@/photo/cache';
import { getPhotosMeta } from '@/photo/db';
import { PATH_ROOT, absolutePathForPhoto } from '@/site/paths';
import { TAG_HIDDEN } from '@/tag';
import { Metadata } from 'next';
@ -59,7 +59,7 @@ export default async function PhotoTagHiddenPage({
if (!photo) { redirect(PATH_ROOT); }
const { count, dateRange } = await getPhotosTagHiddenMetaCached();
const { count, dateRange } = await getPhotosMeta({ hidden: 'only' });
return (
<PhotoDetailPage {...{

View File

@ -3,17 +3,18 @@ import Banner from '@/components/Banner';
import SiteGrid from '@/components/SiteGrid';
import PhotoGrid from '@/photo/PhotoGrid';
import { getPhotosNoStore } from '@/photo/cache';
import { getPhotosTagHiddenMeta } from '@/photo/db';
import { getPhotosMeta } from '@/photo/db';
import { absolutePathForTag } from '@/site/paths';
import { TAG_HIDDEN, descriptionForTaggedPhotos, titleForTag } from '@/tag';
import HiddenHeader from '@/tag/HiddenHeader';
import { Metadata } from 'next';
import { cache } from 'react';
const getPhotosTagHiddenMetaCached = cache(getPhotosTagHiddenMeta);
const getPhotosHiddenMetaCached = cache(() =>
getPhotosMeta({ hidden: 'only' }));
export async function generateMetadata(): Promise<Metadata> {
const { count, dateRange } = await getPhotosTagHiddenMetaCached();
const { count, dateRange } = await getPhotosHiddenMetaCached();
if (count === 0) { return {}; }
@ -47,7 +48,7 @@ export default async function HiddenTagPage() {
{ count, dateRange },
] = await Promise.all([
getPhotosNoStore({ hidden: 'only' }),
getPhotosTagHiddenMetaCached(),
getPhotosHiddenMetaCached(),
]);
return (

View File

@ -22,7 +22,7 @@ import {
} from '@/services/storage';
import {
getPhotosCached,
getPhotosTagHiddenMetaCached,
getPhotosMetaCached,
revalidateAdminPaths,
revalidateAllKeysAndPaths,
revalidatePhoto,
@ -205,21 +205,20 @@ export const streamAiImageQueryAction = async (
export const getImageBlurAction = async (url: string) =>
runAuthenticatedAdminServerAction(() => blurImageFromUrl(url));
export const getPhotosTagHiddenMetaCachedAction = async () =>
runAuthenticatedAdminServerAction(getPhotosTagHiddenMetaCached);
export const getPhotosHiddenMetaCachedAction = async () =>
runAuthenticatedAdminServerAction(() =>
getPhotosMetaCached({ hidden: 'only' }));
// Public/Private actions
export const getPhotosAction = async (options: GetPhotosOptions) =>
(options.hidden === 'include' || options.hidden === 'only')
? runAuthenticatedAdminServerAction(() =>
getPhotos(options))
? runAuthenticatedAdminServerAction(() => getPhotos(options))
: getPhotos(options);
export const getPhotosCachedAction = async (options: GetPhotosOptions) =>
(options.hidden === 'include' || options.hidden === 'only')
? runAuthenticatedAdminServerAction(() =>
getPhotosCached (options))
? runAuthenticatedAdminServerAction(() => getPhotosCached (options))
: getPhotosCached(options);
// Public actions

View File

@ -12,7 +12,6 @@ import {
getPhotosCountIncludingHidden,
getUniqueCameras,
getUniqueTags,
getPhotosTagMeta,
getPhotosCameraMeta,
getUniqueTagsHidden,
getUniqueFilmSimulations,
@ -20,7 +19,7 @@ import {
getPhotosDateRange,
getPhotosNearId,
getPhotosMostRecentUpdate,
getPhotosTagHiddenMeta,
getPhotosMeta,
} from '@/photo/db';
import { parseCachedPhotoDates, parseCachedPhotosDates } from '@/photo';
import { createCameraKey } from '@/camera';
@ -159,6 +158,13 @@ export const getPhotosNearIdCached = (
};
});
export const getPhotosMetaCached = (
...args: Parameters<typeof getPhotosMeta>
) => unstable_cache(
getPhotosMeta,
[KEY_PHOTOS, KEY_COUNT, KEY_DATE_RANGE, ...getPhotosCacheKeys(...args)],
)(...args);
export const getPhotosDateRangeCached =
unstable_cache(
getPhotosDateRange,
@ -183,18 +189,6 @@ export const getPhotosMostRecentUpdateCached =
[KEY_PHOTOS, KEY_COUNT, KEY_DATE_RANGE],
);
export const getPhotosTagMetaCached =
unstable_cache(
getPhotosTagMeta,
[KEY_PHOTOS, KEY_TAGS, KEY_DATE_RANGE],
);
export const getPhotosTagHiddenMetaCached =
unstable_cache(
getPhotosTagHiddenMeta,
[KEY_PHOTOS, KEY_TAGS, KEY_HIDDEN, KEY_DATE_RANGE],
);
export const getPhotosCameraMetaCached =
unstable_cache(
getPhotosCameraMeta,

View File

@ -199,29 +199,6 @@ const sqlGetPhotosDateRange = async () => sql`
? rows[0] as PhotoDateRange
: undefined);
const sqlGetPhotosTagMeta = async (tag: string) => sql`
SELECT COUNT(*), MIN(taken_at_naive) as start, MAX(taken_at_naive) as end
FROM photos
WHERE ${tag}=ANY(tags) AND
hidden IS NOT TRUE
`.then(({ rows }) => ({
count: parseInt(rows[0].count, 10),
...rows[0]?.start && rows[0]?.end
? { dateRange: rows[0] as PhotoDateRange }
: undefined,
}));
const sqlGetPhotosTagHiddenMeta = async () => sql`
SELECT COUNT(*), MIN(taken_at_naive) as start, MAX(taken_at_naive) as end
FROM photos
WHERE hidden IS TRUE
`.then(({ rows }) => ({
count: parseInt(rows[0].count, 10),
...rows[0]?.start && rows[0]?.end
? { dateRange: rows[0] as PhotoDateRange }
: undefined,
}));
const sqlGetPhotosCameraMeta = async (camera: Camera) => sql`
SELECT COUNT(*), MIN(taken_at_naive) as start, MAX(taken_at_naive) as end
FROM photos
@ -358,7 +335,10 @@ const safelyQueryPhotos = async <T>(
return result;
};
const getWheresFromOptions = (options: GetPhotosOptions) => {
const getWheresFromOptions = (
options: GetPhotosOptions,
initialValuesIndex = 1,
) => {
const {
hidden = 'exclude',
takenBefore,
@ -370,8 +350,8 @@ const getWheresFromOptions = (options: GetPhotosOptions) => {
} = options;
const wheres = [] as string[];
const values = [] as (string | number)[];
let valuesIndex = 1;
const wheresValues = [] as (string | number)[];
let valuesIndex = initialValuesIndex;
switch (hidden) {
case 'exclude':
@ -383,90 +363,118 @@ const getWheresFromOptions = (options: GetPhotosOptions) => {
}
if (takenBefore) {
wheres.push(`taken_at > $${valuesIndex++}`);
values.push(takenBefore.toISOString());
wheresValues.push(takenBefore.toISOString());
}
if (takenAfterInclusive) {
wheres.push(`taken_at <= $${valuesIndex++}`);
values.push(takenAfterInclusive.toISOString());
wheresValues.push(takenAfterInclusive.toISOString());
}
if (query) {
// eslint-disable-next-line max-len
wheres.push(`CONCAT(title, ' ', caption, ' ', semantic_description) ILIKE $${valuesIndex++}`);
values.push(`%${query.toLocaleLowerCase()}%`);
wheresValues.push(`%${query.toLocaleLowerCase()}%`);
}
if (tag) {
wheres.push(`$${valuesIndex++}=ANY(tags)`);
values.push(tag);
wheresValues.push(tag);
}
if (camera) {
wheres.push(`LOWER(REPLACE(make, ' ', '-'))=$${valuesIndex++}`);
wheres.push(`LOWER(REPLACE(model, ' ', '-'))=$${valuesIndex++}`);
values.push(parameterize(camera.make, true));
values.push(parameterize(camera.model, true));
wheresValues.push(parameterize(camera.make, true));
wheresValues.push(parameterize(camera.model, true));
}
if (simulation) {
wheres.push(`film_simulation=$${valuesIndex++}`);
values.push(simulation);
wheresValues.push(simulation);
}
return {
wheres: wheres.length > 0
? `WHERE ${wheres.join(' AND ')}`
: '',
values,
wheresValues,
lastValuesIndex: valuesIndex,
};
};
export const getPhotos = async (options: GetPhotosOptions = {}) => {
const getOrderByFromOptions = (options: GetPhotosOptions) => {
const {
sortBy = PRIORITY_ORDER_ENABLED ? 'priority' : 'takenAt',
} = options;
switch (sortBy) {
case 'createdAt':
return 'ORDER BY created_at DESC';
case 'takenAt':
return 'ORDER BY taken_at DESC';
case 'priority':
return 'ORDER BY priority_order ASC, taken_at DESC';
}
};
const getLimitAndOffsetFromOptions = (
options: GetPhotosOptions,
initialValuesIndex = 1,
) => {
const {
limit = PHOTO_DEFAULT_LIMIT,
offset = 0,
} = options;
let sql = ['SELECT * FROM photos'];
let valuesIndex = initialValuesIndex;
const { wheres, values, lastValuesIndex } = getWheresFromOptions(options);
return {
limitAndOffset: `LIMIT $${valuesIndex++} OFFSET $${valuesIndex++}`,
limitAndOffsetValues: [limit, offset],
};
};
export const getPhotos = async (options: GetPhotosOptions = {}) =>
safelyQueryPhotos(async () => {
const sql = ['SELECT * FROM photos'];
const values = [] as (string | number)[];
const {
wheres,
wheresValues,
lastValuesIndex,
} = getWheresFromOptions(options);
let valuesIndex = lastValuesIndex;
if (wheres) { sql.push(wheres); }
// ORDER BY
switch (sortBy) {
case 'createdAt':
sql.push('ORDER BY created_at DESC');
break;
case 'takenAt':
sql.push('ORDER BY taken_at DESC');
break;
case 'priority':
sql.push('ORDER BY priority_order ASC, taken_at DESC');
break;
if (wheres) {
sql.push(wheres);
values.push(...wheresValues);
}
// LIMIT + OFFSET
sql.push(`LIMIT $${valuesIndex++} OFFSET $${valuesIndex++}`);
values.push(limit, offset);
sql.push(getOrderByFromOptions(options));
const {
limitAndOffset,
limitAndOffsetValues,
} = getLimitAndOffsetFromOptions(options, valuesIndex);
// LIMIT + OFFSET
sql.push(limitAndOffset);
values.push(...limitAndOffsetValues);
return safelyQueryPhotos(async () => {
return query(sql.join(' '), values);
}, sql.join(' '))
}, 'getPhotos')
.then(({ rows }) => rows.map(parsePhotoFromDb));
};
export const getPhotosNearId = async (
photoId: string,
options: GetPhotosOptions,
) => safelyQueryPhotos(async () => {
) =>
safelyQueryPhotos(async () => {
const { limit } = options;
const orderBy = PRIORITY_ORDER_ENABLED
? 'ORDER BY priority_order ASC, taken_at DESC'
: 'ORDER BY taken_at DESC';
const { wheres, values, lastValuesIndex } = getWheresFromOptions(options);
const {
wheres,
wheresValues,
lastValuesIndex,
} = getWheresFromOptions(options);
let valuesIndex = lastValuesIndex;
@ -474,7 +482,7 @@ export const getPhotosNearId = async (
`
WITH twi AS (
SELECT *, row_number()
OVER (${orderBy}) as row_number
OVER (${getOrderByFromOptions(options)}) as row_number
FROM photos
${wheres}
),
@ -484,9 +492,9 @@ export const getPhotosNearId = async (
WHERE twi.row_number >= current.row_number - 1
LIMIT $${valuesIndex++}
`,
[...values, photoId, limit]
[...wheresValues, photoId, limit]
);
}, `getPhotosNearId: ${photoId}`)
}, `getPhotosNearId: ${photoId}`)
.then(({ rows }) => {
const photo = rows.find(({ id }) => id === photoId);
const indexNumber = photo ? parseInt(photo.row_number) : undefined;
@ -496,13 +504,27 @@ export const getPhotosNearId = async (
};
});
export const getPhotoIds = async ({ limit }: { limit?: number }) => {
return safelyQueryPhotos(() => limit
export const getPhotosMeta = (options: GetPhotosOptions = {}) =>
safelyQueryPhotos(async () => {
// eslint-disable-next-line max-len
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,
}));
export const getPhotoIds = async ({ limit }: { limit?: number }) =>
safelyQueryPhotos(() => limit
? sql`SELECT id FROM photos LIMIT ${limit}`
: sql`SELECT id FROM photos`,
'getPhotoIds')
.then(({ rows }) => rows.map(({ id }) => id as string));
};
export const getPhoto = async (
id: string,
@ -536,10 +558,6 @@ export const getUniqueTags = () =>
safelyQueryPhotos(sqlGetUniqueTags, 'getUniqueTags');
export const getUniqueTagsHidden = () =>
safelyQueryPhotos(sqlGetUniqueTagsHidden, 'getUniqueTagsHidden');
export const getPhotosTagMeta = (tag: string) =>
safelyQueryPhotos(() => sqlGetPhotosTagMeta(tag), 'getPhotosTagMeta');
export const getPhotosTagHiddenMeta = () =>
safelyQueryPhotos(sqlGetPhotosTagHiddenMeta, 'sqlGetPhotosTagHiddenMeta');
// CAMERAS
export const getUniqueCameras = () =>

View File

@ -7,7 +7,7 @@ import usePathnames from '@/utility/usePathnames';
import { getAuthAction, logClientAuthUpdate } from '@/auth/actions';
import useSWR from 'swr';
import { MATTE_PHOTOS } from '@/site/config';
import { getPhotosTagHiddenMetaCachedAction } from '@/photo/actions';
import { getPhotosHiddenMetaCachedAction } from '@/photo/actions';
export default function AppStateProvider({
children,
@ -53,7 +53,7 @@ export default function AppStateProvider({
useEffect(() => {
if (isUserSignedIn) {
const timeout = setTimeout(() =>
getPhotosTagHiddenMetaCachedAction().then(({ count }) =>
getPhotosHiddenMetaCachedAction().then(({ count }) =>
setHiddenPhotosCount(count))
, 100);
return () => clearTimeout(timeout);

View File

@ -1,6 +1,6 @@
import {
getPhotosCached,
getPhotosTagMetaCached,
getPhotosMetaCached,
} from '@/photo/cache';
export const getPhotosTagDataCached = ({
@ -12,6 +12,6 @@ export const getPhotosTagDataCached = ({
}) =>
Promise.all([
getPhotosCached({ tag, limit }),
getPhotosTagMetaCached(tag),
getPhotosMetaCached({ tag }),
]);