Barazo AppView backend barazo.forum
at main 143 lines 4.5 kB view raw
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}