@datazod/zod-sql
Version:
Convert Zod schemas to SQL table definitions with support for SQLite, PostgreSQL, and MySQL
102 lines (101 loc) • 4.8 kB
JavaScript
import { z } from 'zod';
import { mapZodToSql } from '../../maps';
import { buildColumnDefinition, getAutoIdColumn, getTimestampColumns, isInteger, isNullable, processNestedObject, quoteIdentifier } from '../../utils';
/**
* Creates a SQL DDL statement from a Zod schema
*/
export function createTableDDL(tableName, schema, options = {}) {
const { dialect = 'sqlite', primaryKey, indexes = {}, flattenDepth = 2, extraColumns = [], timestamps = false, autoId = false } = options;
const shape = schema.shape;
const cols = [];
const constraints = [];
// Prepare columns arrays for different positions
const startCols = [];
const mainCols = [];
const endCols = [];
// Add auto ID column if requested (always at the start)
if (autoId) {
const autoIdColumnDef = getAutoIdColumn(dialect, autoId);
if (autoIdColumnDef) {
startCols.push(autoIdColumnDef);
}
}
// Add timestamp columns if requested (after ID but before other columns)
if (timestamps) {
startCols.push(...getTimestampColumns(dialect));
}
// Process extra columns for 'start' position
for (const column of extraColumns) {
if (column.position === 'start') {
startCols.push(buildColumnDefinition(column, dialect));
}
}
// Process each field in the schema
for (const [key, type] of Object.entries(shape)) {
// Unwrap nullable/optional to get the correct SQL type
let unwrappedType = type;
if (unwrappedType instanceof z.ZodNullable ||
unwrappedType instanceof z.ZodOptional) {
unwrappedType = unwrappedType.unwrap();
}
if (unwrappedType instanceof z.ZodObject && flattenDepth > 0) {
// Flatten nested object
processNestedObject(key, unwrappedType, mainCols, flattenDepth, dialect);
}
else if (unwrappedType instanceof z.ZodNumber) {
// Explicitly handle numbers to ensure proper type mapping
const isInt = isInteger(unwrappedType);
let sqlType;
switch (dialect) {
case 'postgres':
sqlType = isInt ? 'INTEGER' : 'DOUBLE PRECISION';
break;
case 'mysql':
sqlType = isInt ? 'INT' : 'DOUBLE';
break;
case 'sqlite':
default:
sqlType = isInt ? 'INTEGER' : 'REAL';
}
const nullable = isNullable(type) ? '' : ' NOT NULL';
// Add PRIMARY KEY directly to the column if it's a single column primary key
const isPrimaryKey = primaryKey === key && !Array.isArray(primaryKey);
const pkStr = isPrimaryKey ? ' PRIMARY KEY' : '';
mainCols.push(`${quoteIdentifier(key, dialect)} ${sqlType}${nullable}${pkStr}`);
}
else {
// Handle other types including arrays
const sqlType = mapZodToSql(unwrappedType, dialect);
const nullable = isNullable(type) ? '' : ' NOT NULL';
// Add PRIMARY KEY directly to the column if it's a single column primary key
const isPrimaryKey = primaryKey === key && !Array.isArray(primaryKey);
const pkStr = isPrimaryKey ? ' PRIMARY KEY' : '';
mainCols.push(`${quoteIdentifier(key, dialect)} ${sqlType}${nullable}${pkStr}`);
}
}
// Process extra columns for 'end' position or unspecified position (default to end)
for (const column of extraColumns) {
if (!column.position || column.position === 'end') {
endCols.push(buildColumnDefinition(column, dialect));
}
}
// Combine all columns in the correct order: start, main, end
cols.push(...startCols, ...mainCols, ...endCols);
// Add compound primary key constraint if specified (and not already set in a column)
const hasPrimaryKeyColumn = extraColumns.some((col) => col.primaryKey) ||
(primaryKey && !Array.isArray(primaryKey)) ||
autoId;
if (primaryKey && Array.isArray(primaryKey) && !hasPrimaryKeyColumn) {
constraints.push(`PRIMARY KEY (${primaryKey.map((k) => quoteIdentifier(k, dialect)).join(', ')})`);
}
// Generate the CREATE TABLE statement
const allDefinitions = [...cols, ...constraints];
let sql = `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(tableName, dialect)} (\n ${allDefinitions.join(',\n ')}\n);`;
// Add indexes
for (const [indexName, columns] of Object.entries(indexes)) {
sql += `\nCREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName, dialect)} ON ${quoteIdentifier(tableName, dialect)} (${columns
.map((c) => quoteIdentifier(c, dialect))
.join(', ')});`;
}
return sql;
}