Generalize photo meta queries, apply to tags
This commit is contained in:
parent
bc87d2ec0f
commit
14ee9b30c9
@ -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 }),
|
||||
]);
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 {...{
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
222
src/photo/db.ts
222
src/photo/db.ts
@ -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,126 +363,168 @@ 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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// LIMIT + OFFSET
|
||||
sql.push(`LIMIT $${valuesIndex++} OFFSET $${valuesIndex++}`);
|
||||
values.push(limit, offset);
|
||||
|
||||
return safelyQueryPhotos(async () => {
|
||||
return query(sql.join(' '), values);
|
||||
}, sql.join(' '))
|
||||
.then(({ rows }) => rows.map(parsePhotoFromDb));
|
||||
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);
|
||||
values.push(...wheresValues);
|
||||
}
|
||||
|
||||
sql.push(getOrderByFromOptions(options));
|
||||
|
||||
const {
|
||||
limitAndOffset,
|
||||
limitAndOffsetValues,
|
||||
} = getLimitAndOffsetFromOptions(options, valuesIndex);
|
||||
|
||||
// LIMIT + OFFSET
|
||||
sql.push(limitAndOffset);
|
||||
values.push(...limitAndOffsetValues);
|
||||
|
||||
return query(sql.join(' '), values);
|
||||
}, 'getPhotos')
|
||||
.then(({ rows }) => rows.map(parsePhotoFromDb));
|
||||
|
||||
export const getPhotosNearId = async (
|
||||
photoId: string,
|
||||
options: GetPhotosOptions,
|
||||
) => safelyQueryPhotos(async () => {
|
||||
const { limit } = options;
|
||||
) =>
|
||||
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,
|
||||
wheresValues,
|
||||
lastValuesIndex,
|
||||
} = getWheresFromOptions(options);
|
||||
|
||||
const { wheres, values, lastValuesIndex } = getWheresFromOptions(options);
|
||||
let valuesIndex = lastValuesIndex;
|
||||
|
||||
let valuesIndex = lastValuesIndex;
|
||||
return query(
|
||||
`
|
||||
WITH twi AS (
|
||||
SELECT *, row_number()
|
||||
OVER (${getOrderByFromOptions(options)}) as row_number
|
||||
FROM photos
|
||||
${wheres}
|
||||
),
|
||||
current AS (SELECT row_number FROM twi WHERE id = $${valuesIndex++})
|
||||
SELECT twi.*
|
||||
FROM twi, current
|
||||
WHERE twi.row_number >= current.row_number - 1
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
return query(
|
||||
`
|
||||
WITH twi AS (
|
||||
SELECT *, row_number()
|
||||
OVER (${orderBy}) as row_number
|
||||
FROM photos
|
||||
${wheres}
|
||||
),
|
||||
current AS (SELECT row_number FROM twi WHERE id = $${valuesIndex++})
|
||||
SELECT twi.*
|
||||
FROM twi, current
|
||||
WHERE twi.row_number >= current.row_number - 1
|
||||
LIMIT $${valuesIndex++}
|
||||
`,
|
||||
[...values, 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,
|
||||
};
|
||||
});
|
||||
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 }) => {
|
||||
return safelyQueryPhotos(() => limit
|
||||
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 = () =>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 }),
|
||||
]);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user