cogsbox-shape
Version:
A TypeScript library for creating type-safe database schemas with Zod validation, SQL type definitions, and automatic client/server transformations. Unifies client, server, and database types through a single schema definition, with built-in support for r
124 lines (123 loc) • 5.4 kB
JavaScript
import fs from "fs/promises";
const sqlTypeMap = {
int: "INTEGER",
varchar: (length = 255) => `VARCHAR(${length})`,
char: (length = 1) => `CHAR(${length})`,
text: "TEXT",
longtext: "LONGTEXT",
boolean: "BOOLEAN",
date: "DATE",
datetime: "DATETIME",
};
function isWrappedSchema(input) {
return (input !== null &&
typeof input === "object" &&
"schemas" in input &&
input.schemas !== null &&
typeof input.schemas === "object");
}
export async function generateSQL(input, outputPath = "cogsbox-shape-sql.sql", options = { includeForeignKeys: true }) {
if (!input) {
throw new Error("No schema input provided");
}
const schemas = isWrappedSchema(input) ? input.schemas : input;
if (!schemas || typeof schemas !== "object") {
throw new Error("Invalid schemas input");
}
const sql = [];
for (const [name, schema] of Object.entries(schemas)) {
const tableName = schema._tableName;
if (!tableName) {
console.warn(`Skipping schema '${name}' - no _tableName found`);
continue;
}
const fields = [];
const foreignKeys = [];
for (const [fieldName, field] of Object.entries(schema)) {
// Skip metadata fields
const f = field; // Just cast once
console.log(`Processing field: ${fieldName}`, f);
// Skip metadata fields
if (fieldName === "_tableName" ||
fieldName === "SchemaWrapperBrand" ||
fieldName.startsWith("__") ||
typeof f !== "object" ||
!f)
continue;
// Handle reference fields
if (f.type === "reference" && f.to) {
const referencedField = f.to();
const targetTableName = referencedField.__parentTableType._tableName;
const targetFieldName = referencedField.__meta._key;
console.log(`Found reference field: ${fieldName} -> ${targetTableName}.${targetFieldName}`);
fields.push(` ${fieldName} INTEGER NOT NULL`);
if (options.includeForeignKeys) {
foreignKeys.push(` FOREIGN KEY (${fieldName}) REFERENCES ${targetTableName}(${targetFieldName})`);
}
continue;
}
// Get the actual field definition from enriched structure
let fieldDef = f;
// If it's an enriched field, extract the original field definition
if (f.__meta && f.__meta._fieldType) {
fieldDef = f.__meta._fieldType;
}
// Now check if fieldDef has config
if (fieldDef && fieldDef.config && fieldDef.config.sql) {
const sqlConfig = fieldDef.config.sql;
// Handle relation configs (hasMany, hasOne, etc.)
if (["hasMany", "hasOne", "belongsTo", "manyToMany"].includes(sqlConfig.type)) {
// Only belongsTo creates a column
if (sqlConfig.type === "belongsTo" &&
sqlConfig.fromKey &&
sqlConfig.schema) {
fields.push(` ${sqlConfig.fromKey} INTEGER`);
if (options.includeForeignKeys) {
const targetSchema = sqlConfig.schema();
foreignKeys.push(` FOREIGN KEY (${sqlConfig.fromKey}) REFERENCES ${targetSchema._tableName}(id)`);
}
}
continue;
}
// Handle regular SQL types
const { type, nullable, pk, length, default: defaultValue } = sqlConfig;
if (!sqlTypeMap[type]) {
console.warn(`Unknown SQL type: ${type} for field ${fieldName}`);
continue;
}
const sqlType = typeof sqlTypeMap[type] === "function"
? sqlTypeMap[type](length)
: sqlTypeMap[type];
let fieldDefStr = ` ${fieldName} ${sqlType}`;
if (pk)
fieldDefStr += " PRIMARY KEY AUTO_INCREMENT";
if (!nullable && !pk)
fieldDefStr += " NOT NULL";
// Handle defaults
if (defaultValue !== undefined &&
defaultValue !== "CURRENT_TIMESTAMP") {
fieldDefStr += ` DEFAULT ${typeof defaultValue === "string" ? `'${defaultValue}'` : defaultValue}`;
}
else if (defaultValue === "CURRENT_TIMESTAMP") {
fieldDefStr += " DEFAULT CURRENT_TIMESTAMP";
}
fields.push(fieldDefStr);
}
}
// Combine fields and foreign keys based on option
const allFields = options.includeForeignKeys
? [...fields, ...foreignKeys]
: fields;
// Create table SQL
if (allFields.length > 0) {
sql.push(`CREATE TABLE ${tableName} (\n${allFields.join(",\n")}\n);\n`);
}
else {
console.warn(`Warning: Table ${tableName} has no fields`);
}
}
// Write to file
const sqlContent = sql.join("\n");
await fs.writeFile(outputPath, sqlContent, "utf-8");
return sqlContent;
}