UNPKG

@t1mmen/srtd

Version:

Supabase Repeatable Template Definitions (srtd): 🪄 Live-reloading SQL templates for Supabase DX. Make your database changes reviewable and migrations maintainable! 🚀

1,068 lines (1,066 loc) • 48.3 kB
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) { if (value !== null && value !== void 0) { if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected."); var dispose, inner; if (async) { if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined."); dispose = value[Symbol.asyncDispose]; } if (dispose === void 0) { if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined."); dispose = value[Symbol.dispose]; if (async) inner = dispose; } if (typeof dispose !== "function") throw new TypeError("Object not disposable."); if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } }; env.stack.push({ value: value, dispose: dispose, async: async }); } else if (async) { env.stack.push({ async: true }); } return value; }; var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) { return function (env) { function fail(e) { env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e; env.hasError = true; } var r, s = 0; function next() { while (r = env.stack.pop()) { try { if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next); if (r.dispose) { var result = r.dispose.call(r.value); if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); }); } else s |= 1; } catch (e) { fail(e); } } if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve(); if (env.hasError) throw env.error; } return next(); }; })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }); import fs from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { default as path, join, relative } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { TEST_FN_PREFIX } from '../__tests__/vitest.setup.js'; import { calculateMD5 } from '../utils/calculateMD5.js'; import { connect } from '../utils/databaseConnection.js'; import { ensureDirectories } from '../utils/ensureDirectories.js'; import { TemplateManager } from './templateManager.js'; const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms)); describe('TemplateManager', () => { const testContext = { testId: 0, testDir: tmpdir(), testFunctionName: TEST_FN_PREFIX, templateCounter: 0, }; beforeEach(async () => { testContext.testId = Math.floor(Math.random() * 1000000); testContext.testDir = join(tmpdir(), `srtd-test`); testContext.testFunctionName = `${TEST_FN_PREFIX}`; testContext.templateCounter = 0; await ensureDirectories(testContext.testDir); const client = await connect(); try { await client.query('BEGIN'); await client.query(`DROP FUNCTION IF EXISTS ${testContext.testFunctionName}()`); await client.query('COMMIT'); } catch (e) { await client.query('ROLLBACK'); throw e; } finally { client.release(); } }); afterEach(async () => { const client = await connect(); try { await client.query('BEGIN'); await client.query(`DROP FUNCTION IF EXISTS ${testContext.testFunctionName}()`); await client.query('COMMIT'); } catch (_) { await client.query('ROLLBACK'); } finally { client.release(); } await fs.rm(testContext.testDir, { recursive: true, force: true }); }); // Helper to generate unique template names const getNextTemplateName = (prefix = 'template') => { testContext.templateCounter++; return `${prefix}_${testContext.testId}_${testContext.templateCounter}`; }; const createTemplate = async (name, content, dir) => { const fullPath = dir ? join(testContext.testDir, 'test-templates', dir, name) : join(testContext.testDir, 'test-templates', name); try { await fs.mkdir(path.dirname(fullPath), { recursive: true }); await fs.writeFile(fullPath, content); return fullPath; } catch (error) { console.error('Error creating template:', error); throw error; } }; const createTemplateWithFunc = async (prefix, funcSuffix = '', dir) => { const name = `${getNextTemplateName(prefix)}.sql`; const funcName = `${testContext.testFunctionName}${funcSuffix}`; const content = `CREATE OR REPLACE FUNCTION ${funcName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`; return createTemplate(name, content, dir); }; it('should create migration file when template changes', async () => { const env_1 = { stack: [], error: void 0, hasError: false }; try { await createTemplateWithFunc('basic', '_file_change'); const manager = __addDisposableResource(env_1, await TemplateManager.create(testContext.testDir), false); await manager.processTemplates({ generateFiles: true }); const migrations = await fs.readdir(join(testContext.testDir, 'test-migrations')); expect(migrations.length).toBe(1); } catch (e_1) { env_1.error = e_1; env_1.hasError = true; } finally { __disposeResources(env_1); } }); it('should not allow building WIP templates', async () => { const env_2 = { stack: [], error: void 0, hasError: false }; try { await createTemplateWithFunc('file.wip', '_wip_wont_build'); const manager = __addDisposableResource(env_2, await TemplateManager.create(testContext.testDir), false); await manager.processTemplates({ generateFiles: true }); const migrations = await fs.readdir(join(testContext.testDir, 'test-migrations')); expect(migrations.filter(m => m.includes(`wip`))).toHaveLength(0); } catch (e_2) { env_2.error = e_2; env_2.hasError = true; } finally { __disposeResources(env_2); } }); it('should maintain separate build and local logs', async () => { const env_3 = { stack: [], error: void 0, hasError: false }; try { const templatePath = join(testContext.testDir, 'test-templates', `template_${testContext.testId}_1.sql`); const templateContent = `CREATE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`; await fs.writeFile(templatePath, templateContent); const manager = __addDisposableResource(env_3, await TemplateManager.create(testContext.testDir), false); // Build writes to build log await manager.processTemplates({ generateFiles: true }); const buildLog = JSON.parse(await fs.readFile(join(testContext.testDir, '.buildlog-test.json'), 'utf-8')); const relPath = relative(testContext.testDir, templatePath); expect(buildLog.templates[relPath].lastBuildHash).toBeDefined(); // Apply writes to local log await manager.processTemplates({ apply: true }); const localLog = JSON.parse(await fs.readFile(join(testContext.testDir, '.buildlog-test.local.json'), 'utf-8')); expect(localLog.templates[relPath].lastAppliedHash).toBeDefined(); } catch (e_3) { env_3.error = e_3; env_3.hasError = true; } finally { __disposeResources(env_3); } }); it('should track template state correctly', async () => { const env_4 = { stack: [], error: void 0, hasError: false }; try { const templatePath = join(testContext.testDir, 'test-templates', `template_${testContext.testId}_1.sql`); const templateContent = `CREATE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`; await fs.writeFile(templatePath, templateContent); const manager = __addDisposableResource(env_4, await TemplateManager.create(testContext.testDir), false); // Initially no state let status = await manager.getTemplateStatus(templatePath); expect(status.buildState.lastBuildHash).toBeUndefined(); expect(status.buildState.lastAppliedHash).toBeUndefined(); // After build await manager.processTemplates({ generateFiles: true }); status = await manager.getTemplateStatus(templatePath); expect(status.buildState.lastBuildHash).toBeDefined(); expect(status.buildState.lastBuildDate).toBeDefined(); // After apply await manager.processTemplates({ apply: true }); status = await manager.getTemplateStatus(templatePath); expect(status.buildState.lastAppliedHash).toBeDefined(); expect(status.buildState.lastAppliedDate).toBeDefined(); } catch (e_4) { env_4.error = e_4; env_4.hasError = true; } finally { __disposeResources(env_4); } }); it('should handle rapid template changes', async () => { const env_5 = { stack: [], error: void 0, hasError: false }; try { const templatePath = join(testContext.testDir, 'test-templates', `template_${testContext.testId}_1.sql`); const baseContent = `CREATE OR REPLACE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`; await fs.writeFile(templatePath, baseContent); const manager = __addDisposableResource(env_5, await TemplateManager.create(testContext.testDir), false); const changes = []; manager.on('templateChanged', async (template) => { changes.push(template.currentHash); }); const watcher = await manager.watch(); await wait(100); // Make rapid changes for (let i = 0; i < 5; i++) { await fs.writeFile(templatePath, `${baseContent}\n-- Change ${i}`); await wait(100); } await wait(500); watcher.close(); expect(changes.length).toBeGreaterThanOrEqual(1); expect(new Set(changes).size).toBe(changes.length); // All changes should be unique } catch (e_5) { env_5.error = e_5; env_5.hasError = true; } finally { __disposeResources(env_5); } }, 10000); it('should apply WIP templates directly to database', async () => { const env_6 = { stack: [], error: void 0, hasError: false }; try { const templatePath = join(testContext.testDir, 'test-templates', `template_${testContext.testId}_1.wip.sql`); const templateContent = `CREATE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`; await fs.writeFile(templatePath, templateContent); const manager = __addDisposableResource(env_6, await TemplateManager.create(testContext.testDir), false); const result = await manager.processTemplates({ apply: true }); expect(result.errors).toHaveLength(0); const client = await connect(); try { const res = await client.query(`SELECT COUNT(*) FROM pg_proc WHERE proname = $1`, [ testContext.testFunctionName, ]); expect(Number.parseInt(res.rows[0].count)).toBe(1); } finally { client.release(); } } catch (e_6) { env_6.error = e_6; env_6.hasError = true; } finally { __disposeResources(env_6); } }); it('should handle sequential template operations', async () => { const env_7 = { stack: [], error: void 0, hasError: false }; try { const tmpls = await Promise.all([...Array(5)].map((_, i) => createTemplateWithFunc(`sequencetest_${i}`, `_sequence_test_${i}`))); expect(tmpls).toHaveLength(5); const manager = __addDisposableResource(env_7, await TemplateManager.create(testContext.testDir), false); const client = await connect(); await wait(100); try { // Start transaction await client.query('BEGIN'); const result = await manager.processTemplates({ apply: true, force: true }); // Add retry logic for verification const verifyFunctions = async (retries = 3, delay = 200) => { try { const allFunctions = await client.query(`SELECT proname FROM pg_proc WHERE proname LIKE $1`, [`${testContext.testFunctionName}_sequence_test_%`]); expect(allFunctions.rows).toHaveLength(5); } catch (error) { console.log('Flakey test failed verifying functions:', error, 'retries', retries); if (retries === 0) throw error; await wait(delay); await verifyFunctions(retries - 1, delay * 2); } }; await verifyFunctions(); await client.query('COMMIT'); expect(result.errors).toHaveLength(0); expect(result.applied).toHaveLength(5); } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } } catch (e_7) { env_7.error = e_7; env_7.hasError = true; } finally { __disposeResources(env_7); } }); it('should generate unique timestamps for multiple templates', async () => { const env_8 = { stack: [], error: void 0, hasError: false }; try { const templates = await Promise.all([...Array(10)].map((_, i) => createTemplateWithFunc(`timestamptest_${i}`, `_unique_timestamps_${i}`))); const manager = __addDisposableResource(env_8, await TemplateManager.create(testContext.testDir), false); await manager.processTemplates({ generateFiles: true }); const migrations = await fs.readdir(join(testContext.testDir, 'test-migrations')); const timestamps = migrations.map(m => m.split('_')[0]); const uniqueTimestamps = new Set(timestamps); expect(uniqueTimestamps.size).toBe(templates.length); expect(timestamps).toEqual([...timestamps].sort()); } catch (e_8) { env_8.error = e_8; env_8.hasError = true; } finally { __disposeResources(env_8); } }); it('should handle mix of working and broken templates', async () => { const env_9 = { stack: [], error: void 0, hasError: false }; try { await createTemplateWithFunc(`a-test-good`, '_good_and_broken_mix'); await createTemplate(`a-test-bad.sql`, 'INVALID SQL SYNTAX;'); const manager = __addDisposableResource(env_9, await TemplateManager.create(testContext.testDir), false); const result = await manager.processTemplates({ apply: true }); expect(result.errors).toHaveLength(1); expect(result.applied).toHaveLength(1); const client = await connect(); try { const res = await client.query(`SELECT COUNT(*) FROM pg_proc WHERE proname = $1`, [ `${testContext.testFunctionName}_good_and_broken_mix`, ]); expect(Number.parseInt(res.rows[0].count)).toBe(1); } finally { client.release(); } } catch (e_9) { env_9.error = e_9; env_9.hasError = true; } finally { __disposeResources(env_9); } }); it('should handle database errors gracefully', async () => { const env_10 = { stack: [], error: void 0, hasError: false }; try { const manager = __addDisposableResource(env_10, await TemplateManager.create(testContext.testDir), false); await createTemplate(`test-error.sql`, 'SELECT 1/0;'); // Division by zero error const result = await manager.processTemplates({ apply: true }); expect(result.errors).toHaveLength(1); expect(result.errors[0]?.error).toMatch(/division by zero/i); } catch (e_10) { env_10.error = e_10; env_10.hasError = true; } finally { __disposeResources(env_10); } }); it('should handle file system errors', async () => { const errorPath = join(testContext.testDir, 'test-templates', `test-error.sql`); try { const env_11 = { stack: [], error: void 0, hasError: false }; try { await createTemplate(`test-error.sql`, 'SELECT 1;'); await fs.chmod(errorPath, 0o000); const manager = __addDisposableResource(env_11, await TemplateManager.create(testContext.testDir), false); try { await manager.processTemplates({ generateFiles: true }); } catch (error) { expect(error).toBeDefined(); } // Cleanup for afterEach await fs.chmod(errorPath, 0o644); } catch (e_11) { env_11.error = e_11; env_11.hasError = true; } finally { __disposeResources(env_11); } } catch (error) { expect(error).toBeDefined(); expect(error).toMatchObject({ errno: -13, code: 'EACCES', syscall: 'open', path: expect.stringContaining('test-error'), }); // expect(error.length).toBeGreaterThan(0); } }); it('should handle large batches of templates', async () => { const env_12 = { stack: [], error: void 0, hasError: false }; try { // Create 50 templates await Promise.all([...Array(50)].map((_, i) => createTemplateWithFunc(`test_${i}`, `_large_batch_${i}`))); const manager = __addDisposableResource(env_12, await TemplateManager.create(testContext.testDir), false); const result = await manager.processTemplates({ generateFiles: true }); expect(result.errors).toHaveLength(0); const migrations = await fs.readdir(join(testContext.testDir, 'test-migrations')); expect(migrations.length).toBe(50); } catch (e_12) { env_12.error = e_12; env_12.hasError = true; } finally { __disposeResources(env_12); } }); it('should handle templates with complex SQL', async () => { const env_13 = { stack: [], error: void 0, hasError: false }; try { const testFunctionName = `${testContext.testFunctionName}_complex`; const complexSQL = ` CREATE OR REPLACE FUNCTION ${testFunctionName}( param1 integer DEFAULT 100, OUT result1 integer, OUT result2 text ) RETURNS record AS $$ DECLARE temp_var integer; BEGIN -- Complex logic with multiple statements SELECT CASE WHEN param1 > 100 THEN param1 * 2 ELSE param1 / 2 END INTO temp_var; result1 := temp_var; result2 := 'Processed: ' || temp_var::text; -- Exception handling EXCEPTION WHEN OTHERS THEN result1 := -1; result2 := SQLERRM; END; $$ LANGUAGE plpgsql; `; await createTemplate(`test-complex.sql`, complexSQL); const manager = __addDisposableResource(env_13, await TemplateManager.create(testContext.testDir), false); const result = await manager.processTemplates({ apply: true }); expect(result.errors).toHaveLength(0); const client = await connect(); try { const res = await client.query(` SELECT proname, pronargs, prorettype::regtype::text as return_type FROM pg_proc WHERE proname = $1 `, [testFunctionName]); expect(res.rows).toHaveLength(1); expect(res.rows[0].return_type).toBe('record'); } finally { client.release(); } } catch (e_13) { env_13.error = e_13; env_13.hasError = true; } finally { __disposeResources(env_13); } }); it('should maintain template state across manager instances', async () => { const env_14 = { stack: [], error: void 0, hasError: false }; try { const template = await createTemplateWithFunc(`test`, 'maintain_state'); // First manager instance const manager1 = __addDisposableResource(env_14, await TemplateManager.create(testContext.testDir), false); await manager1.processTemplates({ generateFiles: true }); // Second manager instance should see the state const manager2 = __addDisposableResource(env_14, await TemplateManager.create(testContext.testDir), false); const status = await manager2.getTemplateStatus(template); expect(status.buildState.lastBuildHash).toBeDefined(); } catch (e_14) { env_14.error = e_14; env_14.hasError = true; } finally { __disposeResources(env_14); } }); it('should handle template additions in watch mode', async () => { const env_15 = { stack: [], error: void 0, hasError: false }; try { const manager = __addDisposableResource(env_15, await TemplateManager.create(testContext.testDir), false); const changes = []; manager.on('templateChanged', template => { changes.push(template.name); }); const watcher = await manager.watch(); // Add new template after watch started await createTemplateWithFunc('new', '_watch_addition'); await wait(150); watcher.close(); expect(changes).toContain(`new_${testContext.testId}_1`); } catch (e_15) { env_15.error = e_15; env_15.hasError = true; } finally { __disposeResources(env_15); } }); it('should handle templates in deep subdirectories', async () => { const env_16 = { stack: [], error: void 0, hasError: false }; try { // Create nested directory structure const depth = 5; const templatePaths = []; for (let i = 1; i <= depth; i++) { const dir = [...Array(i)].map((_, idx) => `level${idx + 1}`).join('/'); const templatePath = await createTemplateWithFunc(`depth-test_${i}`, `_depth_${i}`, dir); templatePaths.push(templatePath); } const manager = __addDisposableResource(env_16, await TemplateManager.create(testContext.testDir), false); const changes = []; manager.on('templateChanged', template => { changes.push(template.name); }); const watcher = await manager.watch(); await wait(depth * 100 * 1.1); watcher.close(); expect(changes.length).toBe(depth); // Verify each template was detected for (let i = 1; i <= depth; i++) { expect(changes).toContain(`depth-test_${i}_${testContext.testId}_${i}`); } } catch (e_16) { env_16.error = e_16; env_16.hasError = true; } finally { __disposeResources(env_16); } }); it('should only watch SQL files', async () => { const env_17 = { stack: [], error: void 0, hasError: false }; try { const manager = __addDisposableResource(env_17, await TemplateManager.create(testContext.testDir), false); const changes = []; manager.on('templateChanged', template => { changes.push(template.name); }); const watcher = await manager.watch(); await wait(100); // Create various file types await fs.writeFile(join(testContext.testDir, 'test-templates/test.txt'), 'not sql'); await fs.writeFile(join(testContext.testDir, 'test-templates/test.md'), 'not sql'); await createTemplateWithFunc(`sql`, '_watch_sql_only'); await wait(500); watcher.close(); expect(changes).toHaveLength(1); expect(changes[0]).toBe(`sql_${testContext.testId}_1`); } catch (e_17) { env_17.error = e_17; env_17.hasError = true; } finally { __disposeResources(env_17); } }); it('should handle multiple template changes simultaneously', async () => { const env_18 = { stack: [], error: void 0, hasError: false }; try { const client = await connect(); const manager = __addDisposableResource(env_18, await TemplateManager.create(testContext.testDir), false); const changes = new Set(); const count = 5; const watcher = await manager.watch(); await wait(100); manager.on('templateChanged', template => { changes.add(template.name); }); // Create multiple templates simultaneously try { await createTemplateWithFunc(`rapid_test_1`, '_batch_changes_1'); await createTemplateWithFunc(`rapid_test_2`, '_batch_changes_2'); await createTemplateWithFunc(`rapid_test_3`, '_batch_changes_3'); await createTemplateWithFunc(`rapid_test_4`, '_batch_changes_4', 'deep'); await createTemplateWithFunc(`rapid_test_5`, '_batch_changes_5', 'deep/nested'); } catch (error) { console.error('Error creating templates:', error); throw error; } // Give enough time for all changes to be detected await wait(count * 100 * 1.1); watcher.close(); expect(changes.size).toBe(count); // Should detect all 5 templates for (let i = 1; i <= count; i++) { expect(changes.has(`rapid_test_${i}_${testContext.testId}_${i}`)).toBe(true); } // Verify all templates were processed await wait(100); try { const res = await client.query(`SELECT proname FROM pg_proc WHERE proname LIKE $1`, [ `${testContext.testFunctionName}_batch_changes_%`, ]); // expect(res).toBe(''); expect(res.rows).toHaveLength(count); } catch (error) { console.error('Error querying functions:', error); } finally { client.release(); } } catch (e_18) { env_18.error = e_18; env_18.hasError = true; } finally { __disposeResources(env_18); } }, 15000); it('should handle rapid bulk template creation realistically', async () => { const env_19 = { stack: [], error: void 0, hasError: false }; try { const TEMPLATE_COUNT = 50; const manager = __addDisposableResource(env_19, await TemplateManager.create(testContext.testDir), false); const processed = new Set(); const failed = new Set(); const inProgress = new Set(); const events = []; let resolveProcessing; const processingComplete = new Promise(resolve => { resolveProcessing = resolve; }); manager.on('templateChanged', ({ name }) => { events.push({ event: 'changed', template: name, time: Date.now() }); inProgress.add(name); }); manager.on('templateApplied', ({ name }) => { events.push({ event: 'applied', template: name, time: Date.now() }); processed.add(name); inProgress.delete(name); if (processed.size + failed.size === TEMPLATE_COUNT) { resolveProcessing(); } }); manager.on('templateError', ({ template: { name }, error }) => { events.push({ event: 'error', template: name, time: Date.now() }); failed.add(name); inProgress.delete(name); console.error('Template error:', { name, error }); if (processed.size + failed.size === TEMPLATE_COUNT) { resolveProcessing(); } }); const watcher = await manager.watch(); // Create all templates await Promise.all(Array.from({ length: TEMPLATE_COUNT }, (_, i) => createTemplateWithFunc(`bulk_${i + 1}`, `_bulk_${i + 1}`))); await processingComplete; watcher.close(); expect(processed.size + failed.size).toBe(TEMPLATE_COUNT); expect(inProgress.size).toBe(0); expect(failed.size).toBe(0); } catch (e_19) { env_19.error = e_19; env_19.hasError = true; } finally { __disposeResources(env_19); } }); it('should cleanup resources when disposed', async () => { const env_20 = { stack: [], error: void 0, hasError: false }; try { const manager = __addDisposableResource(env_20, await TemplateManager.create(testContext.testDir), false); const changes = []; manager.on('templateChanged', template => { changes.push(template.name); }); await manager.watch(); // Create template before disposal await createTemplateWithFunc(`before-dispose`, 'before_dispose'); await wait(100); // Dispose and verify cleanup manager[Symbol.dispose](); // Try creating template after disposal await createTemplateWithFunc(`after-dispose`, 'after_dispose'); await wait(100); expect(changes).toHaveLength(1); expect(changes[0]).toBe(`before-dispose_${testContext.testId}_1`); } catch (e_20) { env_20.error = e_20; env_20.hasError = true; } finally { __disposeResources(env_20); } }); it('should auto-cleanup with using statement', async () => { const changes = []; await (async () => { const env_21 = { stack: [], error: void 0, hasError: false }; try { const manager = __addDisposableResource(env_21, await TemplateManager.create(testContext.testDir), false); manager.on('templateChanged', template => { changes.push(template.name); }); await manager.watch(); await wait(100); await createTemplateWithFunc(`during-scope`, 'during_scope'); await wait(100); } catch (e_21) { env_21.error = e_21; env_21.hasError = true; } finally { __disposeResources(env_21); } })(); // After scope exit, create another template await createTemplateWithFunc(`after-scope`, 'after_scope'); await wait(100); expect(changes[0]).toBe(`during-scope_${testContext.testId}_1`); expect(changes).toHaveLength(1); }); it('should not process unchanged templates', async () => { const env_22 = { stack: [], error: void 0, hasError: false }; try { const templatePath = await createTemplateWithFunc(`initial_will_remain_unchanged`, 'unchanged_tmpl'); const manager = __addDisposableResource(env_22, await TemplateManager.create(testContext.testDir), false); await manager.watch(); // First processing await manager.processTemplates({ apply: true }); // Get the status after first processing const statusAfterFirstRun = await manager.getTemplateStatus(templatePath); const changes = []; manager.on('templateChanged', template => { changes.push(template.name); }); // Process again without changes await manager.processTemplates({ apply: true }); // Get status after second run const statusAfterSecondRun = await manager.getTemplateStatus(templatePath); expect(changes).toHaveLength(0); expect(statusAfterSecondRun.buildState.lastBuildHash).toBe(statusAfterFirstRun.buildState.lastBuildHash); expect(statusAfterSecondRun.buildState.lastAppliedHash).toBe(statusAfterFirstRun.buildState.lastAppliedHash); } catch (e_22) { env_22.error = e_22; env_22.hasError = true; } finally { __disposeResources(env_22); } }); it('should only process modified templates in batch', async () => { const env_23 = { stack: [], error: void 0, hasError: false }; try { // Create two templates const template1 = await createTemplateWithFunc(`modified_tmpl_1`, 'mod_1'); await createTemplateWithFunc(`modified_tmpl_2`, 'mod_2'); const manager = __addDisposableResource(env_23, await TemplateManager.create(testContext.testDir), false); // First processing of both await manager.processTemplates({ apply: true }); const changes = []; manager.on('templateChanged', template => { changes.push(template.name); }); // Modify only template1 try { const tmpl1content = await fs.readFile(template1, 'utf-8'); await fs.writeFile(template1, `${tmpl1content}\n-- Modified`); } catch (error) { console.error('Test: Error modifying template:', error); throw error; } // Process both templates again await manager.processTemplates({ apply: true }); expect(changes).toHaveLength(1); expect(changes[0]).toBe(`modified_tmpl_1_${testContext.testId}_1`); } catch (e_23) { env_23.error = e_23; env_23.hasError = true; } finally { __disposeResources(env_23); } }); it('should correctly update local buildlog on apply', async () => { const env_24 = { stack: [], error: void 0, hasError: false }; try { const templatePath = await createTemplateWithFunc(`buildlog`, '_buildlog'); const manager = __addDisposableResource(env_24, await TemplateManager.create(testContext.testDir), false); const localBuildlogPath = join(testContext.testDir, '.buildlog-test.local.json'); // Initial apply await manager.processTemplates({ apply: true }); const initialLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8')); const relPath = relative(testContext.testDir, templatePath); const initialHash = initialLog.templates[relPath].lastAppliedHash; const initialContent = await fs.readFile(templatePath, 'utf-8'); expect(initialHash).toBeDefined(); // Modify template await fs.writeFile(templatePath, `${initialContent}\n-- Modified`); await wait(100); const changedContent = await fs.readFile(templatePath, 'utf-8'); // Second apply await manager.processTemplates({ apply: true }); await wait(100); const updatedLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8')); const newHash = updatedLog.templates[relPath].lastAppliedHash; const manualMd5 = await calculateMD5(changedContent); expect(newHash).toBeDefined(); expect(newHash).toBe(manualMd5); expect(newHash).not.toBe(initialHash); } catch (e_24) { env_24.error = e_24; env_24.hasError = true; } finally { __disposeResources(env_24); } }); it('should skip apply if template hash matches local buildlog', async () => { const env_25 = { stack: [], error: void 0, hasError: false }; try { const templatePath = await createTemplateWithFunc(`skip`, '_skip_apply'); const manager = __addDisposableResource(env_25, await TemplateManager.create(testContext.testDir), false); const localBuildlogPath = join(testContext.testDir, '.buildlog-test.local.json'); // Initial apply await manager.processTemplates({ apply: true }); const initialLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8')); const relPath = relative(testContext.testDir, templatePath); const initialHash = initialLog.templates[relPath].lastAppliedHash; const initialDate = initialLog.templates[relPath].lastAppliedDate; // Wait a bit to ensure timestamp would be different await wait(100); // Apply again without changes await manager.processTemplates({ apply: true }); const updatedLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8')); // Hash and date should remain exactly the same since no changes were made expect(updatedLog.templates[relPath].lastAppliedHash).toBe(initialHash); expect(updatedLog.templates[relPath].lastAppliedDate).toBe(initialDate); } catch (e_25) { env_25.error = e_25; env_25.hasError = true; } finally { __disposeResources(env_25); } }); it('should not reapply unchanged templates in watch mode', async () => { const env_26 = { stack: [], error: void 0, hasError: false }; try { // Create multiple templates const templates = await Promise.all([ createTemplateWithFunc(`watch-stable_1`, '_watch_1'), createTemplateWithFunc(`watch-stable_2`, '_watch_2'), ]); const manager = __addDisposableResource(env_26, await TemplateManager.create(testContext.testDir), false); const applied = []; const changed = []; manager.on('templateChanged', template => { changed.push(template.name); }); manager.on('templateApplied', template => { applied.push(template.name); }); // First watch session const watcher1 = await manager.watch(); await wait(100); await watcher1.close(); // Record initial state const initialApplied = [...applied]; const initialChanged = [...changed]; applied.length = 0; changed.length = 0; // Second watch session without any changes const watcher2 = await manager.watch(); await wait(100); await watcher2.close(); expect(initialApplied).toHaveLength(2); // First run should apply both expect(initialChanged).toHaveLength(2); // First run should detect both expect(applied).toHaveLength(0); // Second run should apply none expect(changed).toHaveLength(0); // Second run should detect none // Verify the buildlog state const localBuildlogPath = join(testContext.testDir, '.buildlog-test.local.json'); const buildLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8')); for (const templatePath of templates) { const relPath = relative(testContext.testDir, templatePath); const content = await fs.readFile(templatePath, 'utf-8'); const hash = await calculateMD5(content); expect(buildLog.templates[relPath].lastAppliedHash).toBe(hash); } } catch (e_26) { env_26.error = e_26; env_26.hasError = true; } finally { __disposeResources(env_26); } }); it('should process unapplied templates on startup', async () => { const env_27 = { stack: [], error: void 0, hasError: false }; try { // Create template but don't process it await createTemplateWithFunc(`startup-test`, '_startup_test'); // Create a new manager instance const manager = __addDisposableResource(env_27, await TemplateManager.create(testContext.testDir), false); const changes = []; const applied = []; manager.on('templateChanged', t => changes.push(t.name)); manager.on('templateApplied', t => applied.push(t.name)); // Start watching - this should process the template await manager.watch(); await wait(100); expect(changes).toHaveLength(1); expect(applied).toHaveLength(1); expect(changes[0]).toBe(`startup-test_${testContext.testId}_1`); expect(applied[0]).toBe(`startup-test_${testContext.testId}_1`); } catch (e_27) { env_27.error = e_27; env_27.hasError = true; } finally { __disposeResources(env_27); } }); it('should handle error state transitions correctly', async () => { const env_28 = { stack: [], error: void 0, hasError: false }; try { const templatePath = await createTemplateWithFunc(`error-state`, '_error_test'); const manager = __addDisposableResource(env_28, await TemplateManager.create(testContext.testDir), false); const states = []; manager.on('templateChanged', () => states.push({ type: 'changed' })); manager.on('templateApplied', () => states.push({ type: 'applied' })); manager.on('templateError', ({ error }) => states.push({ type: 'error', error: String(error) })); // First apply should succeed await manager.processTemplates({ apply: true }); // Modify template to be invalid await fs.writeFile(templatePath, 'INVALID SQL;'); await manager.processTemplates({ apply: true }); // Fix template with valid SQL await fs.writeFile(templatePath, `CREATE OR REPLACE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`); await manager.processTemplates({ apply: true }); expect(states).toEqual([ { type: 'changed' }, { type: 'applied' }, { type: 'changed' }, { type: 'error', error: expect.stringMatching(/syntax error/) }, { type: 'changed' }, { type: 'applied' }, ]); } catch (e_28) { env_28.error = e_28; env_28.hasError = true; } finally { __disposeResources(env_28); } }); it('should maintain correct state through manager restarts', async () => { const env_29 = { stack: [], error: void 0, hasError: false }; try { const templatePath = await createTemplateWithFunc(`restart-test`, 'restart_test'); // First manager instance const manager1 = __addDisposableResource(env_29, await TemplateManager.create(testContext.testDir), false); await manager1.processTemplates({ apply: true }); // Get initial state const status1 = await manager1.getTemplateStatus(templatePath); const initialHash = status1.buildState.lastAppliedHash; // Modify template await fs.writeFile(templatePath, `${await fs.readFile(templatePath, 'utf-8')}\n-- Modified`); // Create new manager instance const manager2 = __addDisposableResource(env_29, await TemplateManager.create(testContext.testDir), false); const changes = []; const applied = []; manager2.on('templateChanged', t => changes.push(t.name)); manager2.on('templateApplied', t => applied.push(t.name)); await manager2.watch(); await new Promise(resolve => setTimeout(resolve, 100)); // Verify state was maintained and change was detected const status2 = await manager2.getTemplateStatus(templatePath); expect(status2.buildState.lastAppliedHash).not.toBe(initialHash); expect(changes).toContain(`restart-test_${testContext.testId}_1`); expect(applied).toContain(`restart-test_${testContext.testId}_1`); } catch (e_29) { env_29.error = e_29; env_29.hasError = true; } finally { __disposeResources(env_29); } }); it('should properly format and propagate error messages', async () => { const env_30 = { stack: [], error: void 0, hasError: false }; try { const templatePath = await createTemplateWithFunc(`error-format`, 'error_format'); const manager = __addDisposableResource(env_30, await TemplateManager.create(testContext.testDir), false); const errors = []; manager.on('templateError', err => errors.push(err)); // Create invalid SQL await fs.writeFile(templatePath, 'SELECT * FROM nonexistent_table;'); await wait(50); await manager.processTemplates({ apply: true }); await wait(50); expect(errors).toHaveLength(1); const error = errors[0]?.error; expect(typeof error).toBe('string'); expect(error).not.toMatch(/\[object Object\]/); expect(error).toMatch(/relation.*does not exist/i); } catch (e_30) { env_30.error = e_30; env_30.hasError = true; } finally { __disposeResources(env_30); } }); }); //# sourceMappingURL=templateManager.test.js.map