UNPKG

iagate-querykit

Version:

QueryKit: lightweight TypeScript query toolkit with models, views, triggers, events, scheduler and adapters (better-sqlite3).

629 lines (628 loc) 22.5 kB
/** * Tipos de colunas suportados pelo sistema de migração. * Cada tipo é mapeado para o tipo SQL apropriado baseado no dialeto do banco. * * @example * ```typescript * // Dados iniciais * const columnType = ColumnType.String; * * // Como usar * // Passado para definições de coluna em migrações * * // Output: Tipo de coluna configurado para string * ``` */ export var ColumnType; (function (ColumnType) { ColumnType["Int"] = "Int"; ColumnType["BigInt"] = "BigInt"; ColumnType["Float"] = "Float"; ColumnType["Double"] = "Double"; ColumnType["Decimal"] = "Decimal"; ColumnType["String"] = "String"; ColumnType["Text"] = "Text"; ColumnType["Varchar"] = "Varchar"; ColumnType["Date"] = "Date"; ColumnType["Time"] = "Time"; ColumnType["DateTime"] = "DateTime"; ColumnType["Timestamp"] = "Timestamp"; ColumnType["TimestampTz"] = "TimestampTz"; ColumnType["Boolean"] = "Boolean"; ColumnType["Json"] = "Json"; ColumnType["Uuid"] = "Uuid"; ColumnType["Binary"] = "Binary"; })(ColumnType || (ColumnType = {})); /** * Valores padrão especiais para colunas. * Fornece valores padrão comuns como timestamp atual e UUID v4. * * @example * ```typescript * // Dados iniciais * const defaultValue = ColumnDefault.CurrentTimestamp; * * // Como usar * // Passado para opções de coluna em migrações * * // Output: Valor padrão configurado para timestamp atual * ``` */ export var ColumnDefault; (function (ColumnDefault) { ColumnDefault["CurrentTimestamp"] = "CurrentTimestamp"; ColumnDefault["UuidV4"] = "UuidV4"; })(ColumnDefault || (ColumnDefault = {})); /** * Construtor de migrações com DSL fluente. * Permite definir operações de migração de forma declarativa e legível. * Suporta múltiplos dialetos SQL com mapeamento automático de tipos. * * @example * ```typescript * // Dados iniciais * const migrationBuilder = new MigrationBuilder(); * * // Como usar * migrationBuilder * .createTable('users', { * id: { type: ColumnType.Int, primaryKey: true, autoIncrement: true }, * name: { type: ColumnType.String, length: 255, notNull: true }, * email: { type: ColumnType.String, length: 255, unique: true } * }) * .createIndex('users', ['email'], { unique: true }); * * // Output: Builder configurado com operações de migração * ``` */ export class MigrationBuilder { steps = []; /** * Mapeia tipos de coluna para tipos SQL específicos do dialeto. * * @param dialect - Dialeto SQL do banco * @param t - Tipo de coluna do enum ColumnType * @param len - Comprimento opcional para strings * @param prec - Precisão opcional para decimais * @param scale - Escala opcional para decimais * @returns String SQL do tipo de coluna */ typeFor(dialect, t, len, prec, scale) { switch (t) { case ColumnType.Int: switch (dialect) { case 'mysql': return 'INT'; case 'postgres': return 'INTEGER'; case 'mssql': return 'INT'; case 'oracle': return 'NUMBER'; default: return 'INTEGER'; } case ColumnType.BigInt: switch (dialect) { case 'mysql': return 'BIGINT'; case 'postgres': return 'BIGINT'; case 'mssql': return 'BIGINT'; case 'oracle': return 'NUMBER(19)'; default: return 'BIGINT'; } case ColumnType.Float: switch (dialect) { case 'mysql': return 'FLOAT'; case 'postgres': return 'REAL'; case 'mssql': return 'FLOAT'; case 'oracle': return 'BINARY_FLOAT'; default: return 'REAL'; } case ColumnType.Double: switch (dialect) { case 'mysql': return 'DOUBLE'; case 'postgres': return 'DOUBLE PRECISION'; case 'mssql': return 'FLOAT(53)'; case 'oracle': return 'BINARY_DOUBLE'; default: return 'DOUBLE'; } case ColumnType.Decimal: { const p = prec || 10; const s = scale || 2; switch (dialect) { case 'mysql': return `DECIMAL(${p},${s})`; case 'postgres': return `DECIMAL(${p},${s})`; case 'mssql': return `DECIMAL(${p},${s})`; case 'oracle': return `NUMBER(${p},${s})`; default: return `NUMERIC(${p},${s})`; } } case ColumnType.String: case ColumnType.Varchar: { const L = len || 255; switch (dialect) { case 'mysql': return `VARCHAR(${L})`; case 'postgres': return `VARCHAR(${L})`; case 'mssql': return `NVARCHAR(${L})`; case 'oracle': return `VARCHAR2(${L})`; default: return `VARCHAR(${L})`; } } case ColumnType.Text: switch (dialect) { case 'mysql': return 'TEXT'; case 'postgres': return 'TEXT'; case 'mssql': return 'NVARCHAR(MAX)'; case 'oracle': return 'CLOB'; default: return 'TEXT'; } case ColumnType.Date: switch (dialect) { case 'mysql': return 'DATE'; case 'postgres': return 'DATE'; case 'mssql': return 'DATE'; case 'oracle': return 'DATE'; default: return 'DATE'; } case ColumnType.Time: switch (dialect) { case 'mysql': return 'TIME'; case 'postgres': return 'TIME'; case 'mssql': return 'TIME'; case 'oracle': return 'VARCHAR2(20)'; default: return 'TEXT'; } case ColumnType.DateTime: case ColumnType.Timestamp: switch (dialect) { case 'mysql': return 'DATETIME'; case 'postgres': return 'TIMESTAMP'; case 'mssql': return 'DATETIME2'; case 'oracle': return 'TIMESTAMP'; default: return 'DATETIME'; } case ColumnType.TimestampTz: switch (dialect) { case 'mysql': return 'TIMESTAMP'; case 'postgres': return 'TIMESTAMPTZ'; case 'mssql': return 'DATETIMEOFFSET'; case 'oracle': return 'TIMESTAMP WITH TIME ZONE'; default: return 'DATETIME'; } case ColumnType.Boolean: switch (dialect) { case 'mysql': return 'TINYINT(1)'; case 'postgres': return 'BOOLEAN'; case 'mssql': return 'BIT'; case 'oracle': return 'NUMBER(1)'; default: return 'INTEGER'; } case ColumnType.Json: switch (dialect) { case 'mysql': return 'JSON'; case 'postgres': return 'JSONB'; case 'mssql': return 'NVARCHAR(MAX)'; case 'oracle': return 'CLOB'; default: return 'TEXT'; } case ColumnType.Uuid: switch (dialect) { case 'postgres': return 'UUID'; case 'mssql': return 'UNIQUEIDENTIFIER'; case 'oracle': return 'VARCHAR2(36)'; default: return 'CHAR(36)'; } case ColumnType.Binary: switch (dialect) { case 'mysql': return 'BLOB'; case 'postgres': return 'BYTEA'; case 'mssql': return 'VARBINARY(MAX)'; case 'oracle': return 'BLOB'; default: return 'BLOB'; } default: return 'TEXT'; } } /** * Mapeia valores padrão para SQL específico do dialeto. * * @param dialect - Dialeto SQL do banco * @param t - Tipo de coluna * @param v - Valor padrão para mapear * @returns String SQL do valor padrão */ defaultFor(dialect, t, v) { const wantsCurrentTs = v === ColumnDefault.CurrentTimestamp || (typeof v === 'string' && v.toUpperCase() === 'CURRENT_TIMESTAMP'); if (wantsCurrentTs) { switch (dialect) { case 'mssql': return 'GETDATE()'; case 'oracle': return 'CURRENT_TIMESTAMP'; default: return 'CURRENT_TIMESTAMP'; } } if (v === ColumnDefault.UuidV4) { switch (dialect) { case 'mysql': return 'UUID()'; case 'postgres': return 'gen_random_uuid()'; case 'mssql': return 'NEWID()'; case 'oracle': return 'LOWER(RAWTOHEX(SYS_GUID()))'; default: return 'NULL'; } } if (typeof v === 'string') return `'${v.replace(/'/g, "''")}'`; if (v === null) return 'NULL'; if (typeof v === 'boolean') return v ? '1' : '0'; return String(v); } /** * Constrói definição completa de coluna em SQL. * * @param dialect - Dialeto SQL do banco * @param name - Nome da coluna * @param type - Tipo da coluna * @param opts - Opções da coluna * @returns String SQL da definição da coluna */ colDef(dialect, name, type, opts = {}) { const parts = [name, this.typeFor(dialect, type, opts.length, opts.precision, opts.scale)]; if (opts.primaryKey) parts.push('PRIMARY KEY'); if (opts.notNull) parts.push('NOT NULL'); if (opts.unique) parts.push('UNIQUE'); // auto-increment / identity if (opts.autoIncrement) { const ai = typeof opts.autoIncrement === 'object' ? opts.autoIncrement : {}; const mode = ai.mode; const start = ai.start ?? 1; const step = ai.increment ?? 1; switch (dialect) { case 'mysql': parts.push('AUTO_INCREMENT'); break; case 'postgres': { if (mode === 'serial') { if (type === ColumnType.BigInt) parts[1] = 'BIGSERIAL'; else parts[1] = 'SERIAL'; } else { const m = mode === 'always' ? 'ALWAYS' : 'BY DEFAULT'; parts.push(`GENERATED ${m} AS IDENTITY`); } break; } case 'mssql': parts.push(`IDENTITY(${start},${step})`); break; case 'oracle': { const m = mode === 'always' ? 'ALWAYS' : 'BY DEFAULT'; parts.push(`GENERATED ${m} AS IDENTITY`); break; } default: { // sqlite const alreadyPk = parts.some(p => /PRIMARY KEY/i.test(p)); const baseType = parts[1] || ''; if (!alreadyPk) parts.push('PRIMARY KEY'); if (/^INTEGER\b/i.test(baseType)) parts.push('AUTOINCREMENT'); break; } } } if (opts.default !== undefined) { const dv = this.defaultFor(dialect, type, opts.default); parts.push('DEFAULT ' + dv); } if (opts.references) { const refCol = opts.references.column || 'id'; let ref = `REFERENCES ${opts.references.table} (${refCol})`; if (opts.references.onDelete) ref += ` ON DELETE ${opts.references.onDelete}`; if (opts.references.onUpdate) ref += ` ON UPDATE ${opts.references.onUpdate}`; parts.push(ref); } return parts.join(' '); } /** * Adiciona operação para criar tabela. * * @param name - Nome da tabela * @param columns - Definições das colunas * @returns Instância do builder para method chaining * * @example * ```typescript * // Dados iniciais * const migrationBuilder = new MigrationBuilder(); * * // Como usar * migrationBuilder.createTable('users', { * id: { type: ColumnType.Int, primaryKey: true, autoIncrement: true }, * name: { type: ColumnType.String, length: 255, notNull: true }, * email: { type: ColumnType.String, length: 255, unique: true } * }); * * // Output: Operação de criação de tabela adicionada ao builder * ``` */ createTable(name, columns) { this.steps.push(async (ctx) => { const cols = Object.entries(columns).map(([n, def]) => this.colDef(ctx.dialect, n, def.type, def)); const sql = `CREATE TABLE ${name} (${cols.join(', ')})`; await ctx.query(sql); }); return this; } /** * Adiciona operação para remover tabela. * * @param name - Nome da tabela * @returns Instância do builder para method chaining * * @example * ```typescript * // Dados iniciais * const migrationBuilder = new MigrationBuilder(); * * // Como usar * migrationBuilder.dropTable('old_users'); * * // Output: Operação de remoção de tabela adicionada ao builder * ``` */ dropTable(name) { this.steps.push(async (ctx) => { await ctx.query(`DROP TABLE IF EXISTS ${name}`); }); return this; } /** * Adiciona operação para adicionar coluna. * * @param table - Nome da tabela * @param column - Nome da coluna * @param def - Definição da coluna * @returns Instância do builder para method chaining * * @example * ```typescript * // Dados iniciais * const migrationBuilder = new MigrationBuilder(); * * // Como usar * migrationBuilder.addColumn('users', 'age', { * type: ColumnType.Int, * notNull: false, * default: 18 * }); * * // Output: Operação de adição de coluna adicionada ao builder * ``` */ addColumn(table, column, def) { this.steps.push(async (ctx) => { const sql = `ALTER TABLE ${table} ADD COLUMN ${this.colDef(ctx.dialect, column, def.type, def)}`; await ctx.query(sql); }); return this; } /** * Adiciona operação para remover coluna. * * @param table - Nome da tabela * @param column - Nome da coluna * @returns Instância do builder para method chaining * * @example * ```typescript * // Dados iniciais * const migrationBuilder = new MigrationBuilder(); * * // Como usar * migrationBuilder.dropColumn('users', 'old_field'); * * // Output: Operação de remoção de coluna adicionada ao builder * ``` */ dropColumn(table, column) { this.steps.push(async (ctx) => { await ctx.query(`ALTER TABLE ${table} DROP COLUMN ${column}`); }); return this; } /** * Adiciona operação para renomear coluna. * * @param table - Nome da tabela * @param from - Nome atual da coluna * @param to - Novo nome da coluna * @returns Instância do builder para method chaining * * @example * ```typescript * // Dados iniciais * const migrationBuilder = new MigrationBuilder(); * * // Como usar * migrationBuilder.renameColumn('users', 'user_name', 'name'); * * // Output: Operação de renomeação de coluna adicionada ao builder * ``` */ renameColumn(table, from, to) { this.steps.push(async (ctx) => { await ctx.query(`ALTER TABLE ${table} RENAME COLUMN ${from} TO ${to}`); }); return this; } /** * Adiciona operação para criar índice. * * @param table - Nome da tabela * @param columns - Colunas para o índice * @param opts - Opções do índice * @returns Instância do builder para method chaining * * @example * ```typescript * // Dados iniciais * const migrationBuilder = new MigrationBuilder(); * * // Como usar * migrationBuilder.createIndex('users', ['email'], { unique: true }); * migrationBuilder.createIndex('users', ['name', 'age'], { name: 'users_name_age_idx' }); * * // Output: Operação de criação de índice adicionada ao builder * ``` */ createIndex(table, columns, opts = {}) { this.steps.push(async (ctx) => { const name = opts.name || `${table}_${columns.join('_')}_idx`; const uniq = opts.unique ? 'UNIQUE ' : ''; const sql = `CREATE ${uniq}INDEX IF NOT EXISTS ${name} ON ${table} (${columns.join(', ')})`; await ctx.query(sql); }); return this; } /** * Adiciona operação para remover índice. * * @param name - Nome do índice * @returns Instância do builder para method chaining * * @example * ```typescript * // Dados iniciais * const migrationBuilder = new MigrationBuilder(); * * // Como usar * migrationBuilder.dropIndex('users_email_idx'); * * // Output: Operação de remoção de índice adicionada ao builder * ``` */ dropIndex(name) { this.steps.push(async (ctx) => { await ctx.query(`DROP INDEX IF EXISTS ${name}`); }); return this; } /** * Adiciona operação para criar tabela de junção (many-to-many). * * @param name - Nome da tabela de junção * @param leftTable - Tabela esquerda * @param rightTable - Tabela direita * @param opts - Opções da tabela de junção * @returns Instância do builder para method chaining * * @example * ```typescript * // Dados iniciais * const migrationBuilder = new MigrationBuilder(); * * // Como usar * migrationBuilder.createJoinTable('user_roles', 'users', 'roles', { * cascade: true, * leftKeyName: 'user_id', * rightKeyName: 'role_id' * }); * * // Output: Operação de criação de tabela de junção adicionada ao builder * ``` */ createJoinTable(name, leftTable, rightTable, opts = {}) { this.steps.push(async (ctx) => { const leftCol = opts.leftKeyName || `${leftTable}_id`; const rightCol = opts.rightKeyName || `${rightTable}_id`; const on = opts.cascade ? 'CASCADE' : undefined; const cols = [ this.colDef(ctx.dialect, leftCol, ColumnType.Int, { notNull: true, references: { table: leftTable, onDelete: on } }), this.colDef(ctx.dialect, rightCol, ColumnType.Int, { notNull: true, references: { table: rightTable, onDelete: on } }), ]; const sql = `CREATE TABLE ${name} (${cols.join(', ')})`; await ctx.query(sql); // composite unique to avoid duplicates const idxName = `${name}_${leftCol}_${rightCol}_uniq`; await ctx.query(`CREATE UNIQUE INDEX IF NOT EXISTS ${idxName} ON ${name} (${leftCol}, ${rightCol})`); }); return this; } /** * Adiciona operação para executar SQL raw. * * @param sql - SQL raw para executar * @returns Instância do builder para method chaining * * @example * ```typescript * // Dados iniciais * const migrationBuilder = new MigrationBuilder(); * * // Como usar * migrationBuilder.raw('INSERT INTO settings (key, value) VALUES ("version", "2.0")'); * * // Output: Operação de SQL raw adicionada ao builder * ``` */ raw(sql) { this.steps.push(async (ctx) => { await ctx.query(sql); }); return this; } /** * Aplica todas as operações de migração em sequência. * * @param ctx - Contexto da migração * @returns Promise que resolve quando todas as operações forem executadas * * @example * ```typescript * // Dados iniciais * const migrationBuilder = new MigrationBuilder(); * migrationBuilder.createTable('users', { ... }); * const ctx = migrationContext; * * // Como usar * await migrationBuilder.apply(ctx); * * // Output: Todas as operações de migração executadas com sucesso * ``` */ async apply(ctx) { for (const s of this.steps) await s(ctx); } } /** * Factory function para criar passos de migração usando DSL. * Permite definir migrações de forma declarativa e legível. * * @param dsl - Função que recebe MigrationBuilder para configurar operações * @returns MigrationStep que pode ser executado pelo sistema de migração * * @example * ```typescript * // Dados iniciais * const migrationStep = migration((builder) => { * builder * .createTable('users', { * id: { type: ColumnType.Int, primaryKey: true, autoIncrement: true }, * name: { type: ColumnType.String, length: 255, notNull: true }, * email: { type: ColumnType.String, length: 255, unique: true } * }) * .createIndex('users', ['email'], { unique: true }); * }); * * // Como usar * await migrateUp([{ id: '001_create_users', up: migrationStep }]); * * // Output: Migração executada criando tabela users com índice único em email * ``` */ export function migration(dsl) { return async (ctx) => { const b = new MigrationBuilder(); dsl(b); await b.apply(ctx); }; }