Initial commit
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
*/
|
||||
@@ -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>;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
*/
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
*/
|
||||
Reference in New Issue
Block a user