UNPKG

drizzle-zero

Version:

Generate Zero schemas from Drizzle ORM schemas

1,341 lines (1,326 loc) 62.1 kB
#!/usr/bin/env node var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/cli/index.ts import { Command } from "commander"; import * as fs4 from "fs/promises"; import * as path4 from "path"; import { pathToFileURL as pathToFileURL3 } from "url"; import { Project } from "ts-morph"; // src/cli/config.ts import * as fs from "fs/promises"; import * as path from "path"; import * as url from "url"; import { tsImport } from "tsx/esm/api"; var defaultConfigFilePath = "drizzle-zero.config.ts"; var getDefaultConfigFilePath = async () => { const fullConfigPath = path.resolve(process.cwd(), defaultConfigFilePath); try { await fs.access(fullConfigPath); } catch { return null; } return "drizzle-zero.config.ts"; }; var getConfigFromFile = async ({ configFilePath, tsProject }) => { const fullConfigPath = path.resolve(process.cwd(), configFilePath); try { await fs.access(fullConfigPath); } catch { throw new Error( `\u274C drizzle-zero: Failed to find config file at ${fullConfigPath}` ); } const typeModuleErrorMessage = `. You may need to add \` "type": "module" \` to your package.json.`; try { const zeroConfigFilePathUrl = url.pathToFileURL(fullConfigPath).href; const zeroConfigImport = await tsImport( zeroConfigFilePathUrl, import.meta.url ); const exportName = (zeroConfigImport == null ? void 0 : zeroConfigImport.default) ? "default" : "schema"; const zeroSchema = (zeroConfigImport == null ? void 0 : zeroConfigImport.default) ?? (zeroConfigImport == null ? void 0 : zeroConfigImport.schema); const typeDeclarations = getZeroSchemaDefsFromConfig({ tsProject, configPath: fullConfigPath, exportName }); return { type: "config", zeroSchema, exportName, zeroSchemaTypeDeclarations: typeDeclarations }; } catch (error) { console.error( `\u274C drizzle-zero: Failed to import config file at ${fullConfigPath}${typeModuleErrorMessage}`, error ); process.exit(1); } }; function getZeroSchemaDefsFromConfig({ tsProject, configPath, exportName }) { const fileName = configPath.slice(configPath.lastIndexOf("/") + 1); const sourceFile = tsProject.getSourceFile(fileName); if (!sourceFile) { throw new Error( `\u274C drizzle-zero: Failed to find type definitions for ${fileName}` ); } const exportDeclarations = sourceFile.getExportedDeclarations(); for (const [name, declarations] of exportDeclarations.entries()) { for (const declaration of declarations) { if (exportName === name) { return [name, declaration]; } } } throw new Error( `\u274C drizzle-zero: No config type found in the config file - did you export \`default\` or \`schema\`? Found: ${sourceFile.getVariableDeclarations().map((v) => v.getName()).join(", ")}` ); } // src/cli/drizzle-kit.ts import * as fs2 from "fs/promises"; import * as path2 from "path"; import * as url2 from "url"; import { tsImport as tsImport2 } from "tsx/esm/api"; // src/relations.ts import { createSchema } from "@rocicorp/zero"; import { createTableRelationsHelpers, getTableName as getTableName3, getTableUniqueName, is as is2, One, Relations, Table } 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", PgArray: "json" }; var postgresTypeToZeroType = { // string-like "text": "string", "char": "string", "character": "string", "varchar": "string", "character varying": "string", "uuid": "string", "enum": "string", // enums are emitted via zero.enumeration([...]) and are strings // json-like "jsonb": "json", "json": "json", // number-like (all numeric types) "numeric": "number", "decimal": "number", "int": "number", "integer": "number", "smallint": "number", "bigint": "number", "int2": "number", "int4": "number", "int8": "number", "real": "number", "float4": "number", "float8": "number", "double precision": "number", "serial": "number", "bigserial": "number", // date/time mapped to number (epoch millis) "date": "number", "timestamp": "number", "timestamp without time zone": "number", "timestamp with time zone": "number", "timestamptz": "number", // boolean "boolean": "boolean", "bool": "boolean" }; // 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 warnedServerDefaults = /* @__PURE__ */ new Set(); var createZeroTableBuilder = (tableName, table, columns, debug, casing) => { const actualTableName = getTableName2(table); const tableColumns = getTableColumns(table); const tableConfig = getTableConfigForDatabase(table); const columnNameToStableKey = new Map( typedEntries(tableColumns).map(([key, column]) => [ column.name, String(key) ]) ); const primaryKeys = /* @__PURE__ */ new Set(); for (const [key, column] of typedEntries(tableColumns)) { if (column.primary) { primaryKeys.add(String(key)); } } for (const pk of tableConfig.primaryKeys) { for (const pkColumn of pk.columns) { const key = columnNameToStableKey.get(pkColumn.name); if (key) { primaryKeys.add(String(key)); } } } const isColumnBuilder = (value) => typeof value === "object" && value !== null && "schema" in value; const columnsMapped = typedEntries(tableColumns).reduce( (acc, [key, column]) => { const columnConfig = typeof columns === "object" && columns !== null && columns !== void 0 ? columns == null ? void 0 : columns[key] : void 0; const isColumnConfigOverride = isColumnBuilder(columnConfig); const resolvedColumnName = !column.keyAsName || casing === void 0 ? column.name : casing === "camelCase" ? toCamelCase(column.name) : toSnakeCase(column.name); if (typeof columns === "object" && columns !== null) { if (columnConfig !== void 0 && typeof columnConfig !== "boolean" && !isColumnConfigOverride) { throw new Error( `drizzle-zero: Invalid column config for column ${resolvedColumnName} - expected boolean or ColumnBuilder but was ${typeof columnConfig}` ); } if (columnConfig !== true && !isColumnConfigOverride && !primaryKeys.has(String(key))) { debugLog( debug, `Skipping non-primary column ${resolvedColumnName} because it was not explicitly included in the config.` ); return acc; } } const type = drizzleColumnTypeToZeroType[column.columnType] ?? drizzleDataTypeToZeroType[column.dataType] ?? postgresTypeToZeroType[column.getSQLType()] ?? 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 isPrimaryKey = primaryKeys.has(String(key)); const hasServerDefault = column.hasDefault || typeof column.defaultFn !== "undefined"; if (hasServerDefault) { const warningKey = `${actualTableName}.${resolvedColumnName}`; if (!warnedServerDefaults.has(warningKey)) { warnedServerDefaults.add(warningKey); console.warn( `\u26A0\uFE0F drizzle-zero: Column ${actualTableName}.${resolvedColumnName} uses a database default that the Zero client will not be able to use. This probably won't work the way you expect. Set the value with mutators instead. See: https://github.com/rocicorp/drizzle-zero/issues/197` ); } } const isColumnOptional = typeof columnConfig === "boolean" || typeof columnConfig === "undefined" ? isPrimaryKey ? false : hasServerDefault ? true : !column.notNull : isColumnConfigOverride ? columnConfig.schema.optional : false; 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 }; }, {} ); if (primaryKeys.size === 0) { 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 }) => { var _a; const tableColumns = getTableColumns(table); return (_a = typedEntries(tableColumns).find( ([_name, column]) => column.name === columnName )) == null ? void 0 : _a[0]; }; // src/relations.ts var drizzleZeroConfig = (schema, config) => { var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r; let tables = []; const tableColumnNamesForSourceTable = /* @__PURE__ */ new Map(); const assertRelationNameIsNotAColumnName = ({ sourceTableName, relationName }) => { const tableColumnNames = tableColumnNamesForSourceTable.get(sourceTableName); if (tableColumnNames == null ? void 0 : 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, Table)) { const table = tableOrRelations; const tableConfig = (_a = config == null ? void 0 : config.tables) == null ? void 0 : _a[tableName]; if ((config == null ? void 0 : config.tables) !== void 0 && (tableConfig === false || tableConfig === void 0)) { debugLog( config == null ? void 0 : config.debug, `Skipping table ${String(tableName)} - ${tableConfig === false ? "explicitly excluded" : "not mentioned in config"}` ); continue; } const tableSchema = createZeroTableBuilder( String(tableName), table, tableConfig, config == null ? void 0 : config.debug, config == null ? void 0 : 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); } } if (tables.length === 0) { throw new Error( schema["tables"] ? "\u274C drizzle-zero: No tables found in the input - did you pass in a Zero schema to the `drizzleZeroConfig` function instead of a Drizzle schema?" : "\u274C drizzle-zero: No tables found in the input - did you export tables and relations from the Drizzle schema passed to the `drizzleZeroConfig` function?" ); } let relationships = {}; if (config == null ? void 0 : 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 = (_b = typedEntries(schema).find( ([tableName, tableOrRelations]) => is2(tableOrRelations, Table) && tableName === sourceTableName )) == null ? void 0 : _b[1]; const destTable = (_c = typedEntries(schema).find( ([tableName, tableOrRelations]) => is2(tableOrRelations, Table) && tableName === destTableName )) == null ? void 0 : _c[1]; const junctionTable = (_d = typedEntries(schema).find( ([tableName, tableOrRelations]) => is2(tableOrRelations, Table) && tableName === junctionTableName )) == null ? void 0 : _d[1]; if (!sourceTable || !destTable || !junctionTable || !is2(sourceTable, Table) || !is2(destTable, Table) || !is2(junctionTable, 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: getTableName3(junctionTable), referencedTable: junctionTable }); const junctionDestFields = findRelationSourceAndDestFields(schema, { sourceTable: destTable, referencedTableName: getTableName3(junctionTable), referencedTable: 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 (!((_e = config.tables) == null ? void 0 : _e[junctionTableName]) || !((_f = config.tables) == null ? void 0 : _f[sourceTableName]) || !((_g = config.tables) == null ? void 0 : _g[destTableName])) { debugLog( config.debug, `Skipping many-to-many relationship - tables not in schema config:`, { junctionTable, sourceTableName, destTableName } ); continue; } assertRelationNameIsNotAColumnName({ sourceTableName, relationName }); relationships[sourceTableName] = { ...relationships == null ? void 0 : 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: (_h = relationships[sourceTableName]) == null ? void 0 : _h[relationName] }); } else { const junctionTableName = (junctionTableNameOrObject == null ? void 0 : junctionTableNameOrObject.destTable) ?? null; const junctionSourceField = (junctionTableNameOrObject == null ? void 0 : junctionTableNameOrObject.sourceField) ?? null; const junctionDestField = (junctionTableNameOrObject == null ? void 0 : junctionTableNameOrObject.destField) ?? null; const destTableName = (destTableNameOrObject == null ? void 0 : destTableNameOrObject.destTable) ?? null; const destSourceField = (destTableNameOrObject == null ? void 0 : destTableNameOrObject.sourceField) ?? null; const destDestField = (destTableNameOrObject == null ? void 0 : 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 (!((_i = config.tables) == null ? void 0 : _i[junctionTableName]) || !((_j = config.tables) == null ? void 0 : _j[sourceTableName]) || !((_k = config.tables) == null ? void 0 : _k[destTableName])) { continue; } assertRelationNameIsNotAColumnName({ sourceTableName, relationName }); relationships[sourceTableName] = { ...relationships == null ? void 0 : 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 = getDrizzleKeyFromTable({ schema, table: tableOrRelations.table, fallbackTableName: actualTableName }); const relationsConfig = getRelationsConfig(tableOrRelations); for (const relation of Object.values(relationsConfig)) { let sourceFieldNames = []; let destFieldNames = []; if (is2(relation, One)) { sourceFieldNames = ((_m = (_l = relation == null ? void 0 : relation.config) == null ? void 0 : _l.fields) == null ? void 0 : _m.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f == null ? void 0 : f.name, table: f.table }) )) ?? []; destFieldNames = ((_o = (_n = relation == null ? void 0 : relation.config) == null ? void 0 : _n.references) == null ? void 0 : _o.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f == null ? void 0 : 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 = getDrizzleKeyFromTable({ schema, table: relation.referencedTable, fallbackTableName: relation.referencedTableName }); if (typeof (config == null ? void 0 : config.tables) !== "undefined" && (!((_p = config == null ? void 0 : config.tables) == null ? void 0 : _p[tableName]) || !((_q = config == null ? void 0 : config.tables) == null ? void 0 : _q[referencedTableKey]))) { debugLog( config == null ? void 0 : config.debug, `Skipping relation - tables not in schema config:`, { sourceTable: tableName, referencedTable: referencedTableKey } ); continue; } if ((_r = relationships[tableName]) == null ? void 0 : _r[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 == null ? void 0 : relationships[tableName], [relation.fieldName]: [ { sourceField: sourceFieldNames, destField: destFieldNames, destSchema: getDrizzleKeyFromTable({ schema, table: relation.referencedTable, fallbackTableName: 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 == null ? void 0 : config.debug, "Output Zero schema", JSON.stringify(finalSchema, null, 2) ); return finalSchema; }; var getReferencedTableName = (rel) => { if ("referencedTable" in rel && rel.referencedTable) { return getTableUniqueName(rel.referencedTable); } if ("referencedTableName" in rel && rel.referencedTableName) { return rel.referencedTableName; } return void 0; }; var findRelationSourceAndDestFields = (schema, relation) => { var _a, _b, _c, _d, _e, _f, _g, _h; const sourceTableName = getTableUniqueName(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 = getTableUniqueName(relationConfig.sourceTable); const foundReferencedName = getReferencedTableName(relationConfig); if (foundSourceName === referencedTableName && foundReferencedName === sourceTableName) { const sourceFieldNames = ((_b = (_a = relationConfig.config) == null ? void 0 : _a.references) == null ? void 0 : _b.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f.name, table: f.table }) )) ?? []; const destFieldNames = ((_d = (_c = relationConfig.config) == null ? void 0 : _c.fields) == null ? void 0 : _d.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f.name, table: f.table }) )) ?? []; if (sourceFieldNames.length && destFieldNames.length) { return { sourceFieldNames, destFieldNames }; } } if (foundSourceName === sourceTableName && foundReferencedName === referencedTableName) { const sourceFieldNames = ((_f = (_e = relationConfig.config) == null ? void 0 : _e.fields) == null ? void 0 : _f.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f.name, table: f.table }) )) ?? []; const destFieldNames = ((_h = (_g = relationConfig.config) == null ? void 0 : _g.references) == null ? void 0 : _h.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f.name, table: f.table }) )) ?? []; if (sourceFieldNames.length && destFieldNames.length) { return { sourceFieldNames, destFieldNames }; } } } } } return { sourceFieldNames: [], destFieldNames: [] }; }; var findNamedSourceAndDestFields = (schema, relation) => { var _a, _b, _c, _d; 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: ((_b = (_a = relationConfig.config) == null ? void 0 : _a.fields) == null ? void 0 : _b.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f.name, table: f.table }) )) ?? [], sourceFieldNames: ((_d = (_c = relationConfig.config) == null ? void 0 : _c.references) == null ? void 0 : _d.map( (f) => getDrizzleColumnKeyFromColumnName({ columnName: f.name, table: f.table }) )) ?? [] }; } } } } return { sourceFieldNames: [], destFieldNames: [] }; }; var getRelationsConfig = (relations) => relations.config(createTableRelationsHelpers(relations.table)); var getDrizzleKeyFromTable = ({ schema, table, fallbackTableName }) => { var _a, _b, _c; if (table) { const directMatch = (_a = typedEntries(schema).find( ([_name, tableOrRelations]) => is2(tableOrRelations, Table) && tableOrRelations === table )) == null ? void 0 : _a[0]; if (directMatch) { return directMatch; } const uniqueName = getTableUniqueName(table); const uniqueMatch = (_b = typedEntries(schema).find( ([_name, tableOrRelations]) => is2(tableOrRelations, Table) && getTableUniqueName(tableOrRelations) === uniqueName )) == null ? void 0 : _b[0]; if (uniqueMatch) { return uniqueMatch; } } if (fallbackTableName) { const fallbackMatch = (_c = typedEntries(schema).find( ([_name, tableOrRelations]) => is2(tableOrRelations, Table) && getTableName3(tableOrRelations) === fallbackTableName )) == null ? void 0 : _c[0]; if (fallbackMatch) { return fallbackMatch; } } throw new Error( `drizzle-zero: Unable to resolve table key for ${table ? getTableUniqueName(table) : fallbackTableName}` ); }; // src/cli/ts-project.ts var PERMISSION_ERROR_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM"]); function isFsPermissionError(error) { return error instanceof Error && "code" in error && typeof error.code === "string" && PERMISSION_ERROR_CODES.has(error.code ?? ""); } function addSourceFilesFromTsConfigSafe({ tsProject, tsConfigPath, debug = false }) { try { tsProject.addSourceFilesFromTsConfig(tsConfigPath); return true; } catch (error) { if (isFsPermissionError(error)) { const pathInfo = error.path ? ` while reading ${error.path}` : ""; console.warn( `\u26A0\uFE0F drizzle-zero: Skipping files from ${tsConfigPath} due to a permission error${pathInfo} (${error.code}).` ); if (debug) { console.warn(error); } return false; } throw error; } } function ensureSourceFileInProject({ tsProject, filePath, debug }) { const existingSourceFile = tsProject.getSourceFile(filePath) ?? tsProject.addSourceFileAtPathIfExists(filePath); if (existingSourceFile) { return existingSourceFile; } try { return tsProject.addSourceFileAtPath(filePath); } catch (error) { if (debug) { console.warn( `\u26A0\uFE0F drizzle-zero: Could not load ${filePath} into the TypeScript project.`, error ); } return; } } // src/cli/drizzle-kit.ts var getDefaultConfig = async ({ drizzleSchemaPath, drizzleKitConfigPath, tsProject, debug }) => { const { drizzleSchemaPath: resolvedDrizzleSchemaPath, casing: drizzleCasing } = await getFullDrizzleSchemaFilePath({ drizzleSchemaPath, drizzleKitConfigPath }); const resolvedDrizzleSchemaPathUrl = url2.pathToFileURL( resolvedDrizzleSchemaPath ).href; const drizzleSchema = await tsImport2( resolvedDrizzleSchemaPathUrl, import.meta.url ); const zeroSchema = drizzleZeroConfig(drizzleSchema, { casing: drizzleCasing ?? void 0, debug: Boolean(debug) }); ensureSourceFileInProject({ tsProject, filePath: resolvedDrizzleSchemaPath, debug: Boolean(debug) }); return { type: "drizzle-kit", zeroSchema, drizzleSchemaSourceFile: getDrizzleSchemaSourceFile({ tsProject, drizzleSchemaPath: resolvedDrizzleSchemaPath }), drizzleCasing }; }; var getFullDrizzleSchemaFilePath = async ({ drizzleKitConfigPath, drizzleSchemaPath }) => { const typeModuleErrorMessage = `. You may need to add \` "type": "module" \` to your package.json.`; if (drizzleSchemaPath) { const fullPath = path2.resolve(process.cwd(), drizzleSchemaPath); try { await fs2.access(fullPath); return { drizzleSchemaPath: fullPath, casing: null }; } catch { console.error( `\u274C drizzle-zero: could not find Drizzle schema file at ${fullPath}` ); process.exit(1); } } if (drizzleKitConfigPath) { const fullPath = path2.resolve(process.cwd(), drizzleKitConfigPath); try { await fs2.access(fullPath); const drizzleKitConfigFilePathUrl = url2.pathToFileURL(fullPath).href; const drizzleKitConfigImport = await tsImport2( drizzleKitConfigFilePathUrl, import.meta.url ); const drizzleKitConfig = drizzleKitConfigImport == null ? void 0 : drizzleKitConfigImport.default; try { if (Array.isArray(drizzleKitConfig.schema)) { throw new Error( "\u274C drizzle-zero: Drizzle Kit config schema is an array. Please specify a single schema file for imports to be able to work correctly." ); } if (drizzleKitConfig.schema) { const fullPath2 = path2.resolve(process.cwd(), drizzleKitConfig.schema); await fs2.access(fullPath2); return { drizzleSchemaPath: fullPath2, casing: drizzleKitConfig.casing ?? null }; } } catch (error) { console.error( `\u274C drizzle-zero: could not find Drizzle file pulled from Drizzle Kit config at ${JSON.stringify(drizzleKitConfig)}`, error ); process.exit(1); } return { drizzleSchemaPath: fullPath, casing: drizzleKitConfig.casing ?? null }; } catch (error) { console.error( `\u274C drizzle-zero: could not find Drizzle Kit config file at ${drizzleKitConfigPath}${typeModuleErrorMessage}`, error ); process.exit(1); } } console.error(`\u274C drizzle-zero: could not find Drizzle Kit config file`); process.exit(1); }; function getDrizzleSchemaSourceFile({ tsProject, drizzleSchemaPath }) { const sourceFile = tsProject.getSourceFile(drizzleSchemaPath); if (!sourceFile) { throw new Error( `\u274C drizzle-zero: Failed to find type definitions for ${drizzleSchemaPath}` ); } return sourceFile; } // src/cli/shared.ts import camelCase from "camelcase"; import pluralize from "pluralize"; import { VariableDeclarationKind } from "ts-morph"; // src/cli/type-resolution.ts import { StructureKind } from "ts-morph"; import { TypeFormatFlags } from "typescript"; var COLUMN_SEPARATOR = "::|::"; var RESOLVER_FILE_NAME = "__drizzle_zero_type_resolver.ts"; var typeFormatFlags = TypeFormatFlags.UseAliasDefinedOutsideCurrentScope | TypeFormatFlags.NoTruncation | TypeFormatFlags.WriteArrowStyleSignature; function resolveCustomTypes({ project, helperName, schemaTypeExpression, schemaImports, requests }) { const uniqueRequests = /* @__PURE__ */ new Map(); for (const request of requests) { const key = `${request.tableName}${COLUMN_SEPARATOR}${request.columnName}`; if (!uniqueRequests.has(key)) { uniqueRequests.set(key, request); } } if (uniqueRequests.size === 0) { return /* @__PURE__ */ new Map(); } const resolverFile = project.createSourceFile(RESOLVER_FILE_NAME, "", { overwrite: true }); resolverFile.addImportDeclarations( schemaImports.map( (structure) => ({ kind: StructureKind.ImportDeclaration, ...structure }) ) ); resolverFile.addImportDeclaration({ moduleSpecifier: "drizzle-zero", namedImports: [{ name: helperName }], isTypeOnly: true }); const aliasByRequest = /* @__PURE__ */ new Map(); for (const [key, request] of uniqueRequests) { const aliasName = `__DZ_CT_${aliasByRequest.size}`; const typeExpression = `${helperName}<${schemaTypeExpression}, "${request.tableName}", "${request.columnName}">`; aliasByRequest.set( key, resolverFile.addTypeAlias({ name: aliasName, type: typeExpression, isExported: false }) ); } const resolved = /* @__PURE__ */ new Map(); for (const [key, alias] of aliasByRequest.entries()) { const type = alias.getType(); const text = type.getText(alias, typeFormatFlags); if (isSafeResolvedType(text)) { resolved.set(key, text); } } resolverFile.delete(); return resolved; } var allowedTypeIdentifiers = /* @__PURE__ */ new Set([ "boolean", "number", "string", "true", "false", "null", "undefined" ]); var isSafeResolvedType = (typeText) => { if (!typeText) { return false; } if (typeText === "ReadonlyJSONValue") { return true; } if (typeText === "unknown" || typeText === "any" || typeText.includes("__error__") || typeText.includes("() ") || typeText === "SchemaIsAnyError" || typeText.includes("CustomType") || typeText.includes("ZeroCustomType") || typeText.includes("import(") || typeText.includes("=>")) { return false; } const getPrevNonWhitespace = (index) => { for (let i = index - 1; i >= 0; i--) { const char = typeText[i] ?? ""; if (char.trim()) { return char; } } return ""; }; const getNextNonWhitespace = (index) => { for (let i = index; i < typeText.length; i++) { const char = typeText[i] ?? ""; if (char.trim()) { return char; } } return ""; }; const identifierRegex = /\b[A-Za-z_]\w*\b/g; const matches = typeText.matchAll(identifierRegex); for (const match of matches) { const identifier = match[0] ?? ""; const startIndex = match.index ?? 0; const endIndex = startIndex + identifier.length; const prevChar = getPrevNonWhitespace(startIndex); const nextChar = getNextNonWhitespace(endIndex); if (prevChar === "'" || prevChar === '"' || prevChar === "`") { continue; } if (/^_+$/.test(identifier) && prevChar === "}") { continue; } if (nextChar === ":") { continue; } if (nextChar === "?" && getNextNonWhitespace(endIndex + 1) === ":") { continue; } const normalized = identifier.toLowerCase(); if (identifier === normalized && allowedTypeIdentifiers.has(normalized)) { continue; } return false; } return true; }; // src/cli/shared.ts function getGeneratedSchema({ tsProject, result, outputFilePath, jsExtensionOverride = "auto", skipTypes = false, skipBuilder = false, skipDeclare = false, enableLegacyMutators = false, enableLegacyQueries = false, debug }) { var _a, _b; let needsJsExtension = jsExtensionOverride === "force"; if (jsExtensionOverride === "auto") { const compilerOptions = tsProject.getCompilerOptions(); const moduleResolution = compilerOptions.moduleResolution; needsJsExtension = moduleResolution === 3 || moduleResolution === 99; if (needsJsExtension && debug) { console.log( `\u2139\uFE0F drizzle-zero: Auto-detected moduleResolution requires .js extensions (moduleResolution=${moduleResolution})` ); } } const schemaObjectName = "schema"; const typename = "Schema"; const zeroSchemaGenerated = tsProject.createSourceFile(outputFilePath, "", { overwrite: true }); let resolverImportHelperFile; const getResolverImportModuleSpecifier = (sourceFile) => { if (!resolverImportHelperFile) { resolverImportHelperFile = tsProject.createSourceFile( "__drizzle_zero_type_resolver__imports.ts", "", { overwrite: true } ); } const moduleSpecifier = resolverImportHelperFile.getRelativePathAsModuleSpecifierTo(sourceFile); if (needsJsExtension && !moduleSpecifier.endsWith(".js")) { return `${moduleSpecifier}.js`; } return moduleSpecifier; }; let customTypeHelper; let zeroSchemaSpecifier; let schemaTypeExpression; const resolverImports = []; if (result.type === "config") { customTypeHelper = "ZeroCustomType"; zeroSchemaGenerated.addImportDeclaration({ moduleSpecifier: "drizzle-zero", namedImports: [{ name: customTypeHelper }], isTypeOnly: true }); const moduleSpecifier = zeroSchemaGenerated.getRelativePathAsModuleSpecifierTo( result.zeroSchemaTypeDeclarations[1].getSourceFile() ); const runtimeModuleSpecifier = needsJsExtension && !moduleSpecifier.endsWith(".js") ? `${moduleSpecifier}.js` : moduleSpecifier; zeroSchemaGenerated.addImportDeclaration({ moduleSpecifier: runtimeModuleSpecifier, namedImports: [{ name: result.exportName, alias: "zeroSchema" }], isTypeOnly: true }); resolverImports.push({ moduleSpecifier: getResolverImportModuleSpecifier( result.zeroSchemaTypeDeclarations[1].getSourceFile() ), namedImports: [{ name: result.exportName, alias: "zeroSchema" }], isTypeOnly: true }); zeroSchemaSpecifier = "typeof zeroSchema"; schemaTypeExpression = zeroSchemaSpecifier; } else { const moduleSpecifier = zeroSchemaGenerated.getRelativePathAsModuleSpecifierTo( result.drizzleSchemaSourceFile ); const runtimeModuleSpecifier = needsJsExtension && !moduleSpecifier.endsWith(".js") ? `${moduleSpecifier}.js` : moduleSpecifier; zeroSchemaGenerated.addImportDeclaration({ moduleSpecifier: runtimeModuleSpecifier, namespaceImport: "drizzleSchema", isTypeOnly: true }); customTypeHelper = "CustomType"; zeroSchemaGenerated.addImportDeclaration({ moduleSpecifier: "drizzle-zero", namedImports: [{ name: customTypeHelper }], isTypeOnly: true }); zeroSchemaSpecifier = "typeof drizzleSchema"; schemaTypeExpression = zeroSchemaSpecifier; resolverImports.push({ moduleSpecifier: getResolverImportModuleSpecifier( result.drizzleSchemaSourceFile ), namespaceImport: "drizzleSchema", isTypeOnly: true }); } const collectCustomTypeRequests = () => { var _a2; const requests = []; const tables = ((_a2 = result.zeroSchema) == null ? void 0 : _a2.tables) ?? {}; for (const [tableName, tableDef] of Object.entries(tables)) { if (!tableDef || typeof tableDef !== "object") { continue; } const columns = tableDef.columns; if (!columns || typeof columns !== "object") { continue; } for (const [columnName, columnDef] of Object.entries(columns)) { if (columnDef && typeof columnDef === "object" && Object.prototype.hasOwnProperty.call(columnDef, "customType") && columnDef.customType === null) { requests.push({ tableName, columnName }); } } } return requests; }; const customTypeRequests = schemaTypeExpression !== void 0 ? collectCustomTypeRequests() : []; resolverImportHelperFile == null ? void 0 : resolverImportHelperFile.delete(); const resolvedCustomTypes = schemaTypeExpression && customTypeRequests.length > 0 ? resolveCustomTypes({ project: tsProject, helperName: customTypeHelper, schemaTypeExpression, schemaImports: resolverImports, requests: customTypeRequests }) : /* @__PURE__ */ new Map(); const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value); const usedIdentifiers = /* @__PURE__ */ new Set([schemaObjectName]); const tableConstNames = /* @__PURE__ */ new Map(); const relationshipConstNames = /* @__PURE__ */ new Map(); let readonlyJSONValueImported = false; const sanitizeIdentifier = (value, fallback) => { const baseCandidate = camelCase(value, { pascalCase: false }) || value || fallback; const cleaned = baseCandidate.replace(/[^A-Za-z0-9_$]/g, "") || fallback; const startsValid = /^[A-Za-z_$]/.test(cleaned) ? cleaned : `_${cleaned}`; return startsValid.length > 0 ? startsValid : fallback; }; const ensureSuffix = (identifier, suffix) => identifier.toLowerCase().endsWith(suffix.toLowerCase()) ? identifier : `${identifier}${suffix}`; const getUniqueIdentifier = (identifier) => { let candidate = identifier; let counter = 2; while (usedIdentifiers.has(candidate)) { candidate = `${identifier}${counter}`; counter += 1; } usedIdentifiers.add(candidate); return candidate; }; const createConstName = (name, suffix, fallback) => getUniqueIdentifier( ensureSuffix(sanitizeIdentifier(name, fallback), suffix) ); const writeSchemaReferenceCollection = (writer, collection, constNameMap, indent) => { const indentStr = " ".repeat(indent); writer.write("{"); const entries = Object.entries(collection); if (entries.length > 0) { writer.newLine(); for (let i = 0; i < entries.length; i++) { const [key] = entries[i] ?? []; if (!key) { continue; } const identifier = constNameMap.get(key) ?? "undefined"; writer.write( indentStr + " " + JSON.stringify(key) + ": " + identifier ); if (i < entries.length - 1) { writer.write(","); } writer.newLine(); } writer.write(indentStr); } writer.write("}"); }; const writeValue = (writer, value, { keys = [], indent = 0, mode = "default" } = {}) => { const indentStr = " ".repeat(indent); if (!value || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || Array.isArray(value)) { const serialized2 = JSON.stringify(value); writer.write(serialized2 ?? "undefined"); return; } if (typeof value === "object" && value !== null) { writer.write("{"); const entries = Object.entries(value); if (entries.length > 0) { writer.newLine(); for (let i = 0; i < entries.length; i++) { const [key, propValue] = entries[i] ?? []; if (!key) { continue; } writer.write(indentStr + " " + JSON.stringify(key) + ": "); if (mode === "schema" && keys.length === 0 && key === "tables" && isRecord(propValue)) { writeSchemaReferenceCollection( writer, propValue, tableConstNames, indent + 2 ); } else if (mode === "schema" && keys.length === 0 && key === "relationships" && isRecord(propValue)) { writeSchemaReferenceCollection( writer, propValue, relationshipConstNames, indent + 2 ); } else if (key === "customType" && propValue === null) { const tableIndex = 1; const columnIndex = 3; const tableName = keys[tableIndex]; const columnName = keys[columnIndex]; const resolvedType = typeof tableName === "string" && typeof columnName === "string" ? resolvedCustomTypes.get( `${tableName}${COLUMN_SEPARATOR}${columnName}` ) : void 0; if (resolvedType) { writer.write(`null as unknown as ${resolvedType}`); if (resolvedType === "ReadonlyJSONValue") { readonlyJSONValueImported = true; } } else { writer.write( `null as unknown as ${customTypeHelper}<${zeroSchemaSpecifier}, "${keys[tableIndex]}", "${keys[columnIndex]}">` ); } } else if (key === "enableLegacyMutators") { writer.write(enableLegacyMutators ? "true" : "false"); } else if (key === "enableLegacyQueries") { writer.write(enableLegacyQueries ? "true" : "false"); } else { writeValue(writer, propValue, { keys: [...keys, key], indent: indent + 2, mode }); } if (i < entries.length - 1) { writer.write(","); } writer.newLine(); } writer.write(indentStr); } writer.write("}"); return; } const serialized = JSON.stringify(value); writer.write(serialized ?? "undefined"); }; let tableConstCount = 0; if (isRecord((_a = result.zeroSchema) == null ? void 0 : _a.tables)) { for (const [tableName, tableDef] of Object.entries( result.zeroSchema.tables )) { const constName = createConstName(tableName, "Table", "table"); tableConstNames.set(tableName, constName); if (tableConstCount > 0) { zeroSchemaGenerated.addStatements((writer) => writer.blankLine()); } zeroSchemaGenerated.addVariableStatement({ declarationKind: VariableDeclarationKind.Const, declarations: [ { name: constName, initializer: (writer) => { writeValue(writer, tableDef, { keys: ["tables", tableName] }); writer.write(" as const"); } } ] }); tableConstCount += 1; } } let relationshipConstCount = 0; if (isRecord((_b = result.zeroSchema) == null ? void 0 : _b.relationships)) { for (const [relationshipName, relationshipDef] of Object.entries( result.zeroSchema.relationships )) { const constName = createConstName( relationshipName, "Relationships", "relationships" ); relationshipConstNames.set(relationshipName, constName); if (relationshipConstCount === 0) { if (tableConstCount > 0) { zeroSchemaGenerated.addStatements((writer) => writer.blankLine()); } } else { zeroSchemaGenerated.addStatements((writer) => writer.blankLine()); } zeroSchemaGenerated.addVariableStatement({ declarationKind: VariableDeclarationKind.Const, declarations: [ { name: constName, initializer: (writer) => { writeValue(writer, relationshipDef, { keys: ["relationships", relationshipName] }); writer.write(" as const"); } } ] }); relationshipConstCount += 1; } } const schemaVariable = zeroSchemaGenerated.addVariableStatement({ declarationKind: VariableDeclarationKind.Const, isExported: true, declarations: [ { name: schemaObjectName, initializer: (wri