Files
TREK/shared/src/category/category.schema.ts
T
Maurice 724c88c201 style: format server and shared with prettier
A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
2026-10-10 21:23:37 +02:00

59 lines
2.3 KiB
TypeScript

import { hexColorSchema } from '../place/place.schema';
import { z } from 'zod';
/**
* Category API contract — single source of truth for the /api/categories endpoints.
*
* Categories are the place-category palette (also the admin "Personalization"
* surface). Reading is open to any authenticated user; create/update/delete are
* admin-only. The legacy route (server/src/routes/categories.ts) wraps
* services/categoryService.ts 1:1.
*
* The bespoke 400 ("Category name is required") and 404 ("Category not found")
* messages are reproduced in the controller so the bodies stay byte-identical.
*/
/**
* Note the asymmetry below: the response keeps `color: z.string()` while the
* requests demand a hex value. Tightening the response would make an instance
* fail on its own stored rows — the write path was unvalidated for a long time,
* so anything could be in there. The client allow-lists on read for the same
* reason (client/src/utils/safeColor.ts).
*/
export const categorySchema = z.object({
id: z.number(),
name: z.string(),
color: z.string(),
icon: z.string(),
user_id: z.number().nullable().optional(),
created_at: z.string().optional(),
});
export type Category = z.infer<typeof categorySchema>;
export const createCategoryRequestSchema = z.object({
name: z.string().min(1),
// Hex only: the value is interpolated into a style="…" attribute of the
// hand-built marker HTML on both map renderers, and on the share page that
// answers without a guard.
color: hexColorSchema.optional(),
icon: z.string().optional(),
});
export type CreateCategoryRequest = z.infer<typeof createCategoryRequestSchema>;
/** All fields optional — the service COALESCEs each against the stored value. */
export const updateCategoryRequestSchema = z.object({
name: z.string().optional(),
// Hex only: the value is interpolated into a style="…" attribute of the
// hand-built marker HTML on both map renderers, and on the share page that
// answers without a guard.
color: hexColorSchema.optional(),
icon: z.string().optional(),
});
export type UpdateCategoryRequest = z.infer<typeof updateCategoryRequestSchema>;
export const categoryListResponseSchema = z.object({
categories: z.array(categorySchema),
});
export type CategoryListResponse = z.infer<typeof categoryListResponseSchema>;