Barazo AppView backend
barazo.forum
1import { z } from 'zod/v4'
2
3// ---------------------------------------------------------------------------
4// Shared enums
5// ---------------------------------------------------------------------------
6
7/** Valid page status values. */
8export const pageStatusSchema = z.enum(['draft', 'published'])
9
10export type PageStatus = z.infer<typeof pageStatusSchema>
11
12// ---------------------------------------------------------------------------
13// Constants
14// ---------------------------------------------------------------------------
15
16/** Slugs reserved for UI routes that must not collide with page slugs. */
17const RESERVED_SLUGS = ['new', 'edit', 'drafts'] as const
18
19// ---------------------------------------------------------------------------
20// Request schemas
21// ---------------------------------------------------------------------------
22
23/** Slug pattern: lowercase alphanumeric segments separated by single hyphens. */
24const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
25
26/** Slug field with format validation and reserved-slug rejection. */
27const slugField = z
28 .string()
29 .min(1, 'Slug is required')
30 .max(100, 'Slug must be at most 100 characters')
31 .regex(
32 slugPattern,
33 "Slug must be lowercase alphanumeric with single hyphens (e.g. 'terms-of-service')"
34 )
35 .refine(
36 (val) => !RESERVED_SLUGS.includes(val as (typeof RESERVED_SLUGS)[number]),
37 'This slug is reserved and cannot be used'
38 )
39
40/** Schema for creating a new page. */
41export const createPageSchema = z.object({
42 title: z
43 .string()
44 .trim()
45 .min(1, 'Title is required')
46 .max(200, 'Title must be at most 200 characters'),
47 slug: slugField,
48 content: z.string().max(100_000, 'Content must be at most 100000 characters').default(''),
49 status: pageStatusSchema.default('draft'),
50 metaDescription: z
51 .string()
52 .max(320, 'Meta description must be at most 320 characters')
53 .nullable()
54 .optional(),
55 parentId: z.string().nullable().optional(),
56 sortOrder: z
57 .number()
58 .int('Sort order must be an integer')
59 .min(0, 'Sort order must be non-negative')
60 .optional(),
61})
62
63export type CreatePageInput = z.infer<typeof createPageSchema>
64
65/** Schema for updating an existing page (all fields optional). */
66export const updatePageSchema = z.object({
67 title: z
68 .string()
69 .trim()
70 .min(1, 'Title must not be empty')
71 .max(200, 'Title must be at most 200 characters')
72 .optional(),
73 slug: slugField.optional(),
74 content: z.string().max(100_000, 'Content must be at most 100000 characters').optional(),
75 status: pageStatusSchema.optional(),
76 metaDescription: z
77 .string()
78 .max(320, 'Meta description must be at most 320 characters')
79 .nullable()
80 .optional(),
81 parentId: z.string().nullable().optional(),
82 sortOrder: z
83 .number()
84 .int('Sort order must be an integer')
85 .min(0, 'Sort order must be non-negative')
86 .optional(),
87})
88
89export type UpdatePageInput = z.infer<typeof updatePageSchema>
90
91// ---------------------------------------------------------------------------
92// Response schemas (for OpenAPI documentation)
93// ---------------------------------------------------------------------------
94
95/** Schema describing a single page in API responses. */
96export const pageResponseSchema = z.object({
97 id: z.string(),
98 slug: z.string(),
99 title: z.string(),
100 content: z.string(),
101 status: pageStatusSchema,
102 metaDescription: z.string().nullable(),
103 parentId: z.string().nullable(),
104 sortOrder: z.number(),
105 communityDid: z.string(),
106 createdAt: z.string(),
107 updatedAt: z.string(),
108})
109
110export type PageResponse = z.infer<typeof pageResponseSchema>
111
112/** Schema describing a page with its children (tree structure). */
113export const pageTreeResponseSchema: z.ZodType<PageTreeResponse> = z.lazy(() =>
114 z.object({
115 id: z.string(),
116 slug: z.string(),
117 title: z.string(),
118 content: z.string(),
119 status: pageStatusSchema,
120 metaDescription: z.string().nullable(),
121 parentId: z.string().nullable(),
122 sortOrder: z.number(),
123 communityDid: z.string(),
124 createdAt: z.string(),
125 updatedAt: z.string(),
126 children: z.array(pageTreeResponseSchema),
127 })
128)
129
130export interface PageTreeResponse {
131 id: string
132 slug: string
133 title: string
134 content: string
135 status: 'draft' | 'published'
136 metaDescription: string | null
137 parentId: string | null
138 sortOrder: number
139 communityDid: string
140 createdAt: string
141 updatedAt: string
142 children: PageTreeResponse[]
143}