Vercel/src/album/query.ts
Sam Becker 1e66815a3d
Albums (#315)
* Make /db top-level module

* Create Album type

* Pin pnpm version

* Generalize query modules

* Finalize album postgres data type

* Remove temp albums prop

* Create basic album primitives

* Fix temporary album bugs

* Add albums to sidebar

* Disambiguate string date utilities

* Localize album language

* Add album join option to core photo queries

* Tweak album icon placement

* Add album photo detail page

* Refine Album data model

* Display album subhead when available

* Generate album og images

* Finalize album share modal

* Add albums to sitemap

* Statically pre-render albums

* Display tags on albums

* Add albums to cmd-k menu

* Handle album tag overflow

* Stop truncating album subheads

* Create core admin album views

* Make albums editable

* Create/edit albums on photo save, add delete album
2025-09-16 21:47:22 -05:00

125 lines
3.7 KiB
TypeScript

import { safelyQuery } from '@/db/query';
import { sql } from '@/platforms/postgres';
import { Album, Albums, parseAlbumFromDb } from '.';
export const createAlbumsTable = () =>
sql`
CREATE TABLE IF NOT EXISTS albums (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
subhead TEXT,
description TEXT,
location_name VARCHAR(255),
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
`;
export const createAlbumPhotoTable = () =>
sql`
CREATE TABLE IF NOT EXISTS album_photo (
album_id uuid NOT NULL REFERENCES albums(id) ON DELETE CASCADE,
photo_id VARCHAR(8) NOT NULL REFERENCES photos(id) ON DELETE CASCADE,
sort_order SMALLINT NOT NULL DEFAULT 0,
PRIMARY KEY (album_id, photo_id)
)
`;
export const insertAlbum = (album: Omit<Album, 'id'>) =>
safelyQuery(() => sql`
INSERT INTO albums (
title,
slug,
subhead,
description,
location_name,
latitude,
longitude
) VALUES (
${album.title},
${album.slug},
${album.subhead},
${album.description},
${album.locationName},
${album.latitude},
${album.longitude}
)
RETURNING id
`.then(({ rows }) => rows[0]?.id as string)
, 'insertAlbum');
export const updateAlbum = (album: Album) =>
safelyQuery(() => sql`
UPDATE albums SET
title=${album.title},
slug=${album.slug},
subhead=${album.subhead},
description=${album.description},
location_name=${album.locationName},
latitude=${album.latitude},
longitude=${album.longitude},
updated_at=${(new Date()).toISOString()}
WHERE id=${album.id}
`, 'updateAlbum');
export const getAlbumFromSlug = (slug: string) =>
safelyQuery(() => sql<Album>`
SELECT * FROM albums WHERE slug=${slug}
`.then(({ rows }) => rows[0] ? parseAlbumFromDb(rows[0]) : undefined)
, 'getAlbum');
export const deleteAlbum = (id: string) =>
safelyQuery(() => sql`
DELETE FROM albums WHERE id=${id}
`, 'deleteAlbum');
export const getAlbumsWithMeta = () =>
safelyQuery(() => sql`
SELECT
a.*,
COALESCE(COUNT(ap.photo_id), 0) as count
FROM albums a
LEFT JOIN album_photo ap ON a.id = ap.album_id
GROUP BY a.id
ORDER BY a.created_at DESC
`.then(({ rows }): Albums => rows.map(({
count,
...album
}) => ({
album: parseAlbumFromDb(album),
count: parseInt(count, 10),
lastModified: album.updated_at as Date,
})))
, 'getAlbumsWithPhotoCounts');
export const clearPhotoAlbumIds = (photoId: string) =>
safelyQuery(() => sql`
DELETE FROM album_photo WHERE photo_id=${photoId}
`, 'clearPhotoAlbumIds');
export const addPhotoAlbumId = (photoId: string, albumId: string) =>
safelyQuery(() => sql`
INSERT INTO album_photo (album_id, photo_id) VALUES (${albumId}, ${photoId})
ON CONFLICT (album_id, photo_id) DO NOTHING
`, 'updateAlbumPhoto');
export const getAlbumTitlesForPhoto = (photoId: string) =>
safelyQuery(() => sql<{ title: string }>`
SELECT a.title FROM albums a
JOIN album_photo ap ON a.id = ap.album_id
WHERE ap.photo_id=${photoId}
`.then(({ rows }) => rows.map(({ title }) => title))
, 'getAlbumTitlesForPhoto');
export const getTagsForAlbum = (albumId: string) =>
safelyQuery(() => sql`
SELECT DISTINCT unnest(p.tags) as tag
FROM photos p
LEFT JOIN album_photo ap ON p.id = ap.photo_id
WHERE album_id=${albumId}
`.then(({ rows }) => rows.map(({ tag }) => tag))
, 'getTagsForAlbum');