Savvi Studio

Schema Builder Implementations

Purpose: Overview of PostgreSQL to Zod schema builders
Last Updated: 2024-11-28

This document provides an overview of the schema builder architecture. For detailed implementations with code examples, see Builder Implementations.

Builder Architecture

BaseBuilder

Abstract base class for all schema builders:

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

Builder Types

EnumBuilder

Generates z.enum() schemas for PostgreSQL enums.

Handles: Enum types
Output: z.enum([value1, value2, ...])

Example:

CREATE TYPE status AS ENUM ('active', 'inactive');

z.enum(['active', 'inactive'])

DomainBuilder

Generates schemas for domain types with constraints.

Handles: Domain types
Output: Base schema with .refine() for constraints

Example:

CREATE DOMAIN email AS text CHECK (VALUE ~ '@');

z.string().refine((val) => /@/.test(val))

CompositeBuilder

Generates z.object() schemas for composite types.

Handles: Composite/record types
Output: z.object({ field1: schema1, ... })

Example:

CREATE TYPE user_record AS (id bigint, email text);

z.object({ id: z.bigint(), email: z.string() })

ArrayBuilder

Generates z.array() schemas for array types.

Handles: Array types
Output: z.array(elementSchema)

Example:

text[]

z.array(z.string())

RangeBuilder

Generates schemas for range types.

Handles: Range types
Output: Object with lower, upper, and inclusivity flags

Example:

daterange

z.object({ lower: z.string().date().nullable(), upper: z.string().date().nullable(), lowerInclusive: z.boolean(), upperInclusive: z.boolean() })

Base Type Mapping

Complete Mapping Table

const BASE_TYPE_SCHEMAS: Record<string, string> = {
  // Integers
  'int2': 'z.number().int()',
  'int4': 'z.number().int()',
  'int8': 'z.bigint()',
  
  // Floating point
  'float4': 'z.number()',
  'float8': 'z.number()',
  'numeric': 'z.number()',
  
  // Strings
  'text': 'z.string()',
  'varchar': 'z.string()',
  'char': '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()',
  
  // JSON
  'json': 'z.unknown()',
  'jsonb': 'z.unknown()',
  
  // Binary
  'bytea': 'z.instanceof(Buffer)',
  
  // UUID
  'uuid': 'z.string().uuid()',
  
  // Network
  'inet': 'z.string().ip()',
  
  // Special
  'ltree': '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

For self-referential types, use z.lazy():

// PostgreSQL
CREATE TYPE node AS (
    id bigint,
    children node[]
);

// Generated
export const nodeSchema: z.ZodType<Node> = z.lazy(() =>
  z.object({
    id: z.bigint(),
    children: z.array(nodeSchema)
  })
);

Custom Validators

Map common domain patterns to Zod validators:

const validators: Record<string, string> = {
  'email': 'email()',
  'url': 'url()',
  'uuid': 'uuid()',
  'ip_address': 'ip()'
};

Schema Caching

Cache generated schemas to avoid redundant generation:

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;
  }
}

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

Constraint Parsing Errors

class ConstraintParseError extends BuilderError {
  constructor(
    type: Type,
    public constraint: string
  ) {
    super(type, `Could not parse constraint: ${constraint}`);
    this.name = 'ConstraintParseError';
  }
}

Builder Selection

Selection Process

  1. Check if type is in base type mapping → use base schema
  2. Try each builder's canHandle() method
  3. First builder that returns true handles the type
  4. Throw error if no builder can handle the type

Builder Priority

Builders are checked in this order:

  1. EnumBuilder
  2. DomainBuilder
  3. CompositeBuilder
  4. ArrayBuilder
  5. RangeBuilder

Schema Generation Process

Step 1: Load Type Catalog

Introspect database and build type catalog with dependencies

Step 2: Topological Sort

Sort types so dependencies are generated before dependents

Step 3: Generate Schemas

For each type in sorted order:

  1. Find appropriate builder
  2. Generate Zod schema
  3. Generate TypeScript type using z.infer
  4. Write to output file

Step 4: Generate Index

Create index file exporting all schemas and types

Detailed Examples

For complete implementation details and code examples, see:

  • Builder Implementations - Full code examples

    • Complete builder implementations
    • Constraint conversion examples
    • Usage examples
    • Error handling
    • Unit tests
  • Schema Examples - Generated schema examples

    • Enum examples
    • Composite examples
    • Domain examples
    • Array examples
    • Range examples

These builders convert PostgreSQL type metadata into Zod validation schemas, providing runtime type safety.