UNPKG

drizzle-zero

Version:

Generate Zero schemas from Drizzle ORM schemas

528 lines (521 loc) 20.2 kB
// src/relations.ts import { createSchema } from "@rocicorp/zero"; import { createTableRelationsHelpers, getTableName as getTableName3, is as is2, One, Relations, Table as Table2 } from "drizzle-orm"; // src/tables.ts import { boolean as zeroBoolean, enumeration as zeroEnumeration, json as zeroJson, number as zeroNumber, string as zeroString, table as zeroTable } from "@rocicorp/zero"; import { getTableColumns, getTableName as getTableName2 } from "drizzle-orm"; import { toCamelCase, toSnakeCase } from "drizzle-orm/casing"; // src/db.ts import { getTableName, is } from "drizzle-orm"; import { getTableConfig, PgTable } from "drizzle-orm/pg-core"; var getTableConfigForDatabase = (table) => { if (is(table, PgTable)) { return getTableConfig(table); } throw new Error( `drizzle-zero: Unsupported table type: ${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 = getTableName2(table); const tableColumns = 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" ? toCamelCase(column.name) : 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 ? zeroEnumeration() : type === "string" ? zeroString() : type === "number" ? zeroNumber() : type === "json" ? zeroJson() : zeroBoolean(); 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 = zeroTable(tableName); const zeroBuilderWithFrom = resolvedTableName !== tableName ? zeroBuilder.from(resolvedTableName) : zeroBuilder; return zeroBuilderWithFrom.columns(columnsMapped).primaryKey(...primaryKeys); }; var getDrizzleColumnKeyFromColumnName = ({ columnName, table }) => { const tableColumns = 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 (is2(tableOrRelations, Table2)) { 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]) => is2(tableOrRelations, Table2) && tableName === sourceTableName )?.[1]; const destTable = typedEntries(schema).find( ([tableName, tableOrRelations]) => is2(tableOrRelations, Table2) && tableName === destTableName )?.[1]; const junctionTable = typedEntries(schema).find( ([tableName, tableOrRelations]) => is2(tableOrRelations, Table2) && tableName === junctionTableName )?.[1]; if (!sourceTable || !destTable || !junctionTable || !is2(sourceTable, Table2) || !is2(destTable, Table2) || !is2(junctionTable, Table2)) { 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: getTableName3(junctionTable) }); const junctionDestFields = findRelationSourceAndDestFields(schema, { sourceTable: destTable, referencedTableName: getTableName3(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 (is2(tableOrRelations, Relations)) { const actualTableName = getTableName3(tableOrRelations.table); const tableName = getDrizzleKeyFromTableName({ schema, tableName: actualTableName }); const relationsConfig = getRelationsConfig(tableOrRelations); for (const relation of Object.values(relationsConfig)) { let sourceFieldNames = []; let destFieldNames = []; if (is2(relation, 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} (${is2(relation, One) ? "One" : "Many"} from ${String(tableName)} to ${relation.referencedTableName}). Did you forget to define ${relation.relationName ? `a named relation "${relation.relationName}"` : `an opposite ${is2(relation, 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: is2(relation, One) ? "one" : "many" } ] }; } } } const finalSchema = 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 getTableName3(rel.referencedTable); } return void 0; }; var findRelationSourceAndDestFields = (schema, relation) => { const sourceTableName = getTableName3(relation.sourceTable); const referencedTableName = getReferencedTableName(relation); for (const tableOrRelations of Object.values(schema)) { if (is2(tableOrRelations, Relations)) { const relationsConfig = getRelationsConfig(tableOrRelations); for (const relationConfig of Object.values(relationsConfig)) { if (!is2(relationConfig, One)) continue; const foundSourceName = getTableName3(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 (is2(tableOrRelations, Relations)) { const relationsConfig = getRelationsConfig(tableOrRelations); for (const relationConfig of Object.values(relationsConfig)) { if (is2(relationConfig, 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( createTableRelationsHelpers(relations.table) ); }; var getDrizzleKeyFromTableName = ({ schema, tableName }) => { return typedEntries(schema).find( ([_name, tableOrRelations]) => is2(tableOrRelations, Table2) && getTableName3(tableOrRelations) === tableName )?.[0]; }; export { createZeroTableBuilder, drizzleZeroConfig, getDrizzleColumnKeyFromColumnName };