@liam-hq/cli
Version:
Command-line tool designed to generate a web application that displays ER diagrams. See https://liambx.com/docs/cli
1,367 lines (1,355 loc) • 3.51 MB
JavaScript
#!/usr/bin/env node
import { createRequire } from 'node:module';
import { Command } from 'commander';
import { parseSync } from '@swc/core';
import pkg from '@prisma/internals';
import { readFile } from 'node:fs/promises';
import { fileURLToPath, URL as URL$1 } from 'node:url';
import tty from 'node:tty';
import fs, { existsSync, mkdirSync, cpSync } from 'node:fs';
import path, { resolve, dirname, relative } from 'node:path';
import { glob } from 'glob';
import { exit } from 'node:process';
import inquirer from 'inquirer';
import { Transform, render, Text } from 'ink';
import require$$0 from 'os';
import require$$1 from 'tty';
/**
* Type definitions for Drizzle ORM schema parsing
*/
/**
* Type guard to check if a value is an object
*/
const isObject = (value) => {
return typeof value === 'object' && value !== null;
};
/**
* Safe property checker without type casting
*/
const hasProperty = (obj, key) => {
return typeof obj === 'object' && obj !== null && key in obj;
};
/**
* Safe property getter without type casting
*/
const getPropertyValue = (obj, key) => {
if (hasProperty(obj, key)) {
return obj[key];
}
return undefined;
};
/**
* Type guard for CompositePrimaryKeyDefinition
*/
const isCompositePrimaryKey = (value) => {
return (isObject(value) &&
getPropertyValue(value, 'type') === 'primaryKey' &&
hasProperty(value, 'columns') &&
Array.isArray(getPropertyValue(value, 'columns')));
};
/**
* Type guard for DrizzleIndexDefinition
*/
const isDrizzleIndex = (value) => {
return (isObject(value) &&
hasProperty(value, 'name') &&
hasProperty(value, 'columns') &&
hasProperty(value, 'unique'));
};
/**
* AST manipulation utilities for Drizzle ORM schema parsing
*/
/**
* Type guard for SWC Argument wrapper
*/
const isArgumentWrapper = (arg) => {
return isObject(arg) && hasProperty(arg, 'expression');
};
/**
* Extract expression from SWC Argument wrapper
*/
const getArgumentExpression = (arg) => {
if (isArgumentWrapper(arg)) {
return arg.expression;
}
return null;
};
/**
* Type guard for string literal expressions
*/
const isStringLiteral = (expr) => {
return (isObject(expr) &&
getPropertyValue(expr, 'type') === 'StringLiteral' &&
hasProperty(expr, 'value') &&
typeof getPropertyValue(expr, 'value') === 'string');
};
/**
* Type guard for object expressions
*/
const isObjectExpression = (expr) => {
return isObject(expr) && getPropertyValue(expr, 'type') === 'ObjectExpression';
};
/**
* Type guard for array expressions
*/
const isArrayExpression = (expr) => {
return (isObject(expr) &&
getPropertyValue(expr, 'type') === 'ArrayExpression' &&
hasProperty(expr, 'elements') &&
Array.isArray(getPropertyValue(expr, 'elements')));
};
/**
* Type guard for identifier nodes
*/
const isIdentifier = (node) => {
return (isObject(node) &&
getPropertyValue(node, 'type') === 'Identifier' &&
hasProperty(node, 'value') &&
typeof getPropertyValue(node, 'value') === 'string');
};
/**
* Check if a node is an identifier with a specific name
*/
const isIdentifierWithName = (node, name) => {
return isIdentifier(node) && node.value === name;
};
/**
* Type guard for member expressions
*/
const isMemberExpression = (node) => {
return (isObject(node) &&
getPropertyValue(node, 'type') === 'MemberExpression' &&
hasProperty(node, 'object') &&
hasProperty(node, 'property') &&
typeof getPropertyValue(node, 'object') === 'object' &&
typeof getPropertyValue(node, 'property') === 'object');
};
/**
* Check if a call expression is a pgTable call
*/
const isPgTableCall = (callExpr) => {
return isIdentifierWithName(callExpr.callee, 'pgTable');
};
/**
* Check if a call expression is a schema.table() call
*/
const isSchemaTableCall = (callExpr) => {
return (isMemberExpression(callExpr.callee) &&
isIdentifier(callExpr.callee.property) &&
callExpr.callee.property.value === 'table');
};
/**
* Extract string value from a string literal
*/
const getStringValue = (node) => {
if (node.type === 'StringLiteral') {
return node.value;
}
return null;
};
/**
* Extract identifier name
*/
const getIdentifierName = (node) => {
if (isIdentifier(node)) {
return node.value;
}
return null;
};
/**
* Parse method call chain from a call expression
*/
const parseMethodChain = (expr) => {
const methods = [];
let current = expr;
while (current.type === 'CallExpression') {
if (current.callee.type === 'MemberExpression' &&
current.callee.property.type === 'Identifier') {
methods.unshift({
name: current.callee.property.value,
args: current.arguments,
});
current = current.callee.object;
}
else {
break;
}
}
return methods;
};
/**
* Convert Drizzle column types to PostgreSQL column types
* ref: https://orm.drizzle.team/docs/column-types/pg
*/
const convertDrizzleTypeToPgType = (drizzleType, options) => {
switch (drizzleType) {
// String types with length options
case 'varchar':
if (options?.['length']) {
return `varchar(${options['length']})`;
}
return 'varchar';
case 'char':
if (options?.['length']) {
return `char(${options['length']})`;
}
return 'char';
// Numeric types with precision/scale
case 'decimal':
case 'numeric':
if (options?.['precision'] && options?.['scale']) {
return `decimal(${options['precision']},${options['scale']})`;
}
if (options?.['precision']) {
return `decimal(${options['precision']})`;
}
return 'decimal';
// Timestamp with timezone option
case 'timestamp':
if (options?.['withTimezone']) {
return 'timestamp with time zone';
}
return 'timestamp';
// Type mapping for different names
case 'doublePrecision':
return 'double precision';
case 'timestamptz':
return 'timestamp with time zone';
case 'defaultRandom':
return 'uuid';
// Default case: return type name as-is (works for most types)
default:
return drizzleType;
}
};
/**
* Convert default values from Drizzle to PostgreSQL format
*/
const convertDefaultValue = (value, _drizzleType) => {
if (value === undefined || value === null) {
return null;
}
// Handle function calls like defaultNow(), autoincrement()
if (typeof value === 'string') {
if (value === 'defaultNow' || value === 'now()') {
return 'now()';
}
if (value === 'autoincrement' || value === 'autoincrement()') {
return 'autoincrement()';
}
if (value === 'defaultRandom') {
return 'gen_random_uuid()';
}
}
// Handle primitive values
if (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean') {
return value;
}
return null;
};
/**
* Convert constraint reference options from Drizzle to PostgreSQL format
*/
const convertReferenceOption = (option) => {
switch (option.toLowerCase()) {
case 'cascade':
return 'CASCADE';
case 'restrict':
return 'RESTRICT';
case 'setnull':
case 'set null':
return 'SET_NULL';
case 'setdefault':
case 'set default':
return 'SET_DEFAULT';
default:
return 'NO_ACTION';
}
};
/**
* Data conversion logic for Drizzle ORM schema parsing
*/
/**
* Convert Drizzle table definition to internal Table format
*/
const convertToTable = (tableDef, enums = {}, variableToTableMapping = {}) => {
const columns = {};
const constraints = {};
const indexes = {};
// Convert columns
for (const [columnName, columnDef] of Object.entries(tableDef.columns)) {
// Check if this is an enum type and get the actual enum name
let columnType = columnDef.type;
// Check if this is an enum variable name (like userRoleEnum -> user_role)
for (const [enumVarName, enumDef] of Object.entries(enums)) {
if (columnDef.type === enumVarName) {
columnType = enumDef.name;
break;
}
}
// If not found, it might be a call to an enum function (like roleEnum('role'))
// In this case, the type is already the enum name from the first argument
if (columnType === columnDef.type) {
// Check if any enum definition matches this type name
for (const enumDef of Object.values(enums)) {
if (enumDef.name === columnDef.type) {
columnType = enumDef.name;
break;
}
}
}
const column = {
name: columnDef.name,
type: convertDrizzleTypeToPgType(columnType, columnDef.typeOptions),
default: convertDefaultValue(columnDef.default ||
(columnType === 'serial' ? 'autoincrement' : undefined)),
notNull: columnDef.notNull,
comment: columnDef.comment || null,
check: null,
};
columns[columnName] = column;
// Add primary key constraint
if (columnDef.primaryKey) {
const constraintName = `PRIMARY_${columnDef.name}`;
constraints[constraintName] = {
type: 'PRIMARY KEY',
name: constraintName,
columnNames: [columnDef.name],
};
// Add primary key index
const indexName = `${tableDef.name}_pkey`;
indexes[indexName] = {
name: indexName,
columns: [columnDef.name],
unique: true,
type: '',
};
}
// Add unique constraint (inline unique does not create index, only constraint)
if (columnDef.unique && !columnDef.primaryKey) {
const constraintName = `UNIQUE_${columnDef.name}`;
constraints[constraintName] = {
type: 'UNIQUE',
name: constraintName,
columnNames: [columnDef.name],
};
}
// Add foreign key constraint
if (columnDef.references) {
// Resolve variable name to actual table name
const targetTableName = variableToTableMapping[columnDef.references.table] ||
columnDef.references.table;
const constraintName = `${tableDef.name}_${columnDef.name}_${columnDef.references.table}_${columnDef.references.column}_fk`;
const constraint = {
type: 'FOREIGN KEY',
name: constraintName,
columnName: columnDef.name, // Use actual column name, not JS property name
targetTableName: targetTableName,
targetColumnName: columnDef.references.column,
updateConstraint: columnDef.references.onUpdate
? convertReferenceOption(columnDef.references.onUpdate)
: 'NO_ACTION',
deleteConstraint: columnDef.references.onDelete
? convertReferenceOption(columnDef.references.onDelete)
: 'NO_ACTION',
};
constraints[constraintName] = constraint;
}
}
// Handle composite primary key
if (tableDef.compositePrimaryKey) {
// Map JS property names to actual column names
const actualColumnNames = tableDef.compositePrimaryKey.columns
.map((jsPropertyName) => {
const columnDef = tableDef.columns[jsPropertyName];
return columnDef ? columnDef.name : jsPropertyName;
})
.filter((name) => name && name.length > 0);
// Create composite primary key constraint
const constraintName = `${tableDef.name}_pkey`;
constraints[constraintName] = {
type: 'PRIMARY KEY',
name: constraintName,
columnNames: actualColumnNames,
};
// Add composite primary key index
indexes[constraintName] = {
name: constraintName,
columns: actualColumnNames,
unique: true,
type: '',
};
}
// Convert indexes
for (const [_, indexDef] of Object.entries(tableDef.indexes)) {
// Map JS property names to actual column names
const actualColumnNames = indexDef.columns.map((jsPropertyName) => {
const columnDef = tableDef.columns[jsPropertyName];
return columnDef ? columnDef.name : jsPropertyName;
});
// Use the actual index name from the definition
const actualIndexName = indexDef.name;
indexes[actualIndexName] = {
name: actualIndexName,
columns: actualColumnNames,
unique: indexDef.unique,
type: indexDef.type || '',
};
}
return {
name: tableDef.name,
columns,
constraints,
indexes,
comment: tableDef.comment || null,
};
};
/**
* Fix foreign key constraint targetColumnName from JS property names to actual DB column names
*/
const fixForeignKeyTargetColumnNames = (tables, drizzleTables) => {
for (const table of Object.values(tables)) {
for (const constraint of Object.values(table.constraints)) {
if (constraint.type === 'FOREIGN KEY') {
// Check in drizzleTables for column mapping
const drizzleTargetTable = drizzleTables[constraint.targetTableName];
if (drizzleTargetTable) {
// Find column definition by JS property name and get actual DB column name
const targetColumnDef = drizzleTargetTable.columns[constraint.targetColumnName];
if (targetColumnDef) {
constraint.targetColumnName = targetColumnDef.name;
}
}
}
}
}
};
/**
* Convert parsed Drizzle tables to internal format with error handling
*/
const convertDrizzleTablesToInternal = (drizzleTables, enums, variableToTableMapping = {}) => {
const tables = {};
const errors = [];
// Convert Drizzle tables to internal format
for (const [tableName, tableDef] of Object.entries(drizzleTables)) {
try {
tables[tableName] = convertToTable(tableDef, enums, variableToTableMapping);
}
catch (error) {
errors.push(new Error(`Error parsing table ${tableName}: ${error instanceof Error ? error.message : String(error)}`));
}
}
// Fix foreign key constraint targetColumnName from JS property names to actual DB column names
fixForeignKeyTargetColumnNames(tables, drizzleTables);
return { tables, errors };
};
/**
* Enum definition parsing for Drizzle ORM schema parsing
*/
/**
* Parse pgEnum call expression
*/
const parsePgEnumCall = (callExpr) => {
if (callExpr.arguments.length < 2)
return null;
const enumNameArg = callExpr.arguments[0];
const valuesArg = callExpr.arguments[1];
if (!enumNameArg || !valuesArg)
return null;
// Extract expression from SWC argument structure
const enumNameExpr = getArgumentExpression(enumNameArg);
const valuesExpr = getArgumentExpression(valuesArg);
const enumName = enumNameExpr ? getStringValue(enumNameExpr) : null;
if (!enumName || !valuesExpr || !isArrayExpression(valuesExpr))
return null;
const values = [];
for (const element of valuesExpr.elements) {
if (isStringLiteral(element)) {
values.push(element.value);
}
}
return { name: enumName, values };
};
/**
* Expression parsing utilities for Drizzle ORM schema parsing
*/
/**
* Parse default value from expression
*/
const parseDefaultValue = (expr) => {
switch (expr.type) {
case 'StringLiteral':
return expr.value;
case 'NumericLiteral':
return expr.value;
case 'BooleanLiteral':
return expr.value;
case 'NullLiteral':
return null;
case 'Identifier':
// Handle special cases like defaultRandom, defaultNow
switch (expr.value) {
case 'defaultRandom':
return 'defaultRandom';
case 'defaultNow':
return 'now()';
default:
return expr.value;
}
case 'CallExpression':
// Handle function calls like defaultNow()
if (expr.callee.type === 'Identifier') {
switch (expr.callee.value) {
case 'defaultNow':
return 'now()';
case 'defaultRandom':
return 'defaultRandom';
default:
return expr.callee.value;
}
}
return undefined;
default:
return undefined;
}
};
/**
* Parse object expression to plain object
*/
const parseObjectExpression = (obj) => {
const result = {};
for (const prop of obj.properties) {
if (prop.type === 'KeyValueProperty') {
const key = prop.key.type === 'Identifier'
? getIdentifierName(prop.key)
: prop.key.type === 'StringLiteral'
? getStringValue(prop.key)
: null;
if (key) {
result[key] = parsePropertyValue(prop.value);
}
}
}
return result;
};
/**
* Type guard for expression-like objects
*/
const isExpressionLike = (value) => {
return (isObject(value) &&
hasProperty(value, 'type') &&
typeof getPropertyValue(value, 'type') === 'string');
};
/**
* Safe parser for unknown values as expressions
*/
const parseUnknownValue = (value) => {
if (isExpressionLike(value)) {
return parseDefaultValue(value);
}
return value;
};
/**
* Parse property value (including arrays)
*/
const parsePropertyValue = (expr) => {
if (isArrayExpression(expr)) {
const result = [];
for (const element of expr.elements) {
const elementExpr = getArgumentExpression(element);
if (elementExpr &&
elementExpr.type === 'MemberExpression' &&
elementExpr.object.type === 'Identifier' &&
elementExpr.property.type === 'Identifier') {
// For table.columnName references, use the property name
result.push(elementExpr.property.value);
}
else if (isMemberExpression(element) &&
isIdentifier(element.object) &&
isIdentifier(element.property)) {
// Direct MemberExpression (not wrapped in { expression })
result.push(element.property.value);
}
else {
const parsed = elementExpr
? parseDefaultValue(elementExpr)
: parseUnknownValue(element);
result.push(parsed);
}
}
return result;
}
return parseUnknownValue(expr);
};
/**
* Column definition parsing for Drizzle ORM schema parsing
*/
/**
* Parse column definition from object property
*/
const parseColumnFromProperty = (prop) => {
if (prop.type !== 'KeyValueProperty')
return null;
const columnName = prop.key.type === 'Identifier' ? getIdentifierName(prop.key) : null;
if (!columnName)
return null;
if (prop.value.type !== 'CallExpression')
return null;
// Parse the method chain to find the base type
const methods = parseMethodChain(prop.value);
// Find the base type from the root of the chain
let baseType = null;
let current = prop.value;
// Traverse to the bottom of the method chain to find the base type call
while (current.type === 'CallExpression' &&
current.callee.type === 'MemberExpression') {
current = current.callee.object;
}
if (current.type === 'CallExpression' &&
current.callee.type === 'Identifier') {
baseType = current.callee.value;
}
if (!baseType)
return null;
// Extract the actual column name from the first argument of the base type call
let actualColumnName = columnName; // Default to JS property name
if (current.type === 'CallExpression' && current.arguments.length > 0) {
const firstArg = current.arguments[0];
const firstArgExpr = getArgumentExpression(firstArg);
if (firstArgExpr && isStringLiteral(firstArgExpr)) {
actualColumnName = firstArgExpr.value;
}
}
const column = {
name: actualColumnName,
type: baseType,
notNull: false,
primaryKey: false,
unique: false,
};
// Parse type options from second argument (like { length: 255 })
if (current.type === 'CallExpression' && current.arguments.length > 1) {
const secondArg = current.arguments[1];
const secondArgExpr = getArgumentExpression(secondArg);
if (secondArgExpr && isObjectExpression(secondArgExpr)) {
column.typeOptions = parseObjectExpression(secondArgExpr);
}
}
// Parse method calls in the chain (already parsed above)
for (const method of methods) {
switch (method.name) {
case 'primaryKey':
column.primaryKey = true;
column.notNull = true;
break;
case 'notNull':
column.notNull = true;
break;
case 'unique':
column.unique = true;
break;
case 'default':
if (method.args.length > 0) {
const argExpr = getArgumentExpression(method.args[0]);
if (argExpr) {
column.default = parseDefaultValue(argExpr);
}
}
break;
case 'defaultNow':
column.default = 'now()';
break;
case 'references':
if (method.args.length > 0) {
const argExpr = getArgumentExpression(method.args[0]);
// Parse references: () => table.column
if (argExpr && argExpr.type === 'ArrowFunctionExpression') {
const body = argExpr.body;
if (body.type === 'MemberExpression' &&
body.object.type === 'Identifier' &&
body.property.type === 'Identifier') {
const referencesOptions = {
table: body.object.value,
column: body.property.value,
};
// Parse the second argument for onDelete/onUpdate options
if (method.args.length > 1) {
const optionsExpr = getArgumentExpression(method.args[1]);
if (optionsExpr && isObjectExpression(optionsExpr)) {
const options = parseObjectExpression(optionsExpr);
if (typeof options['onDelete'] === 'string') {
referencesOptions.onDelete = options['onDelete'];
}
if (typeof options['onUpdate'] === 'string') {
referencesOptions.onUpdate = options['onUpdate'];
}
}
}
column.references = referencesOptions;
}
}
}
break;
case '$comment':
if (method.args.length > 0) {
const argExpr = getArgumentExpression(method.args[0]);
const commentValue = argExpr ? getStringValue(argExpr) : null;
if (commentValue) {
column.comment = commentValue;
}
}
break;
}
}
// Handle serial types default
if (baseType === 'serial' && column.primaryKey) {
column.default = 'autoincrement';
}
return column;
};
/**
* Table structure parsing for Drizzle ORM schema parsing
*/
/**
* Parse pgTable call with comment method chain
*/
const parsePgTableWithComment = (commentCallExpr) => {
// Extract the comment from the call arguments
let comment = null;
if (commentCallExpr.arguments.length > 0) {
const commentArg = commentCallExpr.arguments[0];
const commentExpr = getArgumentExpression(commentArg);
if (commentExpr && isStringLiteral(commentExpr)) {
comment = commentExpr.value;
}
}
// Get the pgTable call from the object of the member expression
if (commentCallExpr.callee.type === 'MemberExpression') {
const pgTableCall = commentCallExpr.callee.object;
if (pgTableCall.type === 'CallExpression' && isPgTableCall(pgTableCall)) {
const table = parsePgTableCall(pgTableCall);
if (table && comment) {
table.comment = comment;
}
return table;
}
}
return null;
};
/**
* Parse pgTable call expression
*/
const parsePgTableCall = (callExpr) => {
if (callExpr.arguments.length < 2)
return null;
const tableNameArg = callExpr.arguments[0];
const columnsArg = callExpr.arguments[1];
if (!tableNameArg || !columnsArg)
return null;
// Extract expression from SWC argument structure
const tableNameExpr = getArgumentExpression(tableNameArg);
const columnsExpr = getArgumentExpression(columnsArg);
const tableName = tableNameExpr ? getStringValue(tableNameExpr) : null;
if (!tableName || !columnsExpr || !isObjectExpression(columnsExpr))
return null;
const table = {
name: tableName,
columns: {},
indexes: {},
};
// Parse columns from the object expression
for (const prop of columnsExpr.properties) {
if (prop.type === 'KeyValueProperty') {
const column = parseColumnFromProperty(prop);
if (column) {
// Use the JS property name as the key
const jsPropertyName = prop.key.type === 'Identifier' ? getIdentifierName(prop.key) : null;
if (jsPropertyName) {
table.columns[jsPropertyName] = column;
}
}
}
}
// Parse indexes and composite primary key from third argument if present
if (callExpr.arguments.length > 2) {
const thirdArg = callExpr.arguments[2];
const thirdArgExpr = getArgumentExpression(thirdArg);
if (thirdArgExpr && thirdArgExpr.type === 'ArrowFunctionExpression') {
// Parse arrow function like (table) => ({ nameIdx: index(...), pk: primaryKey(...) })
let returnExpr = thirdArgExpr.body;
// Handle parenthesized expressions like (table) => ({ ... })
if (returnExpr.type === 'ParenthesisExpression') {
returnExpr = returnExpr.expression;
}
if (returnExpr.type === 'ObjectExpression') {
for (const prop of returnExpr.properties) {
if (prop.type === 'KeyValueProperty') {
const indexName = prop.key.type === 'Identifier'
? getIdentifierName(prop.key)
: null;
if (indexName && prop.value.type === 'CallExpression') {
const indexDef = parseIndexDefinition(prop.value, indexName);
if (indexDef) {
if (isCompositePrimaryKey(indexDef)) {
table.compositePrimaryKey = indexDef;
}
else if (isDrizzleIndex(indexDef)) {
table.indexes[indexName] = indexDef;
}
}
}
}
}
}
}
}
return table;
};
/**
* Parse schema.table() call expression
*/
const parseSchemaTableCall = (callExpr) => {
if (!isSchemaTableCall(callExpr) || callExpr.arguments.length < 2)
return null;
// Extract expression from SWC argument structure
const tableNameExpr = getArgumentExpression(callExpr.arguments[0]);
const columnsExpr = getArgumentExpression(callExpr.arguments[1]);
const tableName = tableNameExpr ? getStringValue(tableNameExpr) : null;
if (!tableName || !columnsExpr || !isObjectExpression(columnsExpr))
return null;
const table = {
name: tableName,
columns: {},
indexes: {},
};
// TODO: Handle table name conflicts across different schemas
// Currently, if multiple schemas have tables with the same name (e.g., auth.users and public.users),
// the later one will overwrite the earlier one since we only use the table name without schema prefix.
// This is a limitation shared by other parsers and should be addressed consistently across the codebase.
// ref: https://github.com/liam-hq/liam/discussions/2391
// Parse columns from the object expression
for (const prop of columnsExpr.properties) {
if (prop.type === 'KeyValueProperty') {
const column = parseColumnFromProperty(prop);
if (column) {
// Use the JS property name as the key
const jsPropertyName = prop.key.type === 'Identifier' ? getIdentifierName(prop.key) : null;
if (jsPropertyName) {
table.columns[jsPropertyName] = column;
}
}
}
}
// Parse indexes and composite primary key from third argument if present
if (callExpr.arguments.length > 2) {
const thirdArg = callExpr.arguments[2];
const thirdArgExpr = getArgumentExpression(thirdArg);
if (thirdArgExpr && thirdArgExpr.type === 'ArrowFunctionExpression') {
// Parse arrow function like (table) => ({ nameIdx: index(...), pk: primaryKey(...) })
let returnExpr = thirdArgExpr.body;
// Handle parenthesized expressions like (table) => ({ ... })
if (returnExpr.type === 'ParenthesisExpression') {
returnExpr = returnExpr.expression;
}
if (returnExpr.type === 'ObjectExpression') {
for (const prop of returnExpr.properties) {
if (prop.type === 'KeyValueProperty') {
const indexName = prop.key.type === 'Identifier'
? getIdentifierName(prop.key)
: null;
if (indexName && prop.value.type === 'CallExpression') {
const indexDef = parseIndexDefinition(prop.value, indexName);
if (indexDef) {
if (isCompositePrimaryKey(indexDef)) {
table.compositePrimaryKey = indexDef;
}
else if (isDrizzleIndex(indexDef)) {
table.indexes[indexName] = indexDef;
}
}
}
}
}
}
}
}
return table;
};
/**
* Parse index or primary key definition
*/
const parseIndexDefinition = (callExpr, name) => {
// Handle primaryKey({ columns: [...] })
if (callExpr.callee.type === 'Identifier' &&
callExpr.callee.value === 'primaryKey') {
if (callExpr.arguments.length > 0) {
const configArg = callExpr.arguments[0];
const configExpr = getArgumentExpression(configArg);
if (configExpr && isObjectExpression(configExpr)) {
const config = parseObjectExpression(configExpr);
if (config['columns'] && Array.isArray(config['columns'])) {
const columns = config['columns'].filter((col) => typeof col === 'string');
return {
type: 'primaryKey',
columns,
};
}
}
}
return null;
}
// Handle index('name').on(...) or uniqueIndex('name').on(...) with optional .using(...)
let isUnique = false;
let indexName = name;
let indexType = ''; // Index type (btree, gin, gist, etc.)
let currentExpr = callExpr;
// Traverse the method chain to find index(), on(), and using() calls
const methodCalls = [];
while (currentExpr.type === 'CallExpression' &&
currentExpr.callee.type === 'MemberExpression' &&
currentExpr.callee.property.type === 'Identifier') {
const methodName = currentExpr.callee.property.value;
methodCalls.unshift({ method: methodName, expr: currentExpr });
currentExpr = currentExpr.callee.object;
}
// The base should be index() or uniqueIndex()
if (currentExpr.type === 'CallExpression' &&
currentExpr.callee.type === 'Identifier') {
const baseMethod = currentExpr.callee.value;
if (baseMethod === 'index' || baseMethod === 'uniqueIndex') {
isUnique = baseMethod === 'uniqueIndex';
// Get the index name from the first argument
if (currentExpr.arguments.length > 0) {
const nameArg = currentExpr.arguments[0];
const nameExpr = getArgumentExpression(nameArg);
if (nameExpr && isStringLiteral(nameExpr)) {
indexName = nameExpr.value;
}
}
}
}
// Parse method chain to extract columns and index type
const columns = [];
for (const { method, expr } of methodCalls) {
if (method === 'on') {
// Parse column references from .on(...) arguments
for (const arg of expr.arguments) {
const argExpr = getArgumentExpression(arg);
if (argExpr &&
argExpr.type === 'MemberExpression' &&
argExpr.object.type === 'Identifier' &&
argExpr.property.type === 'Identifier') {
columns.push(argExpr.property.value);
}
}
}
else if (method === 'using') {
// Parse index type from .using('type', ...) - first argument is the type
if (expr.arguments.length > 0) {
const typeArg = expr.arguments[0];
const typeExpr = getArgumentExpression(typeArg);
if (typeExpr && isStringLiteral(typeExpr)) {
indexType = typeExpr.value;
}
}
// Also parse columns from remaining arguments if present
for (let i = 1; i < expr.arguments.length; i++) {
const arg = expr.arguments[i];
const argExpr = getArgumentExpression(arg);
if (argExpr &&
argExpr.type === 'MemberExpression' &&
argExpr.object.type === 'Identifier' &&
argExpr.property.type === 'Identifier') {
columns.push(argExpr.property.value);
}
}
}
}
if (columns.length > 0) {
return {
name: indexName,
columns,
unique: isUnique,
type: indexType,
};
}
return null;
};
/**
* Main orchestrator for Drizzle ORM schema parsing
*/
/**
* Parse Drizzle TypeScript schema to extract table definitions using SWC AST
*/
const parseDrizzleSchema = (sourceCode) => {
// Parse TypeScript code into AST
const ast = parseSync(sourceCode, {
syntax: 'typescript',
target: 'es2022',
});
const tables = {};
const enums = {};
const variableToTableMapping = {};
// Traverse the AST to find pgTable calls
visitModule(ast, tables, enums, variableToTableMapping);
return { tables, enums, variableToTableMapping };
};
/**
* Visit and traverse the module AST
*/
const visitModule = (module, tables, enums, variableToTableMapping) => {
for (const item of module.body) {
if (item.type === 'VariableDeclaration') {
for (const declarator of item.declarations) {
visitVariableDeclarator(declarator, tables, enums, variableToTableMapping);
}
}
else if (item.type === 'ExportDeclaration' &&
item.declaration?.type === 'VariableDeclaration') {
for (const declarator of item.declaration.declarations) {
visitVariableDeclarator(declarator, tables, enums, variableToTableMapping);
}
}
}
};
/**
* Visit variable declarator to find pgTable, pgEnum, or relations calls
*/
const visitVariableDeclarator = (declarator, tables, enums, variableToTableMapping) => {
if (!declarator.init || declarator.init.type !== 'CallExpression')
return;
const callExpr = declarator.init;
if (isPgTableCall(callExpr)) {
const table = parsePgTableCall(callExpr);
if (table && declarator.id.type === 'Identifier') {
tables[table.name] = table;
// Map variable name to table name
variableToTableMapping[declarator.id.value] = table.name;
}
}
else if (isSchemaTableCall(callExpr)) {
const table = parseSchemaTableCall(callExpr);
if (table && declarator.id.type === 'Identifier') {
tables[table.name] = table;
// Map variable name to table name
variableToTableMapping[declarator.id.value] = table.name;
}
}
else if (declarator.init.type === 'CallExpression' &&
declarator.init.callee.type === 'MemberExpression' &&
declarator.init.callee.property.type === 'Identifier' &&
declarator.init.callee.property.value === '$comment') {
// Handle table comments: pgTable(...).comment(...)
const table = parsePgTableWithComment(declarator.init);
if (table && declarator.id.type === 'Identifier') {
tables[table.name] = table;
// Map variable name to table name
variableToTableMapping[declarator.id.value] = table.name;
}
}
else if (callExpr.callee.type === 'Identifier' &&
callExpr.callee.value === 'pgEnum') {
const enumDef = parsePgEnumCall(callExpr);
if (enumDef && declarator.id.type === 'Identifier') {
enums[declarator.id.value] = enumDef;
}
}
};
/**
* Main processor function for Drizzle schemas
*/
const parseDrizzleSchemaString = (schemaString) => {
try {
const { tables: drizzleTables, enums, variableToTableMapping, } = parseDrizzleSchema(schemaString);
const { tables, errors } = convertDrizzleTablesToInternal(drizzleTables, enums, variableToTableMapping);
return Promise.resolve({
value: { tables },
errors,
});
}
catch (error) {
return Promise.resolve({
value: { tables: {} },
errors: [
new Error(`Error parsing Drizzle schema: ${error instanceof Error ? error.message : String(error)}`),
],
});
}
};
const processor$4 = (str) => parseDrizzleSchemaString(str);
// Helper function to handle autoincrement types
function getAutoincrementType(typeName) {
switch (typeName) {
case 'Int':
return 'serial';
case 'SmallInt':
return 'smallserial';
case 'BigInt':
return 'bigserial';
default:
return typeName.toLowerCase();
}
}
// Helper function to handle native types
function handleNativeType(nativeTypeName, nativeTypeArgs, defaultValue) {
// Check for autoincrement
if (typeof defaultValue === 'string' &&
defaultValue.includes('autoincrement()')) {
return getAutoincrementType(nativeTypeName);
}
// Handle type with arguments
if (nativeTypeArgs.length > 0) {
return `${nativeTypeName.toLowerCase()}(${nativeTypeArgs.join(',')})`;
}
// Special case for DoublePrecision
if (nativeTypeName === 'DoublePrecision') {
return 'double precision';
}
return nativeTypeName.toLowerCase();
}
// Helper function to map Prisma types to PostgreSQL types
function mapPrismaTypeToPostgres(type) {
switch (type) {
case 'String':
return 'text';
case 'Boolean':
return 'boolean';
case 'Int':
return 'integer';
case 'BigInt':
return 'bigint';
case 'Float':
return 'double precision';
case 'DateTime':
return 'timestamp(3)';
case 'Json':
return 'jsonb';
case 'Decimal':
return 'decimal(65,30)';
case 'Bytes':
return 'bytea';
default:
return type;
}
}
// ref: https://www.prisma.io/docs/orm/reference/prisma-schema-reference#model-field-scalar-types
function convertToPostgresColumnType(type, nativeType, defaultValue) {
// If native type is provided, use it
if (nativeType) {
const [nativeTypeName, nativeTypeArgs] = nativeType;
return handleNativeType(nativeTypeName, nativeTypeArgs, defaultValue);
}
// Handle autoincrement without native type
if (typeof defaultValue === 'string' &&
defaultValue.includes('autoincrement()')) {
return getAutoincrementType(type);
}
// Special case for uuid default value
if (typeof defaultValue === 'string' && defaultValue.includes('uuid')) {
return 'uuid';
}
// Map Prisma type to PostgreSQL type
return mapPrismaTypeToPostgres(type);
}
// NOTE: Workaround for CommonJS module import issue with @prisma/internals
// CommonJS module can not support all module.exports as named exports
const { getDMMF } = pkg;
const getFieldRenamedIndex = (index, tableFieldsRenaming) => {
const fieldsRenaming = tableFieldsRenaming[index.model];
if (!fieldsRenaming)
return index;
const newFields = index.fields.map((field) => ({
...field,
name: fieldsRenaming[field.name] ?? field.name,
}));
return { ...index, fields: newFields };
};
/**
* Build a mapping of field renamings from model fields
*/
function buildFieldRenamingMap(models) {
const tableFieldRenaming = {};
for (const model of models) {
for (const field of model.fields) {
if (field.dbName) {
const tableName = model.dbName || model.name;
const fieldConversions = tableFieldRenaming[tableName] ?? {};
fieldConversions[field.name] = field.dbName;
tableFieldRenaming[tableName] = fieldConversions;
}
}
}
return tableFieldRenaming;
}
function processModelField(field, model, tableFieldRenaming) {
if (field.relationName)
return { column: null, constraint: null };
const defaultValue = extractDefaultValue$2(field);
const fieldName = tableFieldRenaming[model.dbName || model.name]?.[field.name] ?? field.name;
const column = {
name: fieldName,
type: convertToPostgresColumnType(field.type, field.nativeType, defaultValue),
default: defaultValue,
notNull: field.isRequired,
comment: field.documentation ?? null,
check: null,
};
let constraint = null;
if (field.isId) {
const constraintName = `PRIMARY_${fieldName}`;
constraint = {
type: 'PRIMARY KEY',
name: constraintName,
columnNames: [fieldName],
};
}
else if (field.isUnique) {
// to avoid duplicate with PRIMARY KEY constraint, it doesn't create constraint object with `field.isId`
const constraintName = `UNIQUE_${fieldName}`;
constraint = {
type: 'UNIQUE',
name: constraintName,
columnNames: [fieldName],
};
}
return {
column: [fieldName, column],
constraint: constraint ? [constraint.name, constraint] : null,
};
}
/**
* Process a model and create a table
*/
function processModel(model, tableFieldRenaming) {
const columns = {};
const constraints = {};
for (const field of model.fields) {
const { column, constraint } = processModelField(field, model, tableFieldRenaming);
if (column) {
columns[column[0]] = column[1];
}
if (constraint) {
constraints[constraint[0]] = constraint[1];
}
}
return {
name: model.dbName || model.name,
columns,
comment: model.documentation ?? null,
indexes: {},
constraints,
};
}
/**
* Process all models and create tables
*/
function processTables(models, tableFieldRenaming) {
const tables = {};
for (const model of models) {
tables[model.dbName || model.name] = processModel(model, tableFieldRenaming);
}
return tables;
}
/**
* Get Primary Table Name
*/
function getPrimaryTableNameByType(fieldType, models) {
return models.find((model) => model.name === fieldType)?.dbName ?? fieldType;
}
/**
* Process a relationship field and create foreign key constraint
*/
function processRelationshipField(field, model, models, tableFieldRenaming) {
if (!field.relationName)
return null;
const isTargetField = field.relationToFields?.[0] &&
(field.relationToFields?.length ?? 0) > 0 &&
field.relationFromFields?.[0] &&
(field.relationFromFields?.length ?? 0) > 0;
if (!isTargetField)
return null;
// Get the primary table name
const primaryTableName = getPrimaryTableNameByType(field.type, models);
// Get the column names
const primaryColumnName = field.relationToFields?.[0] ?? '';
const foreignColumnName = field.relationFromFields?.[0] ?? '';
// Apply field renaming
const foreignTableName = model.dbName || model.name;
const mappedPrimaryColumnName = tableFieldRenaming[primaryTableName]?.[primaryColumnName] ||
primaryColumnName;
const mappedForeignColumnName = tableFieldRenaming[foreignTableName]?.[foreignColumnName] ||
foreignColumnName;
const constraint = {
type: 'FOREIGN KEY',
name: field.relationName,
columnName: mappedForeignColumnName,
targetTableName: primaryTableName,
targetColumnName: mappedPrimaryColumnName,
updateConstraint: 'NO_ACTION',
deleteConstraint: normalizeConstraintName$2(field.relationOnDelete ?? ''),
};
return constraint;
}
/**
* Process a single model's foreign key constraints
*/
function processModelConstraints(model, models, tables, tableFieldRenaming, processedManyToManyRelations, manyToManyRelations) {
for (const field of model.fields) {
if (!field.relationName)
continue;
// Skip many-to-many relations as they're handled separately
if (detectAndStoreManyToManyRelation(field, model, models, processedManyToManyRelations, manyToManyRelations)) {
continue;
}
// Process foreign key constraint
const constraint = processRelationshipField(field, model, models, tableFieldRenaming);
// Add constraint to table
if (constraint) {
const tableName = model.dbName || model.name;
const table = tables[tableName];
if (table) {
table.constraints[constraint.name] = constraint;
}
}
}
}
/**
* Process constraints for all models
*/
function processConstraints$1(models, tables, tableFieldRenaming, processedManyToManyRelations, manyToManyRelations) {
// Process each model's constraints
for (const model of models) {
processModelConstraints(model, models, tables, tableFieldRenaming, processedManyToManyRelations, manyToManyRelations);
}
}
/**
* Process indexes for all models
*/
function processIndexes$1(indexes, models, tables, tableFieldRenaming) {
const updatedIndexes = indexes.map((index) => {
const model = models.find((m) => m.name === index.model);
return model
? {
model: model.dbName ?? model.name,
type: index.type,
isDefinedOnField: index.isDefinedOnField,
fields: index.fields,
}
: index;
});
for (const index of updatedIndexes) {
const table = tables[index.model];
if (!table)
continue;
const indexInfo = extractIndex(getFieldRenamedIndex(index, tableFieldRenaming));
if (!indexInfo)
continue;
table.indexes[indexInfo.name] = indexInfo;
}
}
/**
* Process many-to-many relationships and create join tables with constraints
*/
function processManyToManyRelationships(manyToManyRelations, tables, models) {