@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
JavaScript
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