diff --git a/src/app/(auth-state)/admin/photos/page.tsx b/src/app/(auth-state)/admin/photos/page.tsx index a970445e..2521b796 100644 --- a/src/app/(auth-state)/admin/photos/page.tsx +++ b/src/app/(auth-state)/admin/photos/page.tsx @@ -33,13 +33,18 @@ export default async function AdminPage({ }) { const { offset, limit } = getPhotosLimitForQuery(searchParams.next); - const photos = await getPhotos('createdAt', limit); - - const count = await getPhotosCount(); + const [ + photos, + count, + blobUploadUrls, + ] = await Promise.all([ + await getPhotos({ sortBy: 'createdAt', limit }), + await getPhotosCount(), + await getBlobUploadUrls(), + ]); const showMorePhotos = count > photos.length; - const blobUploadUrls = await getBlobUploadUrls(); const blobPhotoUrls = DEBUG_PHOTO_BLOBS ? await getBlobPhotoUrls() : []; diff --git a/src/app/(static)/grid/page.tsx b/src/app/(static)/grid/page.tsx index 73af56ec..33f8207b 100644 --- a/src/app/(static)/grid/page.tsx +++ b/src/app/(static)/grid/page.tsx @@ -1,17 +1,22 @@ +import { + getPhotosCached, + getPhotosCountCached, + getUniqueTagsCached, +} from '@/cache'; import AnimateItems from '@/components/AnimateItems'; import MorePhotos from '@/components/MorePhotos'; import SiteGrid from '@/components/SiteGrid'; import { generateOgImageMetaForPhotos, getPhotosLimitForQuery } from '@/photo'; import PhotoGrid from '@/photo/PhotoGrid'; import PhotosEmptyState from '@/photo/PhotosEmptyState'; -import { getPhotos, getPhotosCount, getUniqueTags } from '@/services/postgres'; +import { MAX_PHOTOS_TO_SHOW_HOME } from '@/photo/image-response'; import PhotoTag from '@/tag/PhotoTag'; import { Metadata } from 'next'; export const runtime = 'edge'; export async function generateMetadata(): Promise { - const photos = await getPhotos(); + const photos = await getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_HOME}); return generateOgImageMetaForPhotos(photos); } @@ -22,11 +27,11 @@ export default async function GridPage({ }) { const { offset, limit } = getPhotosLimitForQuery(searchParams.next); - const photos = await getPhotos(undefined, limit); + const photos = await getPhotosCached({ limit }); - const count = await getPhotosCount(); + const count = await getPhotosCountCached(); - const tags = await getUniqueTags(); + const tags = await getUniqueTagsCached(); const showMorePhotos = count > photos.length; diff --git a/src/app/(static)/home-image/route.tsx b/src/app/(static)/home-image/route.tsx index cc160399..564d5932 100644 --- a/src/app/(static)/home-image/route.tsx +++ b/src/app/(static)/home-image/route.tsx @@ -1,20 +1,18 @@ import { auth } from '@/auth'; -import { getImageCacheHeadersForAuth } from '@/cache'; +import { getImageCacheHeadersForAuth, getPhotosCached } from '@/cache'; import { IMAGE_OG_SMALL_SIZE, MAX_PHOTOS_TO_SHOW_HOME, } from '@/photo/image-response'; import HomeImageResponse from '@/photo/image-response/HomeImageResponse'; -import { getPhotos } from '@/services/postgres'; import { ImageResponse } from 'next/server'; export const runtime = 'edge'; export async function GET(request: Request) { - const photos = await getPhotos( - undefined, - MAX_PHOTOS_TO_SHOW_HOME, - ); + const photos = await getPhotosCached({ + limit: MAX_PHOTOS_TO_SHOW_HOME, + }); const headers = await getImageCacheHeadersForAuth(await auth()); diff --git a/src/app/(static)/og/page.tsx b/src/app/(static)/og/page.tsx index 3183cefb..d5d22e3d 100644 --- a/src/app/(static)/og/page.tsx +++ b/src/app/(static)/og/page.tsx @@ -1,7 +1,7 @@ +import { getPhotosCached, getPhotosCountCached } from '@/cache'; import MorePhotos from '@/components/MorePhotos'; import { getPhotosLimitForQuery } from '@/photo'; import StaggeredOgPhotos from '@/photo/StaggeredOgPhotos'; -import { getPhotos, getPhotosCount } from '@/services/postgres'; export const runtime = 'edge'; @@ -12,9 +12,9 @@ export default async function GridPage({ }) { const { offset, limit } = getPhotosLimitForQuery(searchParams.next); - const photos = await getPhotos(undefined, limit); + const photos = await getPhotosCached({ limit }); - const count = await getPhotosCount(); + const count = await getPhotosCountCached(); const showMorePhotos = count > photos.length; diff --git a/src/app/(static)/p/[photoId]/image/route.tsx b/src/app/(static)/p/[photoId]/image/route.tsx index 088fcd37..282fd7c3 100644 --- a/src/app/(static)/p/[photoId]/image/route.tsx +++ b/src/app/(static)/p/[photoId]/image/route.tsx @@ -1,15 +1,14 @@ import { auth } from '@/auth'; -import { getImageCacheHeadersForAuth } from '@/cache'; +import { getImageCacheHeadersForAuth, getPhotoCached } from '@/cache'; import { IMAGE_OG_SIZE } from '@/photo/image-response'; import PhotoImageResponse from '@/photo/image-response/PhotoImageResponse'; -import { getPhoto } from '@/services/postgres'; import { getIBMPlexMonoMedium } from '@/site/font'; import { ImageResponse } from 'next/server'; export const runtime = 'edge'; export async function GET(request: Request, context: any){ - const photo = await getPhoto(context.params.photoId); + const photo = await getPhotoCached(context.params.photoId); const { fontFamily, diff --git a/src/app/(static)/p/[photoId]/layout.tsx b/src/app/(static)/p/[photoId]/layout.tsx index daa2200a..acbc4986 100644 --- a/src/app/(static)/p/[photoId]/layout.tsx +++ b/src/app/(static)/p/[photoId]/layout.tsx @@ -4,14 +4,10 @@ import { titleForPhoto, } from '@/photo'; import { Metadata } from 'next'; -import { - getPhoto, - getPhotosTakenAfterPhotoInclusive, - getPhotosTakenBeforePhoto, -} from '@/services/postgres'; import { redirect } from 'next/navigation'; import { absolutePathForPhoto, absolutePathForPhotoImage } from '@/site/paths'; import PhotoDetailPage from '@/photo/PhotoDetailPage'; +import { getPhotoCached, getPhotosCached } from '@/cache'; export const runtime = 'edge'; @@ -20,7 +16,7 @@ export async function generateMetadata({ }: { params: { photoId: string } }): Promise { - const photo = await getPhoto(photoId); + const photo = await getPhotoCached(photoId); if (!photo) { return {}; } @@ -54,15 +50,21 @@ export default async function PhotoPage({ params: { photoId: string } children: React.ReactNode }) { - const photo = await getPhoto(photoId); + const photo = await getPhotoCached(photoId); if (!photo) { redirect('/'); } - const photosBefore = await getPhotosTakenBeforePhoto(photo, 1); - const photosAfter = await getPhotosTakenAfterPhotoInclusive( - photo, - GRID_THUMBNAILS_TO_SHOW_MAX + 1, - ); + const [ + photosBefore, + photosAfter, + ] = await Promise.all([ + getPhotosCached({ takenBefore: photo.takenAt, limit: 1 }), + getPhotosCached({ + takenAfterInclusive: photo.takenAt, + limit: GRID_THUMBNAILS_TO_SHOW_MAX + 1, + }), + ]); + const photos = photosBefore.concat(photosAfter); return <> diff --git a/src/app/(static)/p/[photoId]/share/page.tsx b/src/app/(static)/p/[photoId]/share/page.tsx index 47a309bb..811af5a2 100644 --- a/src/app/(static)/p/[photoId]/share/page.tsx +++ b/src/app/(static)/p/[photoId]/share/page.tsx @@ -1,5 +1,5 @@ +import { getPhotoCached } from '@/cache'; import PhotoModal from '@/photo/PhotoModal'; -import { getPhoto } from '@/services/postgres'; import { redirect } from 'next/navigation'; export const runtime = 'edge'; @@ -9,7 +9,7 @@ export default async function Share({ }: { params: { photoId: string } }) { - const photo = await getPhoto(photoId); + const photo = await getPhotoCached(photoId); if (!photo) { return redirect('/'); } diff --git a/src/app/(static)/page.tsx b/src/app/(static)/page.tsx index 4c40c5ae..41467ea0 100644 --- a/src/app/(static)/page.tsx +++ b/src/app/(static)/page.tsx @@ -1,16 +1,16 @@ +import { getPhotosCached, getPhotosCountCached } from '@/cache'; import AnimateItems from '@/components/AnimateItems'; import MorePhotos from '@/components/MorePhotos'; import SiteGrid from '@/components/SiteGrid'; import { generateOgImageMetaForPhotos, getPhotosLimitForQuery } from '@/photo'; import PhotoLarge from '@/photo/PhotoLarge'; import PhotosEmptyState from '@/photo/PhotosEmptyState'; -import { getPhotos, getPhotosCount } from '@/services/postgres'; import { Metadata } from 'next'; -export const dynamic = 'force-static'; +export const runtime = 'edge'; export async function generateMetadata(): Promise { - const photos = await getPhotos(); + const photos = await getPhotosCached(); return generateOgImageMetaForPhotos(photos); } @@ -21,9 +21,9 @@ export default async function HomePage({ }) { const { offset, limit } = getPhotosLimitForQuery(searchParams.next, 12); - const photos = await getPhotos(undefined, limit); + const photos = await getPhotosCached({ limit }); - const count = await getPhotosCount(); + const count = await getPhotosCountCached(); const showMorePhotos = count > photos.length; diff --git a/src/app/(static)/t/[tag]/[photoId]/layout.tsx b/src/app/(static)/t/[tag]/[photoId]/layout.tsx index 8a7a6487..8cdb0448 100644 --- a/src/app/(static)/t/[tag]/[photoId]/layout.tsx +++ b/src/app/(static)/t/[tag]/[photoId]/layout.tsx @@ -3,13 +3,10 @@ import { titleForPhoto, } from '@/photo'; import { Metadata } from 'next'; -import { - getPhoto, - getPhotos, -} from '@/services/postgres'; import { redirect } from 'next/navigation'; import { absolutePathForPhoto, absolutePathForPhotoImage } from '@/site/paths'; import PhotoDetailPage from '@/photo/PhotoDetailPage'; +import { getPhotoCached, getPhotosCached } from '@/cache'; export const runtime = 'edge'; @@ -18,7 +15,7 @@ export async function generateMetadata({ }: { params: { photoId: string, tag: string } }): Promise { - const photo = await getPhoto(photoId); + const photo = await getPhotoCached(photoId); if (!photo) { return {}; } @@ -52,11 +49,11 @@ export default async function PhotoTagPage({ params: { photoId: string, tag: string } children: React.ReactNode }) { - const photo = await getPhoto(photoId); + const photo = await getPhotoCached(photoId); if (!photo) { redirect('/'); } - const photos = await getPhotos(undefined, undefined, undefined, tag); + const photos = await getPhotosCached({ tag }); return <> {children} diff --git a/src/app/(static)/t/[tag]/[photoId]/share/page.tsx b/src/app/(static)/t/[tag]/[photoId]/share/page.tsx index 682d5a0a..b6480874 100644 --- a/src/app/(static)/t/[tag]/[photoId]/share/page.tsx +++ b/src/app/(static)/t/[tag]/[photoId]/share/page.tsx @@ -1,5 +1,5 @@ +import { getPhotoCached } from '@/cache'; import PhotoModal from '@/photo/PhotoModal'; -import { getPhoto } from '@/services/postgres'; import { redirect } from 'next/navigation'; export const runtime = 'edge'; @@ -9,7 +9,7 @@ export default async function Share({ }: { params: { photoId: string, tag: string } }) { - const photo = await getPhoto(photoId); + const photo = await getPhotoCached(photoId); if (!photo) { return redirect('/'); } diff --git a/src/app/(static)/t/[tag]/image/route.tsx b/src/app/(static)/t/[tag]/image/route.tsx index 8429efb9..c09e98c3 100644 --- a/src/app/(static)/t/[tag]/image/route.tsx +++ b/src/app/(static)/t/[tag]/image/route.tsx @@ -1,23 +1,20 @@ import { auth } from '@/auth'; -import { getImageCacheHeadersForAuth } from '@/cache'; +import { getImageCacheHeadersForAuth, getPhotosCached } from '@/cache'; import { IMAGE_OG_SMALL_SIZE, MAX_PHOTOS_TO_SHOW_PER_TAG, } from '@/photo/image-response'; import TagImageResponse from '@/photo/image-response/TagImageResponse'; -import { getPhotos } from '@/services/postgres'; import { getIBMPlexMonoMedium } from '@/site/font'; import { ImageResponse } from 'next/server'; export const runtime = 'edge'; export async function GET(request: Request, context: any) { - const photos = await getPhotos( - undefined, - MAX_PHOTOS_TO_SHOW_PER_TAG, - undefined, - context.params.tag, - ); + const photos = await getPhotosCached({ + limit: MAX_PHOTOS_TO_SHOW_PER_TAG, + tag: context.params.tag, + }); const { fontFamily, diff --git a/src/app/(static)/t/[tag]/page.tsx b/src/app/(static)/t/[tag]/page.tsx index 99ae42a4..aa8633a4 100644 --- a/src/app/(static)/t/[tag]/page.tsx +++ b/src/app/(static)/t/[tag]/page.tsx @@ -1,6 +1,6 @@ +import { getPhotosCached } from '@/cache'; import SiteGrid from '@/components/SiteGrid'; import PhotoGrid from '@/photo/PhotoGrid'; -import { getPhotos } from '@/services/postgres'; import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths'; import { descriptionForTaggedPhotos, titleForTag } from '@/tag'; import TagHeader from '@/tag/TagHeader'; @@ -13,7 +13,7 @@ interface TagProps { export async function generateMetadata({ params: { tag }, }: TagProps): Promise { - const photos = await getPhotos(undefined, undefined, undefined, tag); + const photos = await getPhotosCached({ tag }); const url = absolutePathForTag(tag); const title = titleForTag(tag, photos); @@ -38,7 +38,7 @@ export async function generateMetadata({ } export default async function TagPage({ params: { tag } }: TagProps) { - const photos = await getPhotos(undefined, undefined, undefined, tag); + const photos = await getPhotosCached({ tag }); return ( { + const tags = [TAG_PHOTOS]; + + const { + sortBy, + limit, + offset, + tag, + takenAfterInclusive, + takenBefore, + } = options; + + if (sortBy !== undefined) { tags.push(`sortBy-${sortBy}`); } + if (limit !== undefined) { tags.push(`limit-${sortBy}`); } + if (offset !== undefined) { tags.push(`offset-${sortBy}`); } + if (tag !== undefined) { tags.push(`tag-${sortBy}`); } + // eslint-disable-next-line max-len + if (takenBefore !== undefined) { tags.push(`takenBefore-${takenBefore.toISOString()}`); } + // eslint-disable-next-line max-len + if (takenAfterInclusive !== undefined) { tags.push(`takenAfterInclusive-${takenAfterInclusive.toISOString()}`); } + + return tags; +}; const tagForPhoto = (photoId: string) => `photo-${photoId}`; -export const revalidatePhotosTag = ( - includePhotoPaths?: boolean -) => { +export const revalidatePhotosTag = () => revalidateTag(TAG_PHOTOS); - if (includePhotoPaths) { revalidateAllPhotoPaths(); } -}; - -export const revalidateAllPhotoPaths = () => - PHOTO_PATHS.forEach(path => revalidatePath(path)); export const getPhotosCached: typeof getPhotos = (...args) => unstable_cache( () => getPhotos(...args), - [TAG_PHOTOS], { - tags: [TAG_PHOTOS], + getPhotosCacheTags(...args), { + tags: getPhotosCacheTags(...args), + } + )().then(parseCachedPhotosDates); + +export const getPhotosCountCached: typeof getPhotosCount = (...args) => + unstable_cache( + () => getPhotosCount(...args), + [TAG_PHOTOS, TAG_PHOTOS_COUNT], { + tags: [TAG_PHOTOS, TAG_PHOTOS_COUNT], } )(); @@ -44,6 +64,14 @@ export const getPhotoCached: typeof getPhoto = (...args) => [TAG_PHOTOS, tagForPhoto(...args)], { tags: [TAG_PHOTOS, tagForPhoto(...args)], } + )().then(photo => photo ? parseCachedPhotoDates(photo) : undefined); + +export const getUniqueTagsCached: typeof getUniqueTags = (...args) => + unstable_cache( + () => getUniqueTags(...args), + [TAG_PHOTOS, TAG_TAGS], { + tags: [TAG_PHOTOS, TAG_TAGS], + } )(); export const getImageCacheHeadersForAuth = async (session?: Session) => { diff --git a/src/middleware.ts b/src/middleware.ts index 17e4aa78..7210ac03 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,6 +1,6 @@ import { auth } from './auth'; import { NextRequest, NextResponse } from 'next/server'; -import { NextApiRequest, NextApiResponse } from 'next'; +import type { NextApiRequest, NextApiResponse } from 'next'; export default function middleware(req: NextRequest, res:NextResponse) { const pathname = req.nextUrl.pathname; diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 0d433596..490f266a 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -25,7 +25,7 @@ export async function createPhotoAction(formData: FormData) { if (updatedUrl) { photo.url = updatedUrl; } await sqlInsertPhotoIntoDb(photo); - revalidatePhotosTag(true); + revalidatePhotosTag(); redirect('/admin/photos'); } @@ -35,7 +35,7 @@ export async function updatePhotoAction(formData: FormData) { await sqlUpdatePhotoInDb(photo); - revalidatePhotosTag(true); + revalidatePhotosTag(); redirect('/admin/photos'); } @@ -44,7 +44,7 @@ export async function deletePhotoAction(formData: FormData) { await deleteBlobPhoto(formData.get('url') as string); await sqlDeletePhoto(formData.get('id') as string); - revalidatePhotosTag(true); + revalidatePhotosTag(); }; export async function deleteBlobPhotoAction(formData: FormData) { diff --git a/src/photo/index.ts b/src/photo/index.ts index e1d86e49..31407a19 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -91,6 +91,16 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => { }; }; +export const parseCachedPhotoDates = (photo: Photo) => ({ + ...photo, + takenAt: new Date(photo.takenAt), + updatedAt: new Date(photo.updatedAt), + createdAt: new Date(photo.createdAt), +}); + +export const parseCachedPhotosDates = (photos: Photo[]) => + photos.map(parseCachedPhotoDates); + export const convertPhotoToPhotoDbInsert = ( photo: Photo, ): PhotoDbInsert => ({ diff --git a/src/services/postgres.ts b/src/services/postgres.ts index edb0117c..316ceab7 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -136,8 +136,7 @@ const sqlGetPhotosFromDb = ( SELECT * FROM photos ORDER BY taken_at DESC LIMIT ${limit} OFFSET ${offset} - ` - .then(({ rows }) => rows.map(parsePhotoFromDb)); + `; const sqlGetPhotosFromDbSortedByCreatedAt = ( limit = PHOTO_DEFAULT_LIMIT, @@ -147,8 +146,7 @@ const sqlGetPhotosFromDbSortedByCreatedAt = ( SELECT * FROM photos ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset} - ` - .then(({ rows }) => rows.map(parsePhotoFromDb)); + `; const sqlGetPhotosFromDbSortedByPriority = ( limit = PHOTO_DEFAULT_LIMIT, @@ -158,8 +156,7 @@ const sqlGetPhotosFromDbSortedByPriority = ( SELECT * FROM photos ORDER BY priority_order ASC, taken_at DESC LIMIT ${limit} OFFSET ${offset} - ` - .then(({ rows }) => rows.map(parsePhotoFromDb)); + `; const sqlGetPhotosFromDbByTag = ( limit = PHOTO_DEFAULT_LIMIT, @@ -170,43 +167,83 @@ const sqlGetPhotosFromDbByTag = ( SELECT * FROM photos WHERE ${tag}=ANY(tags) ORDER BY taken_at ASC LIMIT ${limit} OFFSET ${offset} - ` - .then(({ rows }) => rows.map(parsePhotoFromDb)); + `; + +const sqlGetPhotosTakenAfterDateInclusive = ( + takenAt: Date, + limit?: number, +) => + sql` + SELECT * FROM photos + WHERE taken_at <= ${takenAt.toISOString()} + ORDER BY taken_at DESC + LIMIT ${limit} + `; + +const sqlGetPhotosTakenBeforeDate = ( + takenAt: Date, + limit?: number, +) => + sql` + SELECT * FROM photos + WHERE taken_at > ${takenAt.toISOString()} + ORDER BY taken_at ASC + LIMIT ${limit} + `; const sqlGetPhotoFromDb = (id: string) => - sql`SELECT * FROM photos WHERE id=${id} LIMIT 1` - .then(({ rows }) => rows.map(parsePhotoFromDb)); + sql`SELECT * FROM photos WHERE id=${id} LIMIT 1`; + +export type GetPhotosOptions = { + sortBy?: 'createdAt' | 'takenAt' | 'priority' + limit?: number + offset?: number + tag?: string + takenBefore?: Date + takenAfterInclusive?: Date +} + +export const getPhotos = async (options: GetPhotosOptions = {}) => { + const { + sortBy = 'takenAt', + limit, + offset, + tag, + takenBefore, + takenAfterInclusive, + } = options; -export const getPhotos = async ( - sortBy: 'createdAt' | 'takenAt' | 'priority' = 'takenAt', - limit?: number, - offset?: number, - tag?: string, -) => { let photos; - const getPhotosRequest = tag - ? () => sqlGetPhotosFromDbByTag(limit, offset, tag) - : sortBy === 'createdAt' - ? () => sqlGetPhotosFromDbSortedByCreatedAt(limit, offset) - : sortBy === 'priority' - ? () => sqlGetPhotosFromDbSortedByPriority(limit, offset) - : () => sqlGetPhotosFromDb(limit, offset); + const getPhotosRequest = takenBefore + ? () => sqlGetPhotosTakenBeforeDate(takenBefore, limit) + : takenAfterInclusive + ? () => sqlGetPhotosTakenAfterDateInclusive(takenAfterInclusive, limit) + : tag + ? () => sqlGetPhotosFromDbByTag(limit, offset, tag) + : sortBy === 'createdAt' + ? () => sqlGetPhotosFromDbSortedByCreatedAt(limit, offset) + : sortBy === 'priority' + ? () => sqlGetPhotosFromDbSortedByPriority(limit, offset) + : () => sqlGetPhotosFromDb(limit, offset); + + const getPhotosRequestAndParse = () => + getPhotosRequest().then(({ rows }) => rows.map(parsePhotoFromDb)); try { - photos = await getPhotosRequest(); + photos = await getPhotosRequestAndParse(); } catch (e: any) { if (/relation "photos" does not exist/i.test(e.message)) { console.log( 'Creating table "photos" because it did not exist', ); await sqlCreatePhotosTable(); - photos = await getPhotosRequest(); + photos = await getPhotosRequestAndParse(); } else if (/endpoint is in transition/i.test(e.message)) { // Wait 5 seconds and try again await new Promise(resolve => setTimeout(resolve, 5000)); try { - photos = await getPhotosRequest(); + photos = await getPhotosRequestAndParse(); } catch (e: any) { console.log(`sql get error on retry (after 5000ms): ${e.message} `); throw e; @@ -220,30 +257,6 @@ export const getPhotos = async ( return photos; }; -export const getPhotosTakenAfterPhotoInclusive = ( - photo: Photo, - limit?: number, -) => - sql` - SELECT * FROM photos - WHERE taken_at <= ${photo.takenAt.toISOString()} - ORDER BY taken_at DESC - LIMIT ${limit} - ` - .then(({ rows }) => rows.map(parsePhotoFromDb)); - -export const getPhotosTakenBeforePhoto = ( - photo: Photo, - limit?: number, -) => - sql` - SELECT * FROM photos - WHERE taken_at > ${photo.takenAt.toISOString()} - ORDER BY taken_at ASC - LIMIT ${limit} - ` - .then(({ rows }) => rows.map(parsePhotoFromDb)); - export const getPhotosCount = async () => sql` SELECT COUNT(*) FROM photos `.then(({ rows }) => parseInt(rows[0].count, 10)); @@ -257,5 +270,6 @@ export const getPhoto = async (id: string): Promise => { // and convert short ids to uuids const photoId = translatePhotoId(id); return sqlGetPhotoFromDb(photoId) + .then(({ rows }) => rows.map(parsePhotoFromDb)) .then(photos => photos.length > 0 ? photos[0] : undefined); };