mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
724c88c201
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.
59 lines
2.3 KiB
TypeScript
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>;
|