UNPKG

@datazod/zod-sql

Version:

Convert Zod schemas to SQL table definitions with support for SQLite, PostgreSQL, and MySQL

166 lines (165 loc) 5.59 kB
import { z } from 'zod'; import { generateAutoIdConfig, filterExtraColumnsByPosition, unwrapOptionalTypes, shouldFlattenType, isInteger, mapTypeToSql } from '@datazod/shared'; /** * Extracts table structure metadata from Zod schema */ export function extractTableStructure(schema, options = {}) { const { primaryKey, indexes = {}, extraColumns = [], timestamps = false, autoId = false, flattenDepth = 2, dialect = 'sqlite' } = options; const columns = []; const primaryKeys = []; // Add auto ID column if enabled const autoIdConfig = generateAutoIdConfig(autoId); if (autoIdConfig) { const idName = autoIdConfig.name || 'id'; const idType = autoIdConfig.type === 'uuid' ? 'TEXT' : 'INTEGER'; columns.push({ name: idName, type: idType, notNull: true, primaryKey: true, unique: true }); primaryKeys.push(idName); } // Add timestamp columns if requested if (timestamps) { columns.push({ name: 'created_at', type: 'DATETIME', notNull: true, defaultValue: 'CURRENT_TIMESTAMP', primaryKey: false, unique: false }, { name: 'updated_at', type: 'DATETIME', notNull: true, defaultValue: 'CURRENT_TIMESTAMP', primaryKey: false, unique: false }); } // Add extra columns at start position const startColumns = filterExtraColumnsByPosition(extraColumns, 'start'); startColumns.forEach(col => { columns.push({ name: col.name, type: col.type, notNull: col.notNull !== false, defaultValue: col.defaultValue, primaryKey: col.primaryKey || false, unique: col.unique || false }); if (col.primaryKey) { primaryKeys.push(col.name); } }); // Process schema fields with flattening Object.entries(schema.shape).forEach(([key, zodType]) => { processZodTypeWithFlattening(key, zodType, columns, flattenDepth, dialect); }); // Add extra columns at end position const endColumns = filterExtraColumnsByPosition(extraColumns, 'end'); endColumns.forEach(col => { columns.push({ name: col.name, type: col.type, notNull: col.notNull !== false, defaultValue: col.defaultValue, primaryKey: col.primaryKey || false, unique: col.unique || false }); if (col.primaryKey) { primaryKeys.push(col.name); } }); // Handle primary key configuration if (primaryKey) { const pkColumns = Array.isArray(primaryKey) ? primaryKey : [primaryKey]; pkColumns.forEach(pk => { if (!primaryKeys.includes(pk)) { primaryKeys.push(pk); } const column = columns.find(col => col.name === pk); if (column) { column.primaryKey = true; } }); } return { columns, primaryKeys, indexes }; } function processZodTypeWithFlattening(name, zodType, columns, flattenDepth, dialect) { if (shouldFlattenType(zodType, flattenDepth)) { const { type } = unwrapOptionalTypes(zodType); processNestedObjectForStructure(name, type, columns, flattenDepth, dialect, zodType); } else { const column = processZodTypeToColumn(name, zodType, dialect); columns.push(column); } } function processNestedObjectForStructure(prefix, objectType, columns, depth, dialect, originalType) { if (depth <= 0) { const column = processZodTypeToColumn(prefix, originalType, dialect); columns.push(column); return; } const shape = objectType.shape; for (const [nestedKey, nestedType] of Object.entries(shape)) { const colName = `${prefix}_${nestedKey}`; if (shouldFlattenType(nestedType, depth)) { const { type } = unwrapOptionalTypes(nestedType); processNestedObjectForStructure(colName, type, columns, depth - 1, dialect, nestedType); } else { const column = processZodTypeToColumn(colName, nestedType, dialect); columns.push(column); } } } function processZodTypeToColumn(name, zodType, dialect) { const { type, isOptional } = unwrapOptionalTypes(zodType); let sqlType = 'TEXT'; let defaultValue; // Handle default values if (type instanceof z.ZodDefault) { defaultValue = String(type._def.defaultValue()); } // Handle numbers specifically if (type instanceof z.ZodNumber) { const isInt = isInteger(type); sqlType = mapTypeToSql(isInt ? 'integer' : 'number', dialect); } else { // Map other Zod types to SQL types switch (type._def.typeName) { case 'ZodString': sqlType = 'TEXT'; break; case 'ZodBoolean': sqlType = 'INTEGER'; break; case 'ZodDate': sqlType = 'DATETIME'; break; case 'ZodArray': case 'ZodObject': sqlType = 'TEXT'; // JSON serialized break; default: sqlType = 'TEXT'; } } return { name, type: sqlType, notNull: !isOptional, defaultValue, primaryKey: false, unique: false }; }