220 lines
5.4 KiB
TypeScript
220 lines
5.4 KiB
TypeScript
import { z } from 'zod';
|
|
import axios from 'axios';
|
|
|
|
interface ValidationMessages {
|
|
required: string;
|
|
invalid_type: string;
|
|
invalid_string: Record<string, string>;
|
|
too_small: Record<string, string>;
|
|
too_big: Record<string, string>;
|
|
invalid_date: string;
|
|
invalid_enum: string;
|
|
custom: Record<string, string>;
|
|
}
|
|
|
|
interface SchemaField {
|
|
type: string;
|
|
items?: string;
|
|
values?: any[];
|
|
validations?: Record<string, any>;
|
|
}
|
|
|
|
interface SchemaDefinition {
|
|
name: string;
|
|
fields: Record<string, SchemaField>;
|
|
messages: ValidationMessages;
|
|
}
|
|
|
|
class UnifiedSchemaBuilder {
|
|
private static instance: UnifiedSchemaBuilder;
|
|
private schemaCache: Map<string, z.ZodSchema> = new Map();
|
|
|
|
private constructor() {}
|
|
|
|
static getInstance(): UnifiedSchemaBuilder {
|
|
if (!UnifiedSchemaBuilder.instance) {
|
|
UnifiedSchemaBuilder.instance = new UnifiedSchemaBuilder();
|
|
}
|
|
return UnifiedSchemaBuilder.instance;
|
|
}
|
|
|
|
async getSchema(modelName: string): Promise<z.ZodSchema> {
|
|
// Check cache first
|
|
if (this.schemaCache.has(modelName)) {
|
|
return this.schemaCache.get(modelName)!;
|
|
}
|
|
|
|
// Fetch schema definition with messages from backend
|
|
const response = await axios.get<SchemaDefinition>(
|
|
`/api/schema/model/${modelName}`,
|
|
{
|
|
headers: {
|
|
'Accept-Language': navigator.language || 'tr'
|
|
}
|
|
}
|
|
);
|
|
|
|
const schema = this.buildSchema(response.data);
|
|
this.schemaCache.set(modelName, schema);
|
|
return schema;
|
|
}
|
|
|
|
private buildSchema(definition: SchemaDefinition): z.ZodSchema {
|
|
const shape: Record<string, z.ZodTypeAny> = {};
|
|
|
|
for (const [fieldName, field] of Object.entries(definition.fields)) {
|
|
shape[fieldName] = this.buildField(field, definition.messages);
|
|
}
|
|
|
|
return z.object(shape);
|
|
}
|
|
|
|
private buildField(
|
|
field: SchemaField,
|
|
messages: ValidationMessages
|
|
): z.ZodTypeAny {
|
|
let zodField: z.ZodTypeAny;
|
|
|
|
switch (field.type) {
|
|
case 'string':
|
|
zodField = z.string({
|
|
required_error: messages.required,
|
|
invalid_type_error: messages.invalid_type
|
|
});
|
|
break;
|
|
case 'email':
|
|
zodField = z.string().email(messages.invalid_string.email);
|
|
break;
|
|
case 'number':
|
|
zodField = z.number({
|
|
required_error: messages.required,
|
|
invalid_type_error: messages.invalid_type
|
|
});
|
|
break;
|
|
case 'boolean':
|
|
zodField = z.boolean({
|
|
required_error: messages.required,
|
|
invalid_type_error: messages.invalid_type
|
|
});
|
|
break;
|
|
case 'date':
|
|
zodField = z.date({
|
|
required_error: messages.required,
|
|
invalid_type_error: messages.invalid_date
|
|
});
|
|
break;
|
|
case 'array':
|
|
zodField = z.array(
|
|
this.buildField({ type: field.items! }, messages)
|
|
);
|
|
break;
|
|
case 'enum':
|
|
zodField = z.enum(field.values as [string, ...string[]], {
|
|
required_error: messages.required,
|
|
invalid_type_error: messages.invalid_enum
|
|
});
|
|
break;
|
|
default:
|
|
zodField = z.any();
|
|
}
|
|
|
|
// Apply validations if any
|
|
if (field.validations) {
|
|
zodField = this.applyValidations(zodField, field.validations, messages);
|
|
}
|
|
|
|
return zodField;
|
|
}
|
|
|
|
private applyValidations(
|
|
field: z.ZodTypeAny,
|
|
validations: Record<string, any>,
|
|
messages: ValidationMessages
|
|
): z.ZodTypeAny {
|
|
let result = field;
|
|
|
|
if ('min_length' in validations) {
|
|
result = (result as z.ZodString).min(
|
|
validations.min_length,
|
|
messages.too_small.string.replace(
|
|
'{min}',
|
|
validations.min_length.toString()
|
|
)
|
|
);
|
|
}
|
|
|
|
if ('max_length' in validations) {
|
|
result = (result as z.ZodString).max(
|
|
validations.max_length,
|
|
messages.too_big.string.replace(
|
|
'{max}',
|
|
validations.max_length.toString()
|
|
)
|
|
);
|
|
}
|
|
|
|
if ('pattern' in validations) {
|
|
result = (result as z.ZodString).regex(
|
|
new RegExp(validations.pattern),
|
|
messages.custom[validations.pattern_message] || 'Invalid format'
|
|
);
|
|
}
|
|
|
|
if ('gt' in validations) {
|
|
result = (result as z.ZodNumber).gt(
|
|
validations.gt,
|
|
messages.too_small.number.replace(
|
|
'{min}',
|
|
(validations.gt + 1).toString()
|
|
)
|
|
);
|
|
}
|
|
|
|
if ('lt' in validations) {
|
|
result = (result as z.ZodNumber).lt(
|
|
validations.lt,
|
|
messages.too_big.number.replace(
|
|
'{max}',
|
|
(validations.lt - 1).toString()
|
|
)
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const schemaBuilder = UnifiedSchemaBuilder.getInstance();
|
|
|
|
// Usage example:
|
|
/*
|
|
import { schemaBuilder } from './validation/unifiedSchemaBuilder';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { useForm } from 'react-hook-form';
|
|
|
|
function UserForm() {
|
|
const [schema, setSchema] = useState<z.ZodSchema | null>(null);
|
|
|
|
useEffect(() => {
|
|
async function loadSchema() {
|
|
const userSchema = await schemaBuilder.getSchema('User');
|
|
setSchema(userSchema);
|
|
}
|
|
loadSchema();
|
|
}, []);
|
|
|
|
const form = useForm({
|
|
resolver: schema ? zodResolver(schema) : undefined
|
|
});
|
|
|
|
if (!schema) return <div>Loading...</div>;
|
|
|
|
return (
|
|
<form onSubmit={form.handleSubmit(data => console.log(data))}>
|
|
{/* Your form fields */}
|
|
</form>
|
|
);
|
|
}
|
|
*/
|