drizzle-dbml-generator
Version:
Convert your Drizzle ORM schema into DBML markup
436 lines (426 loc) • 17 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
mysqlGenerate: () => mysqlGenerate,
pgGenerate: () => pgGenerate,
sqliteGenerate: () => sqliteGenerate
});
module.exports = __toCommonJS(src_exports);
// src/dbml.ts
var DBML = class {
built = "";
insert(str) {
this.built += str;
return this;
}
concatAll(strs) {
for (let i = 0; i < strs.length; i++) {
this.insert(strs[i]);
this.newLine(2);
}
return this;
}
/**
* Escapes characters that aren't allowed in DBML surrounding the input with double quotes
*/
escapeSpaces(str) {
this.built += /\W/.test(str) || /^[0-9_]/.test(str) ? `"${str}"` : str;
return this;
}
escapeType(str) {
this.built += str.includes(" ") || str.includes(")[") ? `"${str}"` : str;
return this;
}
newLine(newLines = 1) {
this.built += "\n".repeat(newLines);
return this;
}
tab(tabs = 1) {
this.built += " ".repeat(tabs * 2);
return this;
}
build() {
return this.built.trimEnd();
}
};
// src/utils.ts
function formatList(items, escapeName, escapeSpaces = false) {
return items.reduce(
(str, item) => `${str}, ${escapeSpaces && item.includes(" ") ? escapeName(item) : item}`,
""
).slice(2);
}
function wrapColumns(columns, escapeName) {
const formatted = formatList(
columns.map((column) => column.name),
escapeName,
true
);
return columns.length === 1 ? columns[0].name : `(${formatted})`;
}
function wrapColumnNames(columns, escapeName) {
return columns.length === 1 ? columns[0] : `(${formatList(columns, escapeName)})`;
}
// src/generators/common.ts
var import_drizzle_orm = require("drizzle-orm");
// src/symbols.ts
var AnyInlineForeignKeys = Symbol.for("drizzle:AnyInlineForeignKeys");
var PgInlineForeignKeys = Symbol.for("drizzle:PgInlineForeignKeys");
var MySqlInlineForeignKeys = Symbol.for("drizzle:MySqlInlineForeignKeys");
var SQLiteInlineForeignKeys = Symbol.for("drizzle:SQLiteInlineForeignKeys");
var TableName = Symbol.for("drizzle:Name");
var Schema = Symbol.for("drizzle:Schema");
var ExtraConfigBuilder = Symbol.for("drizzle:ExtraConfigBuilder");
var ExtraConfigColumns = Symbol.for("drizzle:ExtraConfigColumns");
// src/generators/common.ts
var import_pg_core = require("drizzle-orm/pg-core");
var import_mysql_core = require("drizzle-orm/mysql-core");
var import_sqlite_core = require("drizzle-orm/sqlite-core");
var import_casing = require("drizzle-orm/casing");
var import_fs = require("fs");
var import_path = require("path");
var BaseGenerator = class {
schema;
relational;
generatedRefs = [];
InlineForeignKeys = AnyInlineForeignKeys;
buildQueryConfig = {
escapeName: () => "",
escapeParam: () => "",
escapeString: () => "",
casing: new import_casing.CasingCache()
};
constructor(schema, relational) {
this.schema = schema;
this.relational = relational;
}
isIncremental(_column) {
return false;
}
mapDefaultValue(value) {
let str = "";
if (typeof value === "string") {
str = `'${value}'`;
} else if (typeof value === "boolean" || typeof value === "number") {
str = `${value}`;
} else if (value === null) {
str = "null";
} else if (value instanceof Date) {
str = `'${value.toISOString().replace("T", " ").replace("Z", "")}'`;
} else if ((0, import_drizzle_orm.is)(value, import_drizzle_orm.SQL)) {
str = `\`${value.toQuery(this.buildQueryConfig).sql}\``;
} else {
str = `\`${JSON.stringify(value)}\``;
}
return str;
}
generateColumn(column) {
const dbml = new DBML().tab().escapeSpaces(column.name).insert(" ").escapeType(column.getSQLType());
const constraints = [];
if (column.primary) {
constraints.push("pk");
}
if (column.notNull) {
constraints.push("not null");
}
if (column.isUnique) {
constraints.push("unique");
}
if (this.isIncremental(column)) {
constraints.push("increment");
}
if (column.default !== void 0) {
constraints.push(`default: ${this.mapDefaultValue(column.default)}`);
}
if (constraints.length > 0) {
dbml.insert(` [${formatList(constraints, this.buildQueryConfig.escapeName)}]`);
}
return dbml.build();
}
generateTable(table) {
if (!this.relational) {
this.generateForeignKeys(table[this.InlineForeignKeys]);
}
const dbml = new DBML().insert("table ");
if (table[Schema]) {
dbml.escapeSpaces(table[Schema]).insert(".");
}
dbml.escapeSpaces(table[TableName]).insert(" {").newLine();
const columns = (0, import_drizzle_orm.getTableColumns)(table);
for (const columnName in columns) {
const column = columns[columnName];
const columnDBML = this.generateColumn(column);
dbml.insert(columnDBML).newLine();
}
const extraConfigBuilder = table[ExtraConfigBuilder];
const extraConfigColumns = table[ExtraConfigColumns];
const extraConfig = extraConfigBuilder?.(extraConfigColumns ?? {});
const builtIndexes = (Array.isArray(extraConfig) ? extraConfig : Object.values(extraConfig ?? {})).map((b) => b?.build(table)).filter((b) => b !== void 0).filter((index) => !((0, import_drizzle_orm.is)(index, import_pg_core.Check) || (0, import_drizzle_orm.is)(index, import_mysql_core.Check) || (0, import_drizzle_orm.is)(index, import_sqlite_core.Check)));
const fks = builtIndexes.filter(
(index) => (0, import_drizzle_orm.is)(index, import_pg_core.ForeignKey) || (0, import_drizzle_orm.is)(index, import_mysql_core.ForeignKey) || (0, import_drizzle_orm.is)(index, import_sqlite_core.ForeignKey)
);
if (!this.relational) {
this.generateForeignKeys(fks);
}
if (extraConfigBuilder && builtIndexes.length > fks.length) {
const indexes = extraConfig ?? {};
dbml.newLine().tab().insert("indexes {").newLine();
for (const indexName in indexes) {
const index = indexes[indexName].build(table);
dbml.tab(2);
if ((0, import_drizzle_orm.is)(index, import_pg_core.Index) || (0, import_drizzle_orm.is)(index, import_mysql_core.Index) || (0, import_drizzle_orm.is)(index, import_sqlite_core.Index)) {
const configColumns = index.config.columns.flatMap(
(entry) => (0, import_drizzle_orm.is)(entry, import_drizzle_orm.SQL) ? entry.queryChunks.filter((v) => (0, import_drizzle_orm.is)(v, import_drizzle_orm.Column)) : entry
);
const idxColumns = wrapColumns(
configColumns,
this.buildQueryConfig.escapeName
);
const idxProperties = index.config.name ? ` [name: '${index.config.name}'${index.config.unique ? ", unique" : ""}]` : "";
dbml.insert(`${idxColumns}${idxProperties}`);
}
if ((0, import_drizzle_orm.is)(index, import_pg_core.PrimaryKey) || (0, import_drizzle_orm.is)(index, import_mysql_core.PrimaryKey) || (0, import_drizzle_orm.is)(index, import_sqlite_core.PrimaryKey)) {
const pkColumns = wrapColumns(index.columns, this.buildQueryConfig.escapeName);
dbml.insert(`${pkColumns} [pk]`);
}
if ((0, import_drizzle_orm.is)(index, import_pg_core.UniqueConstraint) || (0, import_drizzle_orm.is)(index, import_mysql_core.UniqueConstraint) || (0, import_drizzle_orm.is)(index, import_sqlite_core.UniqueConstraint)) {
const uqColumns = wrapColumns(index.columns, this.buildQueryConfig.escapeName);
const uqProperties = index.name ? `[name: '${index.name}', unique]` : "[unique]";
dbml.insert(`${uqColumns} ${uqProperties}`);
}
dbml.newLine();
}
dbml.tab().insert("}").newLine();
}
dbml.insert("}");
return dbml.build();
}
generateEnum(_enum_) {
return "";
}
generateForeignKeys(fks) {
for (let i = 0; i < fks.length; i++) {
const sourceTable = fks[i].table;
const foreignTable = fks[i].reference().foreignTable;
const sourceSchema = sourceTable[Schema];
const foreignSchema = foreignTable[Schema];
const sourceColumns = fks[i].reference().columns;
const foreignColumns = fks[i].reference().foreignColumns;
const dbml = new DBML().insert(`ref ${fks[i].getName()}: `);
if (sourceSchema) {
dbml.escapeSpaces(sourceSchema).insert(".");
}
dbml.escapeSpaces(sourceTable[TableName]).insert(".").insert(wrapColumns(sourceColumns, this.buildQueryConfig.escapeName)).insert(" > ");
if (foreignSchema) {
dbml.escapeSpaces(foreignSchema).insert(".");
}
dbml.escapeSpaces(foreignTable[TableName]).insert(".").insert(wrapColumns(foreignColumns, this.buildQueryConfig.escapeName));
const actions = [
`delete: ${fks[i].onDelete || "no action"}`,
`update: ${fks[i].onUpdate || "no action"}`
];
const actionsStr = ` [${formatList(actions, this.buildQueryConfig.escapeName)}]`;
dbml.insert(actionsStr);
this.generatedRefs.push(dbml.build());
}
}
generateRelations(relations_) {
const left = {};
const right = {};
for (let i = 0; i < relations_.length; i++) {
const relations = relations_[i].config({
one: (0, import_drizzle_orm.createOne)(relations_[i].table),
many: (0, import_drizzle_orm.createMany)(relations_[i].table)
});
for (const relationName in relations) {
const relation = relations[relationName];
const tableNames = [
relations_[i].table[TableName],
relation.referencedTableName
].sort();
const key = `${tableNames[0]}-${tableNames[1]}${relation.relationName ? `-${relation.relationName}` : ""}`;
if ((0, import_drizzle_orm.is)(relation, import_drizzle_orm.One) && relation.config?.references.length || 0 > 0) {
left[key] = {
type: "one",
sourceSchema: relation.sourceTable[Schema],
sourceTable: relation.sourceTable[TableName],
sourceColumns: relation.config?.fields.map((col) => col.name) || [],
foreignSchema: relation.referencedTable[Schema],
foreignTable: relation.referencedTableName,
foreignColumns: relation.config?.references.map((col) => col.name) || []
};
} else {
right[key] = {
type: (0, import_drizzle_orm.is)(relation, import_drizzle_orm.One) ? "one" : "many"
};
}
}
}
for (const key in left) {
const sourceSchema = left[key].sourceSchema || "";
const sourceTable = left[key].sourceTable || "";
const foreignSchema = left[key].foreignSchema || "";
const foreignTable = left[key].foreignTable || "";
const sourceColumns = left[key].sourceColumns || [];
const foreignColumns = left[key].foreignColumns || [];
const relationType = right[key]?.type || "one";
if (sourceColumns.length === 0 || foreignColumns.length === 0) {
throw Error(
`Not enough information was provided to create relation between "${sourceTable}" and "${foreignTable}"`
);
}
const dbml = new DBML().insert("ref: ");
if (sourceSchema) {
dbml.escapeSpaces(sourceSchema).insert(".");
}
dbml.escapeSpaces(sourceTable).insert(".").insert(wrapColumnNames(sourceColumns, this.buildQueryConfig.escapeName)).insert(` ${relationType === "one" ? "-" : ">"} `);
if (foreignSchema) {
dbml.escapeSpaces(foreignSchema).insert(".");
}
dbml.escapeSpaces(foreignTable).insert(".").insert(wrapColumnNames(foreignColumns, this.buildQueryConfig.escapeName));
this.generatedRefs.push(dbml.build());
}
}
generate() {
const generatedEnums = [];
const generatedTables = [];
const relations = [];
for (const key in this.schema) {
const value = this.schema[key];
if ((0, import_pg_core.isPgEnum)(value)) {
generatedEnums.push(this.generateEnum(value));
} else if ((0, import_drizzle_orm.is)(value, import_pg_core.PgTable) || (0, import_drizzle_orm.is)(value, import_mysql_core.MySqlTable) || (0, import_drizzle_orm.is)(value, import_sqlite_core.SQLiteTable)) {
generatedTables.push(this.generateTable(value));
} else if ((0, import_drizzle_orm.is)(value, import_drizzle_orm.Relations)) {
relations.push(value);
}
}
if (this.relational) {
this.generateRelations(relations);
}
const dbml = new DBML().concatAll(generatedEnums).concatAll(generatedTables).concatAll(this.generatedRefs).build();
return dbml;
}
};
function writeDBMLFile(dbml, outPath) {
const path = (0, import_path.resolve)(process.cwd(), outPath);
try {
(0, import_fs.writeFileSync)(path, dbml, { encoding: "utf-8" });
} catch (err) {
console.error("An error ocurred while writing the generated DBML");
throw err;
}
}
// src/generators/pg.ts
var import_casing2 = require("drizzle-orm/casing");
var PgGenerator = class extends BaseGenerator {
InlineForeignKeys = PgInlineForeignKeys;
buildQueryConfig = {
escapeName: (name) => `"${name}"`,
escapeParam: (num) => `$${num + 1}`,
escapeString: (str) => `'${str.replace(/'/g, "''")}'`,
casing: new import_casing2.CasingCache()
};
isIncremental(column) {
return column.getSQLType().includes("serial");
}
generateEnum(enum_) {
const dbml = new DBML().insert("enum ").escapeSpaces(enum_.enumName).insert(" {").newLine();
for (let i = 0; i < enum_.enumValues.length; i++) {
dbml.tab().escapeSpaces(enum_.enumValues[i]).newLine();
}
dbml.insert("}");
return dbml.build();
}
};
function pgGenerate(options) {
options.relational ||= false;
const dbml = new PgGenerator(options.schema, options.relational).generate();
options.out && writeDBMLFile(dbml, options.out);
return dbml;
}
// src/generators/mysql.ts
var import_drizzle_orm2 = require("drizzle-orm");
var import_mysql_core2 = require("drizzle-orm/mysql-core");
var import_casing3 = require("drizzle-orm/casing");
var MySqlGenerator = class extends BaseGenerator {
InlineForeignKeys = MySqlInlineForeignKeys;
buildQueryConfig = {
escapeName: (name) => `\`${name}\``,
escapeParam: (_num) => "?",
escapeString: (str) => `'${str.replace(/'/g, "''")}'`,
casing: new import_casing3.CasingCache()
};
isIncremental(column) {
return column.getSQLType().includes("serial") || (0, import_drizzle_orm2.is)(column, import_mysql_core2.MySqlColumnWithAutoIncrement) && column.autoIncrement;
}
};
function mysqlGenerate(options) {
options.relational ||= false;
const dbml = new MySqlGenerator(options.schema, options.relational).generate();
options.out && writeDBMLFile(dbml, options.out);
return dbml;
}
// src/generators/sqlite.ts
var import_drizzle_orm3 = require("drizzle-orm");
var import_sqlite_core2 = require("drizzle-orm/sqlite-core");
var import_casing4 = require("drizzle-orm/casing");
var SQLiteGenerator = class extends BaseGenerator {
InlineForeignKeys = SQLiteInlineForeignKeys;
buildQueryConfig = {
escapeName: (name) => `"${name}"`,
escapeParam: (_num) => "?",
escapeString: (str) => `'${str.replace(/'/g, "''")}'`,
casing: new import_casing4.CasingCache()
};
isIncremental(column) {
return (0, import_drizzle_orm3.is)(column, import_sqlite_core2.SQLiteBaseInteger) && column.autoIncrement;
}
mapDefaultValue(value) {
let str = "";
if (typeof value === "string") {
str = `'${value}'`;
} else if (typeof value === "boolean") {
str = `${value ? 1 : 0}`;
} else if (typeof value === "number") {
str = `${value}`;
} else if (value === null) {
str = "null";
} else if ((0, import_drizzle_orm3.is)(value, import_drizzle_orm3.SQL)) {
str = `\`${value.toQuery(this.buildQueryConfig).sql}\``;
} else {
str = `\`${JSON.stringify(value)}\``;
}
return str;
}
};
function sqliteGenerate(options) {
options.relational ||= false;
const dbml = new SQLiteGenerator(options.schema, options.relational).generate();
options.out && writeDBMLFile(dbml, options.out);
return dbml;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
mysqlGenerate,
pgGenerate,
sqliteGenerate
});
;