drizzle-zero
Version:
Generate Zero schemas from Drizzle ORM schemas
528 lines (521 loc) • 20.2 kB
JavaScript
// 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
};