UNPKG

@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
#!/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) {