|
1 | 1 | import {z} from 'zod'; |
| 2 | +import errors from '@tryghost/errors'; |
2 | 3 | import type {Knex} from 'knex'; |
3 | | -import {camelKeys, snakeKeys} from './case-keys'; |
4 | | -import {DbGiftLink} from './database'; |
5 | | -import {GiftLink} from './models'; |
6 | | - |
7 | | -// The columns the read path selects and the codec decodes into a GiftLink. |
8 | | -export const GiftLinkRow = DbGiftLink.pick({ |
9 | | - token: true, |
10 | | - created_at: true |
11 | | -}); |
12 | | - |
13 | | -// Maps a selected row to/from the domain GiftLink (snake_case to camelCase, token branding). |
14 | | -export const giftLinkCodec = z.codec(GiftLinkRow, GiftLink, { |
15 | | - decode: row => camelKeys(row), |
16 | | - encode: link => snakeKeys(link) |
17 | | -}); |
18 | | - |
19 | | -const giftLinkColumns = Object.keys(GiftLinkRow.shape).map(column => `gift_links.${column}`); |
20 | | - |
21 | | -// Executor-agnostic statements for the read shapes (joins, filters, columns): each is |
22 | | -// parameterised by domain args and takes the connection at execution, so the service binds |
23 | | -// knex (or a trx). The result generic names the row the codec expects, since knex can't infer |
24 | | -// a dynamic column list. |
25 | | -// Anchored on posts: zero rows means the post does not exist; a single all-null row means the |
26 | | -// post exists with no live link (hence the nullable columns). |
| 4 | +import {GiftLinkRow, giftLinkCodec, giftLinkColumns} from './codec'; |
| 5 | +import {type Post} from './models'; |
| 6 | + |
| 7 | +// The LEFT JOIN leaves every link column nullable; the explicit generic names a row shape knex |
| 8 | +// can't infer from a dynamic column list. |
27 | 9 | type LiveLinkRow = {[K in keyof z.input<typeof GiftLinkRow>]: z.input<typeof GiftLinkRow>[K] | null}; |
28 | 10 |
|
29 | | -// LEFT JOINs from posts: a missing post yields zero rows, while a post with no live link yields one |
30 | | -// row with null link columns. So zero rows means "no such post". |
31 | | -export function liveLinksForPost(postId: string) { |
32 | | - return (knex: Knex) => knex('posts') |
33 | | - .where('posts.id', postId) |
34 | | - .leftJoin('post_gift_links', 'post_gift_links.post_id', 'posts.id') |
35 | | - .leftJoin('gift_links', 'gift_links.token', 'post_gift_links.gift_link_token') |
36 | | - .select<LiveLinkRow[]>(giftLinkColumns); |
37 | | -} |
| 11 | +export class GiftLinkQueries { |
| 12 | + private knex: Knex; |
| 13 | + |
| 14 | + constructor({knex}: {knex: Knex}) { |
| 15 | + this.knex = knex; |
| 16 | + } |
| 17 | + |
| 18 | + async getPost(postId: string): Promise<Post> { |
| 19 | + // Anchored on posts: zero rows means the post itself doesn't exist, not merely that it has |
| 20 | + // no live link. |
| 21 | + const rows = await this.knex('posts') |
| 22 | + .where('posts.id', postId) |
| 23 | + .leftJoin('post_gift_links', 'post_gift_links.post_id', 'posts.id') |
| 24 | + .leftJoin('gift_links', 'gift_links.token', 'post_gift_links.gift_link_token') |
| 25 | + .select<LiveLinkRow[]>(giftLinkColumns); |
| 26 | + |
| 27 | + if (rows.length === 0) { |
| 28 | + throw new errors.NotFoundError({message: `Post ${postId} does not exist.`}); |
| 29 | + } |
| 30 | + |
| 31 | + const giftLinks = rows |
| 32 | + .filter((row): row is z.input<typeof GiftLinkRow> => row.token !== null) |
| 33 | + .map(row => z.decode(giftLinkCodec, row)); |
| 34 | + return {id: postId, giftLinks}; |
| 35 | + } |
| 36 | + |
| 37 | + async getPostByToken(token: string): Promise<Post | null> { |
| 38 | + const row = await this.knex('post_gift_links') |
| 39 | + .join('gift_links', 'gift_links.token', 'post_gift_links.gift_link_token') |
| 40 | + .where('gift_links.token', token) |
| 41 | + .first<z.input<typeof GiftLinkRow> & {post_id: string}>( |
| 42 | + [...giftLinkColumns, 'post_gift_links.post_id as post_id'] |
| 43 | + ); |
| 44 | + return row ? {id: row.post_id, giftLinks: [z.decode(giftLinkCodec, row)]} : null; |
| 45 | + } |
38 | 46 |
|
39 | | -export function liveLinkForToken(token: string) { |
40 | | - return (knex: Knex) => knex('post_gift_links') |
41 | | - .join('gift_links', 'gift_links.token', 'post_gift_links.gift_link_token') |
42 | | - .where('gift_links.token', token) |
43 | | - .first<z.input<typeof GiftLinkRow> & {post_id: string}>([...giftLinkColumns, 'post_gift_links.post_id as post_id']); |
| 47 | + async isValidTokenForPost(token: string, postId: string): Promise<boolean> { |
| 48 | + return (await this.getPostByToken(token))?.id === postId; |
| 49 | + } |
44 | 50 | } |
0 commit comments