Barazo AppView backend
barazo.forum
1import { z } from 'zod/v4'
2
3// ---------------------------------------------------------------------------
4// Helpers
5// ---------------------------------------------------------------------------
6
7/**
8 * Count the number of grapheme clusters in a string using Intl.Segmenter.
9 * AT Protocol lexicons specify `maxGraphemes` which counts user-perceived
10 * characters (grapheme clusters), not UTF-16 code units.
11 */
12function graphemeLength(str: string): number {
13 const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
14 return [...segmenter.segment(str)].length
15}
16
17// ---------------------------------------------------------------------------
18// Request schemas
19// ---------------------------------------------------------------------------
20
21/** Schema for creating a reaction on a topic or reply. */
22export const createReactionSchema = z.object({
23 subjectUri: z.string().min(1, 'Subject URI is required'),
24 subjectCid: z.string().min(1, 'Subject CID is required'),
25 type: z
26 .string()
27 .trim()
28 .min(1, 'Reaction type is required')
29 .max(300, 'Reaction type exceeds maximum byte length')
30 .refine((val) => graphemeLength(val) <= 30, 'Reaction type must be at most 30 graphemes'),
31})
32
33export type CreateReactionInput = z.infer<typeof createReactionSchema>
34
35// ---------------------------------------------------------------------------
36// Query schemas
37// ---------------------------------------------------------------------------
38
39/** Schema for listing reactions with pagination and optional type filter. */
40export const reactionQuerySchema = z.object({
41 subjectUri: z.string().min(1, 'Subject URI is required'),
42 type: z.string().optional(),
43 cursor: z.string().optional(),
44 limit: z
45 .string()
46 .transform((val) => Number(val))
47 .pipe(z.number().int().min(1).max(100))
48 .optional()
49 .default(25),
50})
51
52export type ReactionQueryInput = z.infer<typeof reactionQuerySchema>
53
54// ---------------------------------------------------------------------------
55// Admin settings extension
56// ---------------------------------------------------------------------------
57
58/** Schema for validating reactionSet in admin settings updates. */
59export const reactionSetSchema = z
60 .array(
61 z
62 .string()
63 .trim()
64 .min(1, 'Reaction type must not be empty')
65 .max(300, 'Reaction type exceeds maximum byte length')
66 .refine((val) => graphemeLength(val) <= 30, 'Reaction type must be at most 30 graphemes')
67 )
68 .min(1, 'Reaction set must contain at least one reaction type')
69 .refine((arr) => new Set(arr).size === arr.length, 'Reaction set must contain unique values')
70
71export type ReactionSet = z.infer<typeof reactionSetSchema>
72
73// ---------------------------------------------------------------------------
74// Response schemas (for OpenAPI documentation)
75// ---------------------------------------------------------------------------
76
77/** Schema describing a single reaction in API responses. */
78export const reactionResponseSchema = z.object({
79 uri: z.string(),
80 rkey: z.string(),
81 authorDid: z.string(),
82 subjectUri: z.string(),
83 type: z.string(),
84 cid: z.string(),
85 createdAt: z.string(),
86})
87
88export type ReactionResponse = z.infer<typeof reactionResponseSchema>
89
90/** Schema for a paginated reaction list response. */
91export const reactionListResponseSchema = z.object({
92 reactions: z.array(reactionResponseSchema),
93 cursor: z.string().nullable(),
94})
95
96export type ReactionListResponse = z.infer<typeof reactionListResponseSchema>