UNPKG

@lunora/cli

Version:

The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands

162 lines (159 loc) 6.23 kB
import { quoteIdentifier, sqlAffinityForKind, frameworkColumnDdl, columnRef, physicalIndexName } from '@lunora/d1/dialect'; const validatorKindToSqlType = (kind) => sqlAffinityForKind(kind); const renderColumnDefinition = (name, column) => { const parts = [quoteIdentifier(name), column.sqlType]; if (!column.nullable) { parts.push("NOT NULL"); } return parts.join(" "); }; const renderCreateTable = (table) => { const columns = Object.entries(table.columns).map(([columnName, column]) => ` ${renderColumnDefinition(columnName, column)}`); const lines = [...frameworkColumnDdl().map((column) => ` ${column}`), ...columns].join(",\n"); return `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(table.name)} ( ${lines} );`; }; const renderDropTable = (tableName) => `DROP TABLE IF EXISTS ${quoteIdentifier(tableName)};`; const renderAddColumn = (tableName, columnName, column) => `ALTER TABLE ${quoteIdentifier(tableName)} ADD COLUMN ${renderColumnDefinition(columnName, column)};`; const renderCreateIndex = (tableName, index) => { const fields = index.fields.map((field) => columnRef(field)).join(", "); const uniqueClause = index.unique ? "UNIQUE " : ""; return `CREATE ${uniqueClause}INDEX IF NOT EXISTS ${physicalIndexName(tableName, index.name)} ON ${quoteIdentifier(tableName)} (${fields});`; }; const renderDropIndex = (tableName, indexName) => `DROP INDEX IF EXISTS ${physicalIndexName(tableName, indexName)};`; const diffExistingColumn = (tableName, columnName, old, column, unsupported) => { if (old.sqlType !== column.sqlType) { unsupported.push({ kind: "columnTypeChange", summary: `column type change on ${tableName}.${columnName}: ${old.sqlType}${column.sqlType} (write SQL manually)` }); } if (old.nullable !== column.nullable) { unsupported.push({ kind: "columnTypeChange", summary: `nullability change on ${tableName}.${columnName}: ${old.nullable ? "NULL" : "NOT NULL"}${column.nullable ? "NULL" : "NOT NULL"} (write SQL manually)` }); } }; const diffColumns = (tableName, previous, next, entries, unsupported) => { for (const [columnName, column] of Object.entries(next)) { const old = previous[columnName]; if (old === void 0) { entries.push({ kind: "addColumn", sql: renderAddColumn(tableName, columnName, column), summary: `ADD COLUMN ${tableName}.${columnName}` }); continue; } diffExistingColumn(tableName, columnName, old, column, unsupported); } for (const columnName of Object.keys(previous)) { if (next[columnName] === void 0) { unsupported.push({ kind: "dropColumn", summary: `DROP COLUMN ${tableName}.${columnName} (SQLite drop-column requires careful migration — write SQL manually)` }); } } }; const diffIndexes = (tableName, previous, next, entries, unsupported) => { for (const [indexName, index] of Object.entries(next)) { const old = previous[indexName]; if (old === void 0) { entries.push({ kind: "createIndex", sql: renderCreateIndex(tableName, index), summary: `CREATE INDEX ${indexName} ON ${tableName}` }); continue; } const fieldsEqual = old.fields.length === index.fields.length && old.fields.every((field, i) => field === index.fields[i]); if (!fieldsEqual || old.unique !== index.unique) { unsupported.push({ kind: "indexRename", summary: `index ${indexName} changed on ${tableName} — drop+create manually if intentional` }); } } for (const indexName of Object.keys(previous)) { if (next[indexName] === void 0) { entries.push({ kind: "dropIndex", sql: renderDropIndex(tableName, indexName), summary: `DROP INDEX ${indexName}` }); } } }; const diffNewTable = (tableName, table, entries) => { entries.push({ kind: "createTable", sql: renderCreateTable(table), summary: `CREATE TABLE ${tableName}` }); for (const index of Object.values(table.indexes)) { entries.push({ kind: "createIndex", sql: renderCreateIndex(tableName, index), summary: `CREATE INDEX ${index.name} ON ${tableName}` }); } }; const diffSnapshots = (previous, next) => { const previousTables = previous?.tables ?? {}; const entries = []; const unsupported = []; for (const [tableName, table] of Object.entries(next.tables)) { const old = previousTables[tableName]; if (old === void 0) { diffNewTable(tableName, table, entries); continue; } diffColumns(tableName, old.columns, table.columns, entries, unsupported); diffIndexes(tableName, old.indexes, table.indexes, entries, unsupported); } for (const [tableName] of Object.entries(previousTables)) { if (next.tables[tableName] === void 0) { entries.push({ kind: "dropTable", sql: renderDropTable(tableName), summary: `DROP TABLE ${tableName}` }); } } return { empty: entries.length === 0 && unsupported.length === 0, entries, unsupported }; }; const renderMigrationFile = (name, diff, generatedAt) => { const lines = [ `-- Lunora migration: ${name}`, `-- Generated at ${generatedAt}`, "-- This file was produced by `lunora migrate generate`. Review carefully before applying.", "" ]; for (const entry of diff.entries) { lines.push(`-- ${entry.summary}`, entry.sql, ""); } if (diff.unsupported.length > 0) { lines.push( "-- ---------------------------------------------------------------", "-- The following deltas are NOT auto-generated in v0.1.", "-- Write the appropriate SQL below by hand:", "--" ); for (const entry of diff.unsupported) { lines.push(`-- * ${entry.summary}`); } lines.push("-- ---------------------------------------------------------------", ""); } if (diff.empty) { lines.push("-- No changes detected. Re-running `lunora migrate generate` will overwrite this file.", ""); } return lines.join("\n"); }; export { diffSnapshots, renderAddColumn, renderCreateIndex, renderCreateTable, renderDropIndex, renderDropTable, renderMigrationFile, validatorKindToSqlType };