UNPKG

drizzle-zero

Version:

Generate Zero schemas from Drizzle ORM schemas

543 lines (534 loc) 22.3 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { createZeroTableBuilder: () => createZeroTableBuilder, drizzleZeroConfig: () => drizzleZeroConfig, getDrizzleColumnKeyFromColumnName: () => getDrizzleColumnKeyFromColumnName }); module.exports = __toCommonJS(index_exports); // src/relations.ts var import_zero2 = require("@rocicorp/zero"); var import_drizzle_orm3 = require("drizzle-orm"); // src/tables.ts var import_zero = require("@rocicorp/zero"); var import_drizzle_orm2 = require("drizzle-orm"); var import_casing = require("drizzle-orm/casing"); // src/db.ts var import_drizzle_orm = require("drizzle-orm"); var import_pg_core = require("drizzle-orm/pg-core"); var getTableConfigForDatabase = (table) => { if ((0, import_drizzle_orm.is)(table, import_pg_core.PgTable)) { return (0, import_pg_core.getTableConfig)(table); } throw new Error( `drizzle-zero: Unsupported table type: ${(0, import_drizzle_orm.getTableName)(table)}. Only Postgres tables are supported.` ); }; // src/drizzle-to-zero.ts var drizzleDataTypeToZeroType = { number: "number", bigint: "number", boolean: "boolean", date: "number" }; var drizzleColumnTypeToZeroType = { PgText: "string", PgChar: "string", PgVarchar: "string", PgUUID: "string", PgEnumColumn: "string", PgJsonb: "json", PgJson: "json", PgNumeric: "number", PgDateString: "number", PgTimestampString: "number" }; // src/util.ts function typedEntries(obj) { return Object.entries(obj); } function debugLog(debug, message, ...args) { if (debug) { console.log(`\u2139\uFE0F drizzle-zero: ${message}`, ...args); } } // src/tables.ts var createZeroTableBuilder = (tableName, table, columns, debug, casing) => { const actualTableName = (0, import_drizzle_orm2.getTableName)(table); const tableColumns = (0, import_drizzle_orm2.getTableColumns)(table); const tableConfig = getTableConfigForDatabase(table); const primaryKeysFromColumns = []; const columnsMapped = typedEntries(tableColumns).reduce( (acc, [key, column]) => { const columnConfig = columns?.[key]; if (columnConfig === false) { debugLog( debug, `Skipping column ${String(key)} because columnConfig is false` ); return acc; } const resolvedColumnName = !column.keyAsName || casing === void 0 ? column.name : casing === "camelCase" ? (0, import_casing.toCamelCase)(column.name) : (0, import_casing.toSnakeCase)(column.name); if (typeof columnConfig !== "boolean" && typeof columnConfig !== "object" && typeof columnConfig !== "undefined") { throw new Error( `drizzle-zero: Invalid column config for column ${resolvedColumnName} - expected boolean or ColumnBuilder but was ${typeof columnConfig}` ); } const isColumnBuilder = (value) => typeof value === "object" && value !== null && "schema" in value; const isColumnConfigOverride = isColumnBuilder(columnConfig); const type = drizzleColumnTypeToZeroType[column.columnType] ?? drizzleDataTypeToZeroType[column.dataType] ?? null; if (type === null && !isColumnConfigOverride) { console.warn( `\u{1F6A8} drizzle-zero: Unsupported column type: ${resolvedColumnName} - ${column.columnType} (${column.dataType}). It will not be included in the output. Must be supported by Zero, e.g.: ${Object.keys({ ...drizzleDataTypeToZeroType, ...drizzleColumnTypeToZeroType }).join(" | ")}` ); return acc; } const isColumnOptional = typeof columnConfig === "boolean" || typeof columnConfig === "undefined" ? column.hasDefault && column.defaultFn === void 0 ? true : !column.notNull : isColumnConfigOverride ? columnConfig.schema.optional : false; if (column.primary) { primaryKeysFromColumns.push(String(key)); } if (columnConfig && typeof columnConfig !== "boolean") { return { ...acc, [key]: columnConfig }; } const schemaValue = column.enumValues ? (0, import_zero.enumeration)() : type === "string" ? (0, import_zero.string)() : type === "number" ? (0, import_zero.number)() : type === "json" ? (0, import_zero.json)() : (0, import_zero.boolean)(); const schemaValueWithFrom = resolvedColumnName !== key ? schemaValue.from(resolvedColumnName) : schemaValue; return { ...acc, [key]: isColumnOptional ? schemaValueWithFrom.optional() : schemaValueWithFrom }; }, {} ); const primaryKeys = [ ...primaryKeysFromColumns, ...tableConfig.primaryKeys.flatMap( (k) => k.columns.map( (c) => getDrizzleColumnKeyFromColumnName({ columnName: c.name, table: c.table }) ) ) ]; if (!primaryKeys.length) { throw new Error( `drizzle-zero: No primary keys found in table - ${actualTableName}. Did you forget to define a primary key?` ); } const resolvedTableName = tableConfig.schema ? `${tableConfig.schema}.${actualTableName}` : actualTableName; const zeroBuilder = (0, import_zero.table)(tableName); const zeroBuilderWithFrom = resolvedTableName !== tableName ? zeroBuilder.from(resolvedTableName) : zeroBuilder; return zeroBuilderWithFrom.columns(columnsMapped).primaryKey(...primaryKeys); }; var getDrizzleColumnKeyFromColumnName = ({ columnName, table }) => { const tableColumns = (0, import_drizzle_orm2.getTableColumns)(table); return typedEntries(tableColumns).find( ([_name, column]) => column.name === columnName )?.[0]; }; // src/relations.ts var drizzleZeroConfig = (schema, config) => { let tables = []; const tableColumnNamesForSourceTable = /* @__PURE__ */ new Map(); const assertRelationNameIsNotAColumnName = ({ sourceTableName, relationName }) => { const tableColumnNames = tableColumnNamesForSourceTable.get(sourceTableName); if (tableColumnNames?.has(relationName)) { throw new Error( `drizzle-zero: Invalid relationship name for ${String(sourceTableName)}.${relationName}: there is already a table column with the name ${relationName} and this cannot be used as a relationship name` ); } }; for (const [tableName, tableOrRelations] of typedEntries(schema)) { if (!tableOrRelations) { throw new Error( `drizzle-zero: table or relation with key ${String(tableName)} is not defined` ); } if ((0, import_drizzle_orm3.is)(tableOrRelations, import_drizzle_orm3.Table)) { const table = tableOrRelations; const tableConfig = config?.tables?.[tableName]; if (tableConfig === false) { debugLog( config?.debug, `Skipping table ${String(tableName)} - no config provided` ); continue; } const tableSchema = createZeroTableBuilder( String(tableName), table, tableConfig, config?.debug, config?.casing ); tables.push(tableSchema); const tableColumnNames = /* @__PURE__ */ new Set(); for (const columnName of Object.keys(tableSchema.schema.columns)) { tableColumnNames.add(columnName); } tableColumnNamesForSourceTable.set(String(tableName), tableColumnNames); } } let relationships = {}; if (config?.manyToMany) { for (const [sourceTableName, manyConfig] of Object.entries( config.manyToMany )) { if (!manyConfig) continue; for (const [ relationName, [junctionTableNameOrObject, destTableNameOrObject] ] of Object.entries(manyConfig)) { if (typeof junctionTableNameOrObject === "string" && typeof destTableNameOrObject === "string") { const junctionTableName = junctionTableNameOrObject; const destTableName = destTableNameOrObject; const sourceTable = typedEntries(schema).find( ([tableName, tableOrRelations]) => (0, import_drizzle_orm3.is)(tableOrRelations, import_drizzle_orm3.Table) && tableName === sourceTableName )?.[1]; const destTable = typedEntries(schema).find( ([tableName, tableOrRelations]) => (0, import_drizzle_orm3.is)(tableOrRelations, import_drizzle_orm3.Table) && tableName === destTableName )?.[1]; const junctionTable = typedEntries(schema).find( ([tableName, tableOrRelations]) => (0, import_drizzle_orm3.is)(tableOrRelations, import_drizzle_orm3.Table) && tableName === junctionTableName )?.[1]; if (!sourceTable || !destTable || !junctionTable || !(0, import_drizzle_orm3.is)(sourceTable, import_drizzle_orm3.Table) || !(0, import_drizzle_orm3.is)(destTable, import_drizzle_orm3.Table) || !(0, import_drizzle_orm3.is)(junctionTable, import_drizzle_orm3.Table)) { throw new Error( `drizzle-zero: Invalid many-to-many configuration for ${String(sourceTableName)}.${relationName}: Could not find ${!sourceTable ? "source" : !destTable ? "destination" : "junction"} table` ); } const sourceJunctionFields = findRelationSourceAndDestFields(schema, { sourceTable, referencedTableName: (0, import_drizzle_orm3.getTableName)(junctionTable) }); const junctionDestFields = findRelationSourceAndDestFields(schema, { sourceTable: destTable, referencedTableName: (0, import_drizzle_orm3.getTableName)(junctionTable) }); if (!sourceJunctionFields.sourceFieldNames.length || !junctionDestFields.sourceFieldNames.length || !junctionDestFields.destFieldNames.length || !sourceJunctionFields.destFieldNames.length) { throw new Error( `drizzle-zero: Invalid many-to-many configuration for ${String(sourceTableName)}.${relationName}: Could not find relationships in junction table ${junctionTableName}` ); } if (!config.tables?.[junctionTableName] || !config.tables?.[sourceTableName] || !config.tables?.[destTableName]) { debugLog( config.debug, `Skipping many-to-many relationship - tables not in schema config:`, { junctionTable, sourceTableName, destTableName } ); continue; } assertRelationNameIsNotAColumnName({ sourceTableName, relationName }); relationships[sourceTableName] = { ...relationships?.[sourceTableName] ?? {}, [relationName]: [ { sourceField: sourceJunctionFields.sourceFieldNames, destField: sourceJunctionFields.destFieldNames, destSchema: junctionTableName, cardinality: "many" }, { sourceField: junctionDestFields.destFieldNames, destField: junctionDestFields.sourceFieldNames, destSchema: destTableName, cardinality: "many" } ] }; debugLog(config.debug, `Added many-to-many relationship:`, { sourceTable: sourceTableName, relationName, relationship: relationships[sourceTableName]?.[relationName] }); } else { const junctionTableName = junctionTableNameOrObject?.destTable ?? null; const junctionSourceField = junctionTableNameOrObject?.sourceField ?? null; const junctionDestField = junctionTableNameOrObject?.destField ?? null; const destTableName = destTableNameOrObject?.destTable ?? null; const destSourceField = destTableNameOrObject?.sourceField ?? null; const destDestField = destTableNameOrObject?.destField ?? null; if (!junctionSourceField || !junctionDestField || !destSourceField || !destDestField || !junctionTableName || !destTableName) { throw new Error( `drizzle-zero: Invalid many-to-many configuration for ${String(sourceTableName)}.${relationName}: Not all required fields were provided.` ); } if (!config.tables?.[junctionTableName] || !config.tables?.[sourceTableName] || !config.tables?.[destTableName]) { continue; } assertRelationNameIsNotAColumnName({ sourceTableName, relationName }); relationships[sourceTableName] = { ...relationships?.[sourceTableName] ?? {}, [relationName]: [ { sourceField: junctionSourceField, destField: junctionDestField, destSchema: junctionTableName, cardinality: "many" }, { sourceField: destSourceField, destField: destDestField, destSchema: destTableName, cardinality: "many" } ] }; } } } } for (const [relationName, tableOrRelations] of typedEntries(schema)) { if (!tableOrRelations) { throw new Error( `drizzle-zero: table or relation with key ${String(relationName)} is not defined` ); } if ((0, import_drizzle_orm3.is)(tableOrRelations, import_drizzle_orm3.Relations)) { const actualTableName = (0, import_drizzle_orm3.getTableName)(tableOrRelations.table); const tableName = getDrizzleKeyFromTableName({ schema, tableName: actualTableName }); const relationsConfig = getRelationsConfig(tableOrRelations); for (const relation of Object.values(relationsConfig)) { let sourceFieldNames = []; let destFieldNames = []; if ((0, import_drizzle_orm3.is)(relation, import_drizzle_orm3.One)) { sourceFieldNames = relation?.config?.fields?.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f?.name, table: f.table }) ) ?? []; destFieldNames = relation?.config?.references?.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f?.name, table: f.table }) ) ?? []; } if (!sourceFieldNames.length || !destFieldNames.length) { if (relation.relationName) { const sourceAndDestFields = findNamedSourceAndDestFields( schema, relation ); sourceFieldNames = sourceAndDestFields.sourceFieldNames; destFieldNames = sourceAndDestFields.destFieldNames; } else { const sourceAndDestFields = findRelationSourceAndDestFields( schema, relation ); sourceFieldNames = sourceAndDestFields.sourceFieldNames; destFieldNames = sourceAndDestFields.destFieldNames; } } if (!sourceFieldNames.length || !destFieldNames.length) { throw new Error( `drizzle-zero: No relationship found for: ${relation.fieldName} (${(0, import_drizzle_orm3.is)(relation, import_drizzle_orm3.One) ? "One" : "Many"} from ${String(tableName)} to ${relation.referencedTableName}). Did you forget to define ${relation.relationName ? `a named relation "${relation.relationName}"` : `an opposite ${(0, import_drizzle_orm3.is)(relation, import_drizzle_orm3.One) ? "Many" : "One"} relation`}?` ); } const referencedTableKey = getDrizzleKeyFromTableName({ schema, tableName: relation.referencedTableName }); if (typeof config?.tables !== "undefined" && (!config?.tables?.[tableName] || !config?.tables?.[referencedTableKey])) { debugLog( config?.debug, `Skipping relation - tables not in schema config:`, { sourceTable: tableName, referencedTable: referencedTableKey } ); continue; } if (relationships[tableName]?.[relation.fieldName]) { throw new Error( `drizzle-zero: Duplicate relationship found for: ${relation.fieldName} (from ${String(tableName)} to ${relation.referencedTableName}).` ); } assertRelationNameIsNotAColumnName({ sourceTableName: tableName, relationName: relation.fieldName }); relationships[tableName] = { ...relationships?.[tableName] ?? {}, [relation.fieldName]: [ { sourceField: sourceFieldNames, destField: destFieldNames, destSchema: getDrizzleKeyFromTableName({ schema, tableName: relation.referencedTableName }), cardinality: (0, import_drizzle_orm3.is)(relation, import_drizzle_orm3.One) ? "one" : "many" } ] }; } } } const finalSchema = (0, import_zero2.createSchema)({ tables, relationships: Object.entries(relationships).map(([key, value]) => ({ name: key, relationships: value })) }); debugLog( config?.debug, "Output Zero schema", JSON.stringify(finalSchema, null, 2) ); return finalSchema; }; var getReferencedTableName = (rel) => { if ("referencedTableName" in rel && rel.referencedTableName) { return rel.referencedTableName; } if ("referencedTable" in rel && rel.referencedTable) { return (0, import_drizzle_orm3.getTableName)(rel.referencedTable); } return void 0; }; var findRelationSourceAndDestFields = (schema, relation) => { const sourceTableName = (0, import_drizzle_orm3.getTableName)(relation.sourceTable); const referencedTableName = getReferencedTableName(relation); for (const tableOrRelations of Object.values(schema)) { if ((0, import_drizzle_orm3.is)(tableOrRelations, import_drizzle_orm3.Relations)) { const relationsConfig = getRelationsConfig(tableOrRelations); for (const relationConfig of Object.values(relationsConfig)) { if (!(0, import_drizzle_orm3.is)(relationConfig, import_drizzle_orm3.One)) continue; const foundSourceName = (0, import_drizzle_orm3.getTableName)(relationConfig.sourceTable); const foundReferencedName = getReferencedTableName(relationConfig); if (foundSourceName === referencedTableName && foundReferencedName === sourceTableName) { const sourceFieldNames = relationConfig.config?.references?.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f.name, table: f.table }) ) ?? []; const destFieldNames = relationConfig.config?.fields?.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f.name, table: f.table }) ) ?? []; if (sourceFieldNames.length && destFieldNames.length) { return { sourceFieldNames, destFieldNames }; } } if (foundSourceName === sourceTableName && foundReferencedName === referencedTableName) { const sourceFieldNames = relationConfig.config?.fields?.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f.name, table: f.table }) ) ?? []; const destFieldNames = relationConfig.config?.references?.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f.name, table: f.table }) ) ?? []; if (sourceFieldNames.length && destFieldNames.length) { return { sourceFieldNames, destFieldNames }; } } } } } return { sourceFieldNames: [], destFieldNames: [] }; }; var findNamedSourceAndDestFields = (schema, relation) => { for (const tableOrRelations of Object.values(schema)) { if ((0, import_drizzle_orm3.is)(tableOrRelations, import_drizzle_orm3.Relations)) { const relationsConfig = getRelationsConfig(tableOrRelations); for (const relationConfig of Object.values(relationsConfig)) { if ((0, import_drizzle_orm3.is)(relationConfig, import_drizzle_orm3.One) && relationConfig.relationName === relation.relationName) { return { destFieldNames: relationConfig.config?.fields?.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f.name, table: f.table }) ) ?? [], sourceFieldNames: relationConfig.config?.references?.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f.name, table: f.table }) ) ?? [] }; } } } } return { sourceFieldNames: [], destFieldNames: [] }; }; var getRelationsConfig = (relations) => { return relations.config( (0, import_drizzle_orm3.createTableRelationsHelpers)(relations.table) ); }; var getDrizzleKeyFromTableName = ({ schema, tableName }) => { return typedEntries(schema).find( ([_name, tableOrRelations]) => (0, import_drizzle_orm3.is)(tableOrRelations, import_drizzle_orm3.Table) && (0, import_drizzle_orm3.getTableName)(tableOrRelations) === tableName )?.[0]; }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { createZeroTableBuilder, drizzleZeroConfig, getDrizzleColumnKeyFromColumnName });