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