Schema Builder Implementation Examples
Purpose: Detailed code examples for schema builder implementations
Last Updated: 2024-11-28
Base Builder
Abstract Base Class
abstract class BaseBuilder {
constructor(protected typeCatalog: TypeCatalog) {}
abstract build(type: Type): string;
abstract canHandle(type: Type): boolean;
protected buildTypeSchema(type: Type): string {
// Recursively build schema for any type
const builder = this.findBuilder(type);
return builder.build(type);
}
}
EnumBuilder
Basic Implementation
class EnumBuilder extends BaseBuilder {
canHandle(type: Type): boolean {
return type.category === 'enum';
}
build(type: EnumType): string {
const values = type.values
.map(v => `'${this.escapeString(v)}'`)
.join(', ');
return `z.enum([${values}])`;
}
private escapeString(value: string): string {
return value.replace(/'/g, "\\'");
}
}
Usage Example
const builder = new EnumBuilder(typeCatalog);
const type: EnumType = {
name: 'status',
category: 'enum',
values: ['active', 'inactive', 'pending']
};
const schema = builder.build(type);
// Result: z.enum(['active', 'inactive', 'pending'])
DomainBuilder
Full Implementation
class DomainBuilder extends BaseBuilder {
canHandle(type: Type): boolean {
return type.category === 'domain';
}
build(type: DomainType): string {
const baseSchema = this.getBaseTypeSchema(type.baseType);
if (!type.constraint) {
return baseSchema;
}
const refinement = this.parseConstraint(type.constraint);
if (!refinement) {
console.warn(`Could not parse constraint for ${type.name}`);
return baseSchema;
}
return `${baseSchema}.refine(${refinement})`;
}
private parseConstraint(constraint: string): string | null {
// Extract constraint expression
const match = constraint.match(/CHECK\s*\((.+)\)/i);
if (!match) return null;
const expr = match[1];
// Convert PostgreSQL patterns to JavaScript
return this.convertConstraint(expr);
}
private convertConstraint(expr: string): string {
// Simple pattern matching for common constraints
// Email pattern
if (expr.includes('~') && expr.includes('@')) {
const regex = this.extractRegex(expr);
return `(val) => /${regex}/.test(val)`;
}
// Length constraints
if (expr.includes('length(')) {
return this.convertLengthConstraint(expr);
}
// Numeric ranges
if (expr.includes('>=') || expr.includes('<=')) {
return this.convertRangeConstraint(expr);
}
// Fallback: return generic validator
return `(val) => true /* TODO: implement ${expr} */`;
}
private extractRegex(expr: string): string {
const match = expr.match(/'([^']+)'/);
return match ? match[1] : '.+';
}
}
Constraint Conversion Examples
// Email constraint
const emailDomain = {
constraint: "CHECK (VALUE ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$')"
};
// Result: .refine((val) => /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(val))
// Length constraint
const usernameDomain = {
constraint: "CHECK (length(VALUE) >= 3 AND length(VALUE) <= 50)"
};
// Result: .min(3).max(50)
// Numeric range
const ageDomain = {
constraint: "CHECK (VALUE >= 0 AND VALUE <= 120)"
};
// Result: .min(0).max(120)
CompositeBuilder
Full Implementation
class CompositeBuilder extends BaseBuilder {
canHandle(type: Type): boolean {
return type.category === 'composite';
}
build(type: CompositeType): string {
const fields = type.fields
.map(f => this.buildField(f))
.join(',\n');
return `z.object({\n${fields}\n})`;
}
private buildField(field: CompositeField): string {
const fieldType = this.typeCatalog.getType(field.typeOid);
let schema = this.buildTypeSchema(fieldType);
// Add nullable modifier
if (field.nullable) {
schema += '.nullable()';
}
// Add optional modifier for fields with defaults
if (field.hasDefault) {
schema += '.optional()';
}
return ` ${field.name}: ${schema}`;
}
}
Usage Examples
// Simple composite
const userType: CompositeType = {
name: 'user_record',
category: 'composite',
fields: [
{ name: 'id', typeOid: INT8_OID, nullable: false },
{ name: 'email', typeOid: TEXT_OID, nullable: false }
]
};
const schema = builder.build(userType);
// Result:
// z.object({
// id: z.bigint(),
// email: z.string()
// })
// Composite with nullable fields
const profileType: CompositeType = {
name: 'profile',
category: 'composite',
fields: [
{ name: 'user_id', typeOid: INT8_OID, nullable: false },
{ name: 'bio', typeOid: TEXT_OID, nullable: true },
{ name: 'avatar_url', typeOid: TEXT_OID, nullable: true }
]
};
// Result:
// z.object({
// user_id: z.bigint(),
// bio: z.string().nullable(),
// avatar_url: z.string().nullable()
// })
ArrayBuilder
Implementation
class ArrayBuilder extends BaseBuilder {
canHandle(type: Type): boolean {
return type.category === 'array';
}
build(type: ArrayType): string {
const elementType = this.typeCatalog.getType(type.elementOid);
const elementSchema = this.buildTypeSchema(elementType);
return `z.array(${elementSchema})`;
}
}
Usage Examples
// Simple array
const textArrayType: ArrayType = {
name: '_text',
category: 'array',
elementOid: TEXT_OID
};
// Result: z.array(z.string())
// Nested array
const matrixType: ArrayType = {
name: '__int4',
category: 'array',
elementOid: INT4_ARRAY_OID
};
// Result: z.array(z.array(z.number().int()))
// Array of composite
const userArrayType: ArrayType = {
name: '_user_record',
category: 'array',
elementOid: USER_RECORD_OID
};
// Result: z.array(userRecordSchema)
RangeBuilder
Implementation
class RangeBuilder extends BaseBuilder {
canHandle(type: Type): boolean {
return type.category === 'range';
}
build(type: RangeType): string {
const subtypeSchema = this.buildSubtypeSchema(type);
return `z.object({
lower: ${subtypeSchema},
upper: ${subtypeSchema},
lowerInclusive: z.boolean(),
upperInclusive: z.boolean()
})`;
}
private buildSubtypeSchema(type: RangeType): string {
const subtype = this.typeCatalog.getType(type.subtypeOid);
let schema = this.buildTypeSchema(subtype);
// Range bounds can be infinite (null)
return `${schema}.nullable()`;
}
}
Usage Examples
// Integer range
const int4rangeType: RangeType = {
name: 'int4range',
category: 'range',
subtypeOid: INT4_OID
};
// Result:
// z.object({
// lower: z.number().int().nullable(),
// upper: z.number().int().nullable(),
// lowerInclusive: z.boolean(),
// upperInclusive: z.boolean()
// })
// Date range
const daterangeType: RangeType = {
name: 'daterange',
category: 'range',
subtypeOid: DATE_OID
};
// Result:
// z.object({
// lower: z.string().date().nullable(),
// upper: z.string().date().nullable(),
// lowerInclusive: z.boolean(),
// upperInclusive: z.boolean()
// })
Base Type Mapping
Complete Mapping Implementation
const BASE_TYPE_SCHEMAS: Record<string, string> = {
// Integers
'int2': 'z.number().int()',
'int4': 'z.number().int()',
'int8': 'z.bigint()',
'smallint': 'z.number().int()',
'integer': 'z.number().int()',
'bigint': 'z.bigint()',
// Floating point
'float4': 'z.number()',
'float8': 'z.number()',
'real': 'z.number()',
'double precision': 'z.number()',
'numeric': 'z.number()',
'decimal': 'z.number()',
// Strings
'text': 'z.string()',
'varchar': 'z.string()',
'char': 'z.string()',
'bpchar': 'z.string()',
'name': 'z.string()',
// Boolean
'bool': 'z.boolean()',
'boolean': 'z.boolean()',
// Dates and times
'timestamp': 'z.string().datetime()',
'timestamptz': 'z.string().datetime()',
'date': 'z.string().date()',
'time': 'z.string().time()',
'timetz': 'z.string().time()',
'interval': 'z.string()',
// JSON
'json': 'z.unknown()',
'jsonb': 'z.unknown()',
// Binary
'bytea': 'z.instanceof(Buffer)',
// UUID
'uuid': 'z.string().uuid()',
// Network
'inet': 'z.string().ip()',
'cidr': 'z.string()',
'macaddr': 'z.string()',
// Special
'ltree': 'z.string()',
'xml': 'z.string()',
'void': 'z.void()'
};
function getBaseTypeSchema(typeName: string): string {
const schema = BASE_TYPE_SCHEMAS[typeName];
if (!schema) {
throw new CodegenError(`Unknown base type: ${typeName}`);
}
return schema;
}
Advanced Patterns
Circular References
class RecursiveBuilder extends BaseBuilder {
build(type: CompositeType): string {
if (this.hasSelfReference(type)) {
return this.buildLazy(type);
}
return this.buildNormal(type);
}
private buildLazy(type: CompositeType): string {
return `z.lazy(() => ${this.buildNormal(type)})`;
}
private hasSelfReference(type: CompositeType): boolean {
return type.fields.some(f => f.typeOid === type.oid);
}
}
Custom Validators
class CustomValidatorBuilder extends BaseBuilder {
build(type: DomainType): string {
const validator = this.getCustomValidator(type);
if (validator) {
return `${this.getBaseSchema(type)}.${validator}`;
}
return this.getBaseSchema(type);
}
private getCustomValidator(type: DomainType): string | null {
// Map known patterns to Zod validators
const validators: Record<string, string> = {
'email': 'email()',
'url': 'url()',
'uuid': 'uuid()',
'ip_address': 'ip()'
};
return validators[type.name] || null;
}
}
Schema Caching
class BuilderRegistry {
private schemaCache = new Map<string, string>();
getOrBuildSchema(type: Type): string {
const cached = this.schemaCache.get(type.oid.toString());
if (cached) {
return cached;
}
const schema = this.buildSchema(type);
this.schemaCache.set(type.oid.toString(), schema);
return schema;
}
private buildSchema(type: Type): string {
const builder = this.findBuilder(type);
return builder.build(type);
}
}
Error Handling
Builder Errors
class BuilderError extends Error {
constructor(
public type: Type,
message: string
) {
super(`Error building schema for ${type.name}: ${message}`);
this.name = 'BuilderError';
}
}
// Usage
if (!this.canHandle(type)) {
throw new BuilderError(type, 'No builder can handle this type');
}
Constraint Parsing Errors
class ConstraintParseError extends BuilderError {
constructor(
type: Type,
public constraint: string
) {
super(type, `Could not parse constraint: ${constraint}`);
this.name = 'ConstraintParseError';
}
}
// Usage
const refinement = this.parseConstraint(type.constraint);
if (!refinement) {
throw new ConstraintParseError(type, type.constraint);
}
Testing
Unit Tests
import { describe, it, expect } from 'vitest';
describe('EnumBuilder', () => {
it('builds enum schema', () => {
const type: EnumType = {
name: 'status',
category: 'enum',
values: ['active', 'inactive']
};
const builder = new EnumBuilder(typeCatalog);
const schema = builder.build(type);
expect(schema).toBe("z.enum(['active', 'inactive'])");
});
it('escapes special characters in enum values', () => {
const type: EnumType = {
name: 'special',
category: 'enum',
values: ["value's", 'value"s']
};
const builder = new EnumBuilder(typeCatalog);
const schema = builder.build(type);
expect(schema).toContain("\\'");
});
});
describe('CompositeBuilder', () => {
it('builds object schema', () => {
const type: CompositeType = {
name: 'user_record',
category: 'composite',
fields: [
{ name: 'id', typeOid: INT8_OID, nullable: false },
{ name: 'email', typeOid: TEXT_OID, nullable: false }
]
};
const builder = new CompositeBuilder(typeCatalog);
const schema = builder.build(type);
expect(schema).toContain('z.object({');
expect(schema).toContain('id: z.bigint()');
expect(schema).toContain('email: z.string()');
});
it('handles nullable fields', () => {
const type: CompositeType = {
name: 'profile',
category: 'composite',
fields: [
{ name: 'bio', typeOid: TEXT_OID, nullable: true }
]
};
const builder = new CompositeBuilder(typeCatalog);
const schema = builder.build(type);
expect(schema).toContain('.nullable()');
});
});
describe('ArrayBuilder', () => {
it('builds array schema', () => {
const type: ArrayType = {
name: '_text',
category: 'array',
elementOid: TEXT_OID
};
const builder = new ArrayBuilder(typeCatalog);
const schema = builder.build(type);
expect(schema).toBe('z.array(z.string())');
});
});
Related Documentation
- Schema Builders - Builder architecture overview
- Schema Examples - More schema examples
- Type System - Type mappings
These implementations show how PostgreSQL types are converted to Zod validation schemas.