drizzle-zero
Version:
Generate Zero schemas from Drizzle ORM schemas
1,341 lines (1,326 loc) • 62.1 kB
JavaScript
#!/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