mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[api] adjust to match images v5 derived shape
This commit is contained in:
@@ -1664,6 +1664,14 @@ export const ImagesPlayerDto = z.object({
|
|||||||
RoomId: z.int().nullable(),
|
RoomId: z.int().nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bulk lookup's projection (`GET /api/images/v5/bulk`): {@link ImagesPlayerDto} plus a
|
||||||
|
* `ClubId`, always null.
|
||||||
|
*/
|
||||||
|
export const ImagesBulkDto = ImagesPlayerDto.extend({
|
||||||
|
ClubId: z.int().nullable().describe('Always null — images are not filed under clubs'),
|
||||||
|
})
|
||||||
|
|
||||||
/** One entry in the anonymous slideshow feed, joined to its creator and room. */
|
/** One entry in the anonymous slideshow feed, joined to its creator and room. */
|
||||||
export const SlideshowImageDto = z.object({
|
export const SlideshowImageDto = z.object({
|
||||||
SavedImageId: z.int(),
|
SavedImageId: z.int(),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
setImageDescription,
|
setImageDescription,
|
||||||
SLIDESHOW_LIMIT,
|
SLIDESHOW_LIMIT,
|
||||||
SLIDESHOW_MAX_LIMIT,
|
SLIDESHOW_MAX_LIMIT,
|
||||||
|
toImagesBulk,
|
||||||
toImagesPlayer,
|
toImagesPlayer,
|
||||||
} from '@repo/domain'
|
} from '@repo/domain'
|
||||||
import { SNIFFED_IMAGE_FORMATS, sniffImage } from '@repo/hono-helpers'
|
import { SNIFFED_IMAGE_FORMATS, sniffImage } from '@repo/hono-helpers'
|
||||||
@@ -34,6 +35,7 @@ import {
|
|||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
form,
|
form,
|
||||||
idParam,
|
idParam,
|
||||||
|
ImagesBulkDto,
|
||||||
ImagesPlayerDto,
|
ImagesPlayerDto,
|
||||||
intQuery,
|
intQuery,
|
||||||
json,
|
json,
|
||||||
@@ -615,9 +617,9 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
|||||||
// with what it asked for; an id with no record (or one that isn't public) is simply
|
// with what it asked for; an id with no record (or one that isn't public) is simply
|
||||||
// absent, which is why this answers 200 with a short list rather than 404ing the lot.
|
// absent, which is why this answers 200 with a short list rather than 404ing the lot.
|
||||||
//
|
//
|
||||||
// Serves the RAW `SavedImage`, like `v6` (metadata by filename) and the room feed — NOT
|
// Serves the `ImagesBulk` projection: the player lists' `ImagesPlayer` shape
|
||||||
// the `ImagesPlayer` projection the player photo LISTS use. Those are a rendered grid,
|
// (`SavedImageId`/`SavedImageType`, no `TaggedPlayerIds`) plus a `ClubId`, always null.
|
||||||
// where the raw record comes up blank; this is a metadata lookup.
|
// NOT the raw `SavedImage` that `v6` and the room feed serve.
|
||||||
//
|
//
|
||||||
// Public-only, as every image read here is: ids are sequential, so honouring whatever
|
// Public-only, as every image read here is: ids are sequential, so honouring whatever
|
||||||
// id is named would hand out private photos to anyone who counts.
|
// id is named would hand out private photos to anyone who counts.
|
||||||
@@ -630,12 +632,13 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
|||||||
'The stored `SavedImage` records for the given ids (`?ids=207&ids=106`), as a bare ' +
|
'The stored `SavedImage` records for the given ids (`?ids=207&ids=106`), as a bare ' +
|
||||||
'array in request order. An id with no record, or one that is not public, is absent ' +
|
'array in request order. An id with no record, or one that is not public, is absent ' +
|
||||||
'from the answer rather than an error — the list can be shorter than the request. ' +
|
'from the answer rather than an error — the list can be shorter than the request. ' +
|
||||||
'Serves the raw `SavedImage` (as `v6` does), not the `ImagesPlayer` projection the ' +
|
'Serves the `ImagesPlayer` projection (`SavedImageId`/`SavedImageType`, no ' +
|
||||||
'player photo lists use.',
|
'`TaggedPlayerIds`) plus a `ClubId` that is always null — not the raw `SavedImage` ' +
|
||||||
|
'that `v6` serves.',
|
||||||
parameters: [
|
parameters: [
|
||||||
intQuery('ids', 'Repeatable; each value may also be a comma-separated list of image ids'),
|
intQuery('ids', 'Repeatable; each value may also be a comma-separated list of image ids'),
|
||||||
],
|
],
|
||||||
responses: { 200: json(SavedImageDto.array(), 'The matching records, in request order') },
|
responses: { 200: json(ImagesBulkDto.array(), 'The matching records, in request order') },
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const ids =
|
const ids =
|
||||||
@@ -644,7 +647,7 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
|||||||
?.flatMap((raw) => raw.split(','))
|
?.flatMap((raw) => raw.split(','))
|
||||||
.map((raw) => Number.parseInt(raw.trim(), 10))
|
.map((raw) => Number.parseInt(raw.trim(), 10))
|
||||||
.filter((imageId) => !Number.isNaN(imageId)) ?? []
|
.filter((imageId) => !Number.isNaN(imageId)) ?? []
|
||||||
return c.json(await getImagesByIds(c.env.DB, ids))
|
return c.json((await getImagesByIds(c.env.DB, ids)).map(toImagesBulk))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -661,7 +664,7 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
|||||||
summary: 'Image metadata by filename',
|
summary: 'Image metadata by filename',
|
||||||
description:
|
description:
|
||||||
'An image’s stored `SavedImage` record for a bucket key — the raw record, as ' +
|
'An image’s stored `SavedImage` record for a bucket key — the raw record, as ' +
|
||||||
'`v6/{id}` and `v5/bulk` serve, with `Description`, `RoomId` and `PlayerEventId` ' +
|
'`v6/{id}` serves, with `Description`, `RoomId` and `PlayerEventId` ' +
|
||||||
'null where the row holds null. 404s when the object exists but has no metadata row.',
|
'null where the row holds null. 404s when the object exists but has no metadata row.',
|
||||||
parameters: [stringQuery('name', 'The image name (bucket key); required')],
|
parameters: [stringQuery('name', 'The image name (bucket key); required')],
|
||||||
responses: {
|
responses: {
|
||||||
@@ -682,7 +685,7 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
|||||||
//
|
//
|
||||||
// Serves the RAW 13-key `SavedImage` (`Id`/`Type`, `TaggedPlayerIds`, nulls left null),
|
// Serves the RAW 13-key `SavedImage` (`Id`/`Type`, `TaggedPlayerIds`, nulls left null),
|
||||||
// which is the type the client decodes this route into — the same record `v6?name=`
|
// which is the type the client decodes this route into — the same record `v6?name=`
|
||||||
// serves, and `v5/bulk` a list of. NOT the `ImagesPlayer` projection of the player lists.
|
// serves. NOT the `ImagesPlayer` projection of the player lists.
|
||||||
//
|
//
|
||||||
// Public, or the caller's own: ids are sequential, so anyone else's private photo is a
|
// Public, or the caller's own: ids are sequential, so anyone else's private photo is a
|
||||||
// 404 here, the same as an id that doesn't exist.
|
// 404 here, the same as an id that doesn't exist.
|
||||||
@@ -692,7 +695,7 @@ export const imageRoutes = new Hono<App>({ strict: false })
|
|||||||
tags: ['Images'],
|
tags: ['Images'],
|
||||||
summary: 'One image by id',
|
summary: 'One image by id',
|
||||||
description:
|
description:
|
||||||
'The stored `SavedImage` record for an image id — the raw record, as `v5/bulk` ' +
|
'The stored `SavedImage` record for an image id — the raw record, as `v6?name=` ' +
|
||||||
'serves, with `Description`, `RoomId` and `PlayerEventId` null where the row holds ' +
|
'serves, with `Description`, `RoomId` and `PlayerEventId` null where the row holds ' +
|
||||||
'null. A private image is served only to the player who took it; to anyone else ' +
|
'null. A private image is served only to the player who took it; to anyone else ' +
|
||||||
'it is a 404, like an unknown id.',
|
'it is a 404, like an unknown id.',
|
||||||
|
|||||||
@@ -7106,20 +7106,37 @@ describe('images', () => {
|
|||||||
|
|
||||||
// Request order, not id order — the client lines the answers up with what it asked for.
|
// Request order, not id order — the client lines the answers up with what it asked for.
|
||||||
const both = await bulk(`?ids=${two.Id}&ids=${one.Id}`)
|
const both = await bulk(`?ids=${two.Id}&ids=${one.Id}`)
|
||||||
expect(both.map((i) => i.Id)).toEqual([two.Id, one.Id])
|
expect(both.map((i) => i.SavedImageId)).toEqual([two.Id, one.Id])
|
||||||
// The RAW SavedImage: `Id`/`Type`, and TaggedPlayerIds present — not the
|
// The ImagesPlayer projection plus ClubId — not the raw SavedImage (`Id`/`Type`,
|
||||||
// SavedImageId/SavedImageType projection the player photo lists serve.
|
// `TaggedPlayerIds`).
|
||||||
|
expect(Object.keys(both[0]).sort()).toEqual(
|
||||||
|
[
|
||||||
|
'SavedImageId',
|
||||||
|
'ImageName',
|
||||||
|
'PlayerId',
|
||||||
|
'RoomId',
|
||||||
|
'PlayerEventId',
|
||||||
|
'ClubId',
|
||||||
|
'Description',
|
||||||
|
'Accessibility',
|
||||||
|
'AccessibilityLocked',
|
||||||
|
'SavedImageType',
|
||||||
|
'CreatedAt',
|
||||||
|
'CheerCount',
|
||||||
|
'CommentCount',
|
||||||
|
].sort()
|
||||||
|
)
|
||||||
expect(both[0]).toMatchObject({
|
expect(both[0]).toMatchObject({
|
||||||
Id: two.Id,
|
SavedImageId: two.Id,
|
||||||
Type: two.Type,
|
SavedImageType: two.Type,
|
||||||
ImageName: 'bulktwo.jpg',
|
ImageName: 'bulktwo.jpg',
|
||||||
PlayerId: 7102,
|
PlayerId: 7102,
|
||||||
TaggedPlayerIds: [],
|
ClubId: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
// An unknown id and a non-public one are absent rather than errors or holes, so the
|
// An unknown id and a non-public one are absent rather than errors or holes, so the
|
||||||
// answer can be shorter than the request.
|
// answer can be shorter than the request.
|
||||||
expect((await bulk(`?ids=${one.Id}&ids=999999&ids=${hidden.Id}`)).map((i) => i.Id)).toEqual([
|
expect((await bulk(`?ids=${one.Id}&ids=999999&ids=${hidden.Id}`)).map((i) => i.SavedImageId)).toEqual([
|
||||||
one.Id,
|
one.Id,
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -7128,7 +7145,7 @@ describe('images', () => {
|
|||||||
|
|
||||||
// More ids than D1 will bind in one query (100) — the lookup has to split.
|
// More ids than D1 will bind in one query (100) — the lookup has to split.
|
||||||
const many = [...Array.from({ length: 130 }, (_, i) => 800000 + i), one.Id, two.Id]
|
const many = [...Array.from({ length: 130 }, (_, i) => 800000 + i), one.Id, two.Id]
|
||||||
expect((await bulk(`?${many.map((id) => `ids=${id}`).join('&')}`)).map((i) => i.Id)).toEqual([
|
expect((await bulk(`?${many.map((id) => `ids=${id}`).join('&')}`)).map((i) => i.SavedImageId)).toEqual([
|
||||||
one.Id,
|
one.Id,
|
||||||
two.Id,
|
two.Id,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -467,6 +467,46 @@ export function toImagesPlayer(img: SavedImage): ImagesPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bulk-lookup projection (`GET /api/images/v5/bulk`): the {@link ImagesPlayer} shape
|
||||||
|
* plus a `ClubId`. Images aren't filed under clubs here, so it is always null — but the
|
||||||
|
* key is part of the shape and must be present.
|
||||||
|
*/
|
||||||
|
export interface ImagesBulk {
|
||||||
|
SavedImageId: number
|
||||||
|
ImageName: string
|
||||||
|
PlayerId: number
|
||||||
|
RoomId: number | null
|
||||||
|
PlayerEventId: number | null
|
||||||
|
ClubId: number | null
|
||||||
|
Description: string | null
|
||||||
|
Accessibility: number
|
||||||
|
AccessibilityLocked: boolean
|
||||||
|
SavedImageType: number
|
||||||
|
CreatedAt: string
|
||||||
|
CheerCount: number
|
||||||
|
CommentCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Project a stored image to the bulk lookup's {@link ImagesBulk} shape. */
|
||||||
|
export function toImagesBulk(img: SavedImage): ImagesBulk {
|
||||||
|
return {
|
||||||
|
SavedImageId: img.Id,
|
||||||
|
ImageName: img.ImageName,
|
||||||
|
PlayerId: img.PlayerId,
|
||||||
|
RoomId: img.RoomId,
|
||||||
|
PlayerEventId: img.PlayerEventId,
|
||||||
|
ClubId: null,
|
||||||
|
Description: img.Description,
|
||||||
|
Accessibility: img.Accessibility,
|
||||||
|
AccessibilityLocked: img.AccessibilityLocked,
|
||||||
|
SavedImageType: img.Type,
|
||||||
|
CreatedAt: img.CreatedAt,
|
||||||
|
CheerCount: img.CheerCount,
|
||||||
|
CommentCount: img.CommentCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** How many recent images the slideshow feed returns when the caller doesn't say. */
|
/** How many recent images the slideshow feed returns when the caller doesn't say. */
|
||||||
export const SLIDESHOW_LIMIT = 10
|
export const SLIDESHOW_LIMIT = 10
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user