Savvi Studio

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())');
  });
});

These implementations show how PostgreSQL types are converted to Zod validation schemas.