Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:46:06 +08:00
commit 61aa7d532c
10 changed files with 1723 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
/**
* API Route validation schema template
*
* Usage:
* 1. Define request and response schemas
* 2. Validate incoming requests with safeParse
* 3. Return typed errors for invalid requests
*/
// Request schema
const requestSchema = z.object({
query: z.string().min(1, 'Query is required'),
filters: z.object({
type: z.array(z.string()).optional(),
tags: z.array(z.string()).optional(),
dateRange: z.object({
start: z.string().datetime().optional(),
end: z.string().datetime().optional(),
}).optional(),
}).optional(),
pagination: z.object({
page: z.number().int().positive().default(1),
limit: z.number().int().min(1).max(100).default(10),
}).optional(),
});
// Response schema
const responseSchema = z.object({
success: z.boolean(),
data: z.any().optional(),
error: z.string().optional(),
details: z.any().optional(),
});
// Types
type RequestBody = z.infer<typeof requestSchema>;
type ResponseBody = z.infer<typeof responseSchema>;
/**
* POST endpoint with validation
*/
export async function POST(request: NextRequest): Promise<NextResponse<ResponseBody>> {
try {
// Parse request body
const body = await request.json();
// Validate request
const result = requestSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{
success: false,
error: 'Invalid request body',
details: result.error.format(),
},
{ status: 400 }
);
}
// Type-safe validated data
const { query, filters, pagination } = result.data;
// Perform business logic
const data = await processRequest(query, filters, pagination);
return NextResponse.json({
success: true,
data,
});
} catch (error) {
return NextResponse.json(
{
success: false,
error: 'Internal server error',
},
{ status: 500 }
);
}
}
async function processRequest(
query: string,
filters?: RequestBody['filters'],
pagination?: RequestBody['pagination']
) {
// Implement your business logic here
return { results: [], total: 0 };
}
/**
* GET endpoint with query parameter validation
*/
const queryParamsSchema = z.object({
id: z.string().uuid('Invalid ID format'),
includeRelated: z.coerce.boolean().optional(),
});
export async function GET(request: NextRequest): Promise<NextResponse<ResponseBody>> {
// Parse query parameters
const searchParams = request.nextUrl.searchParams;
const queryParams = {
id: searchParams.get('id'),
includeRelated: searchParams.get('includeRelated'),
};
// Validate query parameters
const result = queryParamsSchema.safeParse(queryParams);
if (!result.success) {
return NextResponse.json(
{
success: false,
error: 'Invalid query parameters',
details: result.error.format(),
},
{ status: 400 }
);
}
const { id, includeRelated } = result.data;
// Fetch data
const data = await fetchData(id, includeRelated);
return NextResponse.json({
success: true,
data,
});
}
async function fetchData(id: string, includeRelated?: boolean) {
// Implement your data fetching logic here
return { id, includeRelated };
}
/**
* PATCH endpoint with partial update validation
*/
const updateSchema = z.object({
name: z.string().min(1).max(100).optional(),
description: z.string().max(500).optional(),
tags: z.array(z.string()).max(10).optional(),
status: z.enum(['active', 'inactive', 'archived']).optional(),
}).refine(
(data) => Object.keys(data).length > 0,
{ message: 'At least one field must be provided for update' }
);
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
): Promise<NextResponse<ResponseBody>> {
// Validate ID from route params
const idResult = z.string().uuid().safeParse(params.id);
if (!idResult.success) {
return NextResponse.json(
{
success: false,
error: 'Invalid ID format',
},
{ status: 400 }
);
}
// Parse and validate request body
const body = await request.json();
const result = updateSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{
success: false,
error: 'Invalid update data',
details: result.error.format(),
},
{ status: 400 }
);
}
const id = idResult.data;
const updates = result.data;
// Perform update
const data = await updateData(id, updates);
return NextResponse.json({
success: true,
data,
});
}
async function updateData(id: string, updates: z.infer<typeof updateSchema>) {
// Implement your update logic here
return { id, ...updates };
}
// Example: Middleware with validation
export function validateRequest<T>(schema: z.ZodSchema<T>) {
return async (request: NextRequest): Promise<{ data?: T; error?: ResponseBody }> => {
try {
const body = await request.json();
const result = schema.safeParse(body);
if (!result.success) {
return {
error: {
success: false,
error: 'Validation failed',
details: result.error.format(),
},
};
}
return { data: result.data };
} catch {
return {
error: {
success: false,
error: 'Invalid JSON body',
},
};
}
};
}
// Usage of middleware:
/*
export async function POST(request: NextRequest) {
const { data, error } = await validateRequest(requestSchema)(request);
if (error) {
return NextResponse.json(error, { status: 400 });
}
// Use validated data
console.log(data);
return NextResponse.json({ success: true });
}
*/

View File

@@ -0,0 +1,210 @@
import { z } from 'zod';
/**
* Entity validation schema template for worldbuilding applications
*
* Common schemas for worldbuilding entities like characters, locations, items, etc.
*/
// Base entity schema (shared fields across all entities)
const baseEntitySchema = z.object({
id: z.string().uuid(),
name: z.string().min(1, 'Name is required').max(200),
description: z.string().max(5000).optional(),
tags: z.array(z.string()).max(20),
metadata: z.object({
createdAt: z.date(),
updatedAt: z.date(),
createdBy: z.string(),
}),
});
// Entity types enum
export const entityTypeSchema = z.enum([
'character',
'location',
'item',
'faction',
'event',
'concept',
]);
export type EntityType = z.infer<typeof entityTypeSchema>;
// Character entity schema
export const characterSchema = baseEntitySchema.extend({
type: z.literal('character'),
attributes: z.object({
age: z.number().int().nonnegative().optional(),
gender: z.string().optional(),
species: z.string().optional(),
occupation: z.string().optional(),
birthplace: z.string().uuid().optional(), // Reference to location entity
}),
relationships: z.array(
z.object({
targetId: z.string().uuid(),
type: z.string(),
strength: z.number().min(0).max(1).optional(),
})
),
});
export type Character = z.infer<typeof characterSchema>;
// Location entity schema
export const locationSchema = baseEntitySchema.extend({
type: z.literal('location'),
attributes: z.object({
locationType: z.enum(['city', 'region', 'country', 'landmark', 'building', 'other']),
population: z.number().int().nonnegative().optional(),
parentLocation: z.string().uuid().optional(), // Reference to parent location
coordinates: z.object({
x: z.number(),
y: z.number(),
}).optional(),
}),
});
export type Location = z.infer<typeof locationSchema>;
// Item entity schema
export const itemSchema = baseEntitySchema.extend({
type: z.literal('item'),
attributes: z.object({
itemType: z.enum(['weapon', 'armor', 'tool', 'artifact', 'consumable', 'other']),
rarity: z.enum(['common', 'uncommon', 'rare', 'legendary']).optional(),
owner: z.string().uuid().optional(), // Reference to character entity
location: z.string().uuid().optional(), // Reference to location entity
}),
});
export type Item = z.infer<typeof itemSchema>;
// Faction/Organization entity schema
export const factionSchema = baseEntitySchema.extend({
type: z.literal('faction'),
attributes: z.object({
factionType: z.enum(['guild', 'government', 'religion', 'military', 'criminal', 'other']),
size: z.enum(['small', 'medium', 'large', 'massive']).optional(),
headquarters: z.string().uuid().optional(), // Reference to location entity
leader: z.string().uuid().optional(), // Reference to character entity
founded: z.object({
year: z.number().int(),
month: z.number().int().min(1).max(12).optional(),
day: z.number().int().min(1).max(31).optional(),
}).optional(),
}),
members: z.array(
z.object({
characterId: z.string().uuid(),
role: z.string(),
joinDate: z.date().optional(),
})
),
});
export type Faction = z.infer<typeof factionSchema>;
// Event entity schema
export const eventSchema = baseEntitySchema.extend({
type: z.literal('event'),
attributes: z.object({
eventType: z.enum(['battle', 'meeting', 'ceremony', 'discovery', 'disaster', 'other']),
date: z.object({
year: z.number().int(),
month: z.number().int().min(1).max(12).optional(),
day: z.number().int().min(1).max(31).optional(),
}),
location: z.string().uuid().optional(), // Reference to location entity
participants: z.array(z.string().uuid()), // References to character entities
}),
}).refine(
(data) => !data.attributes.date.day || data.attributes.date.month,
{ message: 'Month is required when day is specified', path: ['attributes', 'date', 'month'] }
);
export type Event = z.infer<typeof eventSchema>;
// Concept entity schema (abstract ideas, magic systems, technologies, etc.)
export const conceptSchema = baseEntitySchema.extend({
type: z.literal('concept'),
attributes: z.object({
conceptType: z.enum(['magic', 'technology', 'philosophy', 'language', 'custom']),
relatedEntities: z.array(z.string().uuid()), // References to any entity
}),
});
export type Concept = z.infer<typeof conceptSchema>;
// Union of all entity types
export const entitySchema = z.discriminatedUnion('type', [
characterSchema,
locationSchema,
itemSchema,
factionSchema,
eventSchema,
conceptSchema,
]);
export type Entity = z.infer<typeof entitySchema>;
// Relationship schema
export const relationshipSchema = z.object({
id: z.string().uuid(),
sourceId: z.string().uuid(),
targetId: z.string().uuid(),
type: z.string().min(1).max(50),
description: z.string().max(500).optional(),
strength: z.number().min(0).max(1).optional(),
metadata: z.record(z.string(), z.any()).optional(),
createdAt: z.date(),
updatedAt: z.date(),
}).refine(
(data) => data.sourceId !== data.targetId,
{ message: 'Source and target must be different entities', path: ['targetId'] }
);
export type Relationship = z.infer<typeof relationshipSchema>;
// Create entity input schema (without auto-generated fields)
export const createEntityInputSchema = z.object({
type: entityTypeSchema,
name: z.string().min(1).max(200),
description: z.string().max(5000).optional(),
tags: z.array(z.string()).max(20).default([]),
attributes: z.record(z.string(), z.any()),
});
export type CreateEntityInput = z.infer<typeof createEntityInputSchema>;
// Update entity input schema (partial)
export const updateEntityInputSchema = z.object({
name: z.string().min(1).max(200).optional(),
description: z.string().max(5000).optional(),
tags: z.array(z.string()).max(20).optional(),
attributes: z.record(z.string(), z.any()).optional(),
}).refine(
(data) => Object.keys(data).length > 0,
{ message: 'At least one field must be provided for update' }
);
export type UpdateEntityInput = z.infer<typeof updateEntityInputSchema>;
// Query/filter schema for entity search
export const entityQuerySchema = z.object({
query: z.string().optional(),
types: z.array(entityTypeSchema).optional(),
tags: z.array(z.string()).optional(),
createdAfter: z.date().optional(),
createdBefore: z.date().optional(),
createdBy: z.string().optional(),
pagination: z.object({
page: z.number().int().positive().default(1),
limit: z.number().int().min(1).max(100).default(10),
}).optional(),
sortBy: z.enum(['name', 'createdAt', 'updatedAt']).default('createdAt'),
sortOrder: z.enum(['asc', 'desc']).default('desc'),
});
export type EntityQuery = z.infer<typeof entityQuerySchema>;

View File

@@ -0,0 +1,81 @@
import { z } from 'zod';
/**
* Form validation schema template
*
* Usage:
* 1. Customize the schema fields for your form
* 2. Use with React Hook Form or other form libraries
* 3. Extract TypeScript type with z.infer<typeof formSchema>
*/
export const formSchema = z.object({
// Text input
name: z.string()
.min(1, 'Name is required')
.max(100, 'Name must be 100 characters or less'),
// Email input
email: z.string()
.email('Invalid email address'),
// Optional text field
description: z.string()
.max(500, 'Description must be 500 characters or less')
.optional(),
// Number input
age: z.coerce.number()
.int('Age must be a whole number')
.positive('Age must be positive')
.optional(),
// Select/dropdown
category: z.enum(['option1', 'option2', 'option3'], {
required_error: 'Please select a category',
}),
// Checkbox
agreedToTerms: z.boolean()
.refine((val) => val === true, {
message: 'You must agree to the terms',
}),
// Multi-select
tags: z.array(z.string())
.min(1, 'At least one tag is required')
.max(10, 'Maximum 10 tags allowed'),
// Date input
birthDate: z.coerce.date()
.max(new Date(), 'Birth date cannot be in the future')
.optional(),
});
// Infer TypeScript type from schema
export type FormData = z.infer<typeof formSchema>;
// Example: React Hook Form integration
/*
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(formSchema),
});
const onSubmit = (data: FormData) => {
console.log('Valid form data:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
*/

View File

@@ -0,0 +1,131 @@
'use server';
import { z } from 'zod';
/**
* Server Action validation schema template
*
* Usage:
* 1. Define input schema for Server Action parameters
* 2. Validate with safeParse at the beginning of the action
* 3. Return validation errors to the client
*/
// Input schema
const actionInputSchema = z.object({
id: z.string().uuid('Invalid ID format'),
name: z.string().min(1, 'Name is required').max(100),
type: z.enum(['type1', 'type2', 'type3']),
metadata: z.record(z.string(), z.any()).optional(),
});
// Output schema (optional, for type safety)
const actionOutputSchema = z.object({
success: z.boolean(),
data: z.any().optional(),
errors: z.record(z.array(z.string())).optional(),
});
// Types
type ActionInput = z.infer<typeof actionInputSchema>;
type ActionOutput = z.infer<typeof actionOutputSchema>;
/**
* Example Server Action with validation
*/
export async function myServerAction(input: unknown): Promise<ActionOutput> {
// Validate input
const result = actionInputSchema.safeParse(input);
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors,
};
}
// Type-safe validated data
const { id, name, type, metadata } = result.data;
try {
// Perform server-side logic
// Example: database operation, API call, etc.
const data = await performAction(id, name, type, metadata);
return {
success: true,
data,
};
} catch (error) {
return {
success: false,
errors: {
_general: ['An unexpected error occurred'],
},
};
}
}
async function performAction(
id: string,
name: string,
type: string,
metadata?: Record<string, any>
) {
// Implement your server-side logic here
return { id, name, type, metadata };
}
// Example: FormData-based Server Action
export async function formDataAction(formData: FormData): Promise<ActionOutput> {
const rawData = {
id: formData.get('id'),
name: formData.get('name'),
type: formData.get('type'),
};
const result = actionInputSchema.safeParse(rawData);
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors,
};
}
// Process validated data
const { id, name, type } = result.data;
// ... rest of implementation
return { success: true, data: { id, name, type } };
}
// Example: Client-side usage
/*
'use client';
import { myServerAction } from './actions';
import { useState } from 'react';
function MyComponent() {
const [errors, setErrors] = useState<Record<string, string[]>>({});
async function handleSubmit(data: any) {
const result = await myServerAction(data);
if (!result.success) {
setErrors(result.errors || {});
} else {
console.log('Success:', result.data);
setErrors({});
}
}
return (
<div>
{errors.name && <span>{errors.name[0]}</span>}
</div>
);
}
*/