@t1mmen/srtd 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -118
- package/dist/package.json +97 -0
- package/dist/src/__tests__/apply.test.js +69 -0
- package/dist/src/__tests__/apply.test.js.map +1 -0
- package/dist/src/__tests__/build.test.js +67 -0
- package/dist/src/__tests__/build.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/vitest.setup.js +41 -25
- package/dist/src/__tests__/vitest.setup.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/watch.test.js +9 -3
- package/dist/src/__tests__/watch.test.js.map +1 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/commands/_app.js +29 -0
- package/dist/src/commands/_app.js.map +1 -0
- package/dist/{commands → src/commands}/apply.d.ts +2 -1
- package/dist/src/commands/apply.js +30 -0
- package/dist/src/commands/apply.js.map +1 -0
- package/dist/{commands → src/commands}/build.d.ts +5 -1
- package/dist/src/commands/build.js +41 -0
- package/dist/src/commands/build.js.map +1 -0
- package/dist/src/commands/clear.d.ts +2 -0
- package/dist/src/commands/clear.js +45 -0
- package/dist/src/commands/clear.js.map +1 -0
- package/dist/{commands → src/commands}/index.js +11 -5
- package/dist/src/commands/index.js.map +1 -0
- package/dist/src/commands/init.js.map +1 -0
- package/dist/{commands → src/commands}/register.js +6 -5
- package/dist/src/commands/register.js.map +1 -0
- package/dist/{commands → src/commands}/watch.js +61 -33
- package/dist/src/commands/watch.js.map +1 -0
- package/dist/{components → src/components}/Branding.js +7 -7
- package/dist/src/components/Branding.js.map +1 -0
- package/dist/src/components/Debug.d.ts +2 -0
- package/dist/src/components/Debug.js +160 -0
- package/dist/src/components/Debug.js.map +1 -0
- package/dist/src/components/ProcessingResults.d.ts +9 -0
- package/dist/src/components/ProcessingResults.js +32 -0
- package/dist/src/components/ProcessingResults.js.map +1 -0
- package/dist/{components → src/components}/Quittable.js +1 -1
- package/dist/src/components/Quittable.js.map +1 -0
- package/dist/src/components/TimeSince.js.map +1 -0
- package/dist/src/components/customTheme.d.ts +10 -0
- package/dist/src/components/customTheme.js +66 -0
- package/dist/src/components/customTheme.js.map +1 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/hooks/useDatabaseConnection.js +57 -0
- package/dist/src/hooks/useDatabaseConnection.js.map +1 -0
- package/dist/{hooks → src/hooks}/useTemplateManager.d.ts +6 -8
- package/dist/src/hooks/useTemplateManager.js +124 -0
- package/dist/src/hooks/useTemplateManager.js.map +1 -0
- package/dist/src/hooks/useTemplateProcessor.d.ts +11 -0
- package/dist/src/hooks/useTemplateProcessor.js +71 -0
- package/dist/src/hooks/useTemplateProcessor.js.map +1 -0
- package/dist/src/hooks/useTemplateState.js.map +1 -0
- package/dist/{lib → src/lib}/templateManager.d.ts +13 -2
- package/dist/{lib → src/lib}/templateManager.js +172 -50
- package/dist/src/lib/templateManager.js.map +1 -0
- package/dist/src/lib/templateManager.test.js +729 -0
- package/dist/src/lib/templateManager.test.js.map +1 -0
- package/dist/{types.d.ts → src/types.d.ts} +3 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/applyMigration.js.map +1 -0
- package/dist/src/utils/applyMigrations.test.js.map +1 -0
- package/dist/src/utils/calculateMD5.js.map +1 -0
- package/dist/{utils → src/utils}/config.d.ts +2 -0
- package/dist/src/utils/config.js +65 -0
- package/dist/src/utils/config.js.map +1 -0
- package/dist/src/utils/config.test.js.map +1 -0
- package/dist/src/utils/createEmptyBuildLog.js.map +1 -0
- package/dist/{utils → src/utils}/databaseConnection.d.ts +5 -0
- package/dist/{utils → src/utils}/databaseConnection.js +40 -13
- package/dist/src/utils/databaseConnection.js.map +1 -0
- package/dist/src/utils/databaseConnection.test.d.ts +1 -0
- package/dist/src/utils/databaseConnection.test.js.map +1 -0
- package/dist/src/utils/ensureDirectories.js.map +1 -0
- package/dist/src/utils/fileExists.js.map +1 -0
- package/dist/src/utils/getNextTimestamp.js.map +1 -0
- package/dist/src/utils/isWipTemplate.js.map +1 -0
- package/dist/src/utils/loadBuildLog.js.map +1 -0
- package/dist/src/utils/loadBuildLog.test.d.ts +1 -0
- package/dist/src/utils/loadBuildLog.test.js.map +1 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/registerTemplate.js.map +1 -0
- package/dist/src/utils/safeCreate.js.map +1 -0
- package/dist/src/utils/saveBuildLog.js.map +1 -0
- package/dist/src/utils/store.d.ts +5 -0
- package/dist/src/utils/store.js +10 -0
- package/dist/src/utils/store.js.map +1 -0
- package/package.json +4 -3
- package/dist/__tests__/vitest.setup.js.map +0 -1
- package/dist/__tests__/watch.test.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/_app.js +0 -27
- package/dist/commands/_app.js.map +0 -1
- package/dist/commands/apply.js +0 -32
- package/dist/commands/apply.js.map +0 -1
- package/dist/commands/build.js +0 -25
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/index.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/register.js.map +0 -1
- package/dist/commands/watch.js.map +0 -1
- package/dist/components/Branding.js.map +0 -1
- package/dist/components/Quittable.js.map +0 -1
- package/dist/components/TimeSince.js.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/hooks/useDatabaseConnection.js +0 -68
- package/dist/hooks/useDatabaseConnection.js.map +0 -1
- package/dist/hooks/useTemplateManager.js +0 -141
- package/dist/hooks/useTemplateManager.js.map +0 -1
- package/dist/hooks/useTemplateState.js.map +0 -1
- package/dist/lib/templateManager.js.map +0 -1
- package/dist/lib/templateManager.test.js +0 -289
- package/dist/lib/templateManager.test.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils/applyMigration.js.map +0 -1
- package/dist/utils/applyMigrations.test.js.map +0 -1
- package/dist/utils/calculateMD5.js.map +0 -1
- package/dist/utils/config.js +0 -79
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/config.test.js.map +0 -1
- package/dist/utils/createEmptyBuildLog.js.map +0 -1
- package/dist/utils/databaseConnection.js.map +0 -1
- package/dist/utils/databaseConnection.test.js.map +0 -1
- package/dist/utils/ensureDirectories.js.map +0 -1
- package/dist/utils/fileExists.js.map +0 -1
- package/dist/utils/getNextTimestamp.js.map +0 -1
- package/dist/utils/isWipTemplate.js.map +0 -1
- package/dist/utils/loadBuildLog.js.map +0 -1
- package/dist/utils/loadBuildLog.test.js.map +0 -1
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/registerTemplate.js.map +0 -1
- package/dist/utils/safeCreate.js.map +0 -1
- package/dist/utils/saveBuildLog.js.map +0 -1
- /package/dist/{__tests__/watch.test.d.ts → src/__tests__/apply.test.d.ts} +0 -0
- /package/dist/{lib/templateManager.test.d.ts → src/__tests__/build.test.d.ts} +0 -0
- /package/dist/{__tests__ → src/__tests__}/vitest.setup.d.ts +0 -0
- /package/dist/{utils/applyMigrations.test.d.ts → src/__tests__/watch.test.d.ts} +0 -0
- /package/dist/{cli.d.ts → src/cli.d.ts} +0 -0
- /package/dist/{cli.js → src/cli.js} +0 -0
- /package/dist/{commands → src/commands}/_app.d.ts +0 -0
- /package/dist/{commands → src/commands}/index.d.ts +0 -0
- /package/dist/{commands → src/commands}/init.d.ts +0 -0
- /package/dist/{commands → src/commands}/init.js +0 -0
- /package/dist/{commands → src/commands}/register.d.ts +0 -0
- /package/dist/{commands → src/commands}/watch.d.ts +0 -0
- /package/dist/{components → src/components}/Branding.d.ts +0 -0
- /package/dist/{components → src/components}/Quittable.d.ts +0 -0
- /package/dist/{components → src/components}/TimeSince.d.ts +0 -0
- /package/dist/{components → src/components}/TimeSince.js +0 -0
- /package/dist/{constants.d.ts → src/constants.d.ts} +0 -0
- /package/dist/{constants.js → src/constants.js} +0 -0
- /package/dist/{hooks → src/hooks}/useDatabaseConnection.d.ts +0 -0
- /package/dist/{hooks → src/hooks}/useTemplateState.d.ts +0 -0
- /package/dist/{hooks → src/hooks}/useTemplateState.js +0 -0
- /package/dist/{utils/config.test.d.ts → src/lib/templateManager.test.d.ts} +0 -0
- /package/dist/{types.js → src/types.js} +0 -0
- /package/dist/{utils → src/utils}/applyMigration.d.ts +0 -0
- /package/dist/{utils → src/utils}/applyMigration.js +0 -0
- /package/dist/{utils/databaseConnection.test.d.ts → src/utils/applyMigrations.test.d.ts} +0 -0
- /package/dist/{utils → src/utils}/applyMigrations.test.js +0 -0
- /package/dist/{utils → src/utils}/calculateMD5.d.ts +0 -0
- /package/dist/{utils → src/utils}/calculateMD5.js +0 -0
- /package/dist/{utils/loadBuildLog.test.d.ts → src/utils/config.test.d.ts} +0 -0
- /package/dist/{utils → src/utils}/config.test.js +0 -0
- /package/dist/{utils → src/utils}/createEmptyBuildLog.d.ts +0 -0
- /package/dist/{utils → src/utils}/createEmptyBuildLog.js +0 -0
- /package/dist/{utils → src/utils}/databaseConnection.test.js +0 -0
- /package/dist/{utils → src/utils}/ensureDirectories.d.ts +0 -0
- /package/dist/{utils → src/utils}/ensureDirectories.js +0 -0
- /package/dist/{utils → src/utils}/fileExists.d.ts +0 -0
- /package/dist/{utils → src/utils}/fileExists.js +0 -0
- /package/dist/{utils → src/utils}/getNextTimestamp.d.ts +0 -0
- /package/dist/{utils → src/utils}/getNextTimestamp.js +0 -0
- /package/dist/{utils → src/utils}/isWipTemplate.d.ts +0 -0
- /package/dist/{utils → src/utils}/isWipTemplate.js +0 -0
- /package/dist/{utils → src/utils}/loadBuildLog.d.ts +0 -0
- /package/dist/{utils → src/utils}/loadBuildLog.js +0 -0
- /package/dist/{utils → src/utils}/loadBuildLog.test.js +0 -0
- /package/dist/{utils → src/utils}/logger.d.ts +0 -0
- /package/dist/{utils → src/utils}/logger.js +0 -0
- /package/dist/{utils → src/utils}/registerTemplate.d.ts +0 -0
- /package/dist/{utils → src/utils}/registerTemplate.js +0 -0
- /package/dist/{utils → src/utils}/safeCreate.d.ts +0 -0
- /package/dist/{utils → src/utils}/safeCreate.js +0 -0
- /package/dist/{utils → src/utils}/saveBuildLog.d.ts +0 -0
- /package/dist/{utils → src/utils}/saveBuildLog.js +0 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import fs from 'node:fs/promises';
|
|
54
|
+
import { tmpdir } from 'node:os';
|
|
55
|
+
import { default as path, join, relative } from 'node:path';
|
|
56
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
57
|
+
import { TEST_FN_PREFIX } from '../__tests__/vitest.setup.js';
|
|
58
|
+
import { calculateMD5 } from '../utils/calculateMD5.js';
|
|
59
|
+
import { connect, disconnect } from '../utils/databaseConnection.js';
|
|
60
|
+
import { ensureDirectories } from '../utils/ensureDirectories.js';
|
|
61
|
+
import { TemplateManager } from './templateManager.js';
|
|
62
|
+
describe('TemplateManager', () => {
|
|
63
|
+
const testContext = {
|
|
64
|
+
timestamp: Date.now(),
|
|
65
|
+
testDir: '',
|
|
66
|
+
testFunctionName: '',
|
|
67
|
+
};
|
|
68
|
+
beforeEach(async () => {
|
|
69
|
+
testContext.testDir = join(tmpdir(), `srtd-test-${testContext.timestamp}`);
|
|
70
|
+
testContext.testFunctionName = `${TEST_FN_PREFIX}${testContext.timestamp}`;
|
|
71
|
+
await ensureDirectories(testContext.testDir);
|
|
72
|
+
const client = await connect();
|
|
73
|
+
try {
|
|
74
|
+
await client.query('BEGIN');
|
|
75
|
+
await client.query(`DROP FUNCTION IF EXISTS ${testContext.testFunctionName}()`);
|
|
76
|
+
await client.query('COMMIT');
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
await client.query('ROLLBACK');
|
|
80
|
+
throw e;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
client.release();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
afterEach(async () => {
|
|
87
|
+
const client = await connect();
|
|
88
|
+
try {
|
|
89
|
+
await client.query('BEGIN');
|
|
90
|
+
await client.query(`DROP FUNCTION IF EXISTS ${testContext.testFunctionName}()`);
|
|
91
|
+
await client.query('COMMIT');
|
|
92
|
+
}
|
|
93
|
+
catch (_) {
|
|
94
|
+
await client.query('ROLLBACK');
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
client.release();
|
|
98
|
+
}
|
|
99
|
+
await fs.rm(testContext.testDir, { recursive: true, force: true });
|
|
100
|
+
disconnect();
|
|
101
|
+
});
|
|
102
|
+
const createTemplate = async (name, content, dir) => {
|
|
103
|
+
const fullPath = dir
|
|
104
|
+
? join(testContext.testDir, 'test-templates', dir, name)
|
|
105
|
+
: join(testContext.testDir, 'test-templates', name);
|
|
106
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
107
|
+
await fs.writeFile(fullPath, content);
|
|
108
|
+
return fullPath;
|
|
109
|
+
};
|
|
110
|
+
const createTemplateWithFunc = async (name, funcSuffix = '', dir) => {
|
|
111
|
+
const funcName = `${testContext.testFunctionName}${funcSuffix}`;
|
|
112
|
+
const content = `CREATE OR REPLACE FUNCTION ${funcName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`;
|
|
113
|
+
return createTemplate(name, content, dir);
|
|
114
|
+
};
|
|
115
|
+
it('should create migration file when template changes', async () => {
|
|
116
|
+
await createTemplateWithFunc(`test-${testContext.timestamp}.sql`);
|
|
117
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
118
|
+
await manager.processTemplates({ generateFiles: true });
|
|
119
|
+
const migrations = await fs.readdir(join(testContext.testDir, 'test-migrations'));
|
|
120
|
+
const relevantMigrations = migrations.filter(m => m.includes(`test-${testContext.timestamp}`));
|
|
121
|
+
expect(relevantMigrations).toHaveLength(1);
|
|
122
|
+
});
|
|
123
|
+
it('should not allow building WIP templates', async () => {
|
|
124
|
+
await createTemplateWithFunc(`test-${testContext.timestamp}.wip.sql`);
|
|
125
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
126
|
+
await manager.processTemplates({ generateFiles: true });
|
|
127
|
+
const migrations = await fs.readdir(join(testContext.testDir, 'test-migrations'));
|
|
128
|
+
expect(migrations.filter(m => m.includes(`test-${testContext.timestamp}`))).toHaveLength(0);
|
|
129
|
+
});
|
|
130
|
+
it('should maintain separate build and local logs', async () => {
|
|
131
|
+
const templatePath = join(testContext.testDir, 'test-templates', `test-${testContext.timestamp}.sql`);
|
|
132
|
+
const templateContent = `CREATE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`;
|
|
133
|
+
await fs.writeFile(templatePath, templateContent);
|
|
134
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
135
|
+
// Build writes to build log
|
|
136
|
+
await manager.processTemplates({ generateFiles: true });
|
|
137
|
+
const buildLog = JSON.parse(await fs.readFile(join(testContext.testDir, '.buildlog-test.json'), 'utf-8'));
|
|
138
|
+
const relPath = relative(testContext.testDir, templatePath);
|
|
139
|
+
expect(buildLog.templates[relPath].lastBuildHash).toBeDefined();
|
|
140
|
+
// Apply writes to local log
|
|
141
|
+
await manager.processTemplates({ apply: true });
|
|
142
|
+
const localLog = JSON.parse(await fs.readFile(join(testContext.testDir, '.buildlog-test.local.json'), 'utf-8'));
|
|
143
|
+
expect(localLog.templates[relPath].lastAppliedHash).toBeDefined();
|
|
144
|
+
});
|
|
145
|
+
it('should track template state correctly', async () => {
|
|
146
|
+
const templatePath = join(testContext.testDir, 'test-templates', `test-${testContext.timestamp}.sql`);
|
|
147
|
+
const templateContent = `CREATE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`;
|
|
148
|
+
await fs.writeFile(templatePath, templateContent);
|
|
149
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
150
|
+
// Initially no state
|
|
151
|
+
let status = await manager.getTemplateStatus(templatePath);
|
|
152
|
+
expect(status.buildState.lastBuildHash).toBeUndefined();
|
|
153
|
+
expect(status.buildState.lastAppliedHash).toBeUndefined();
|
|
154
|
+
// After build
|
|
155
|
+
await manager.processTemplates({ generateFiles: true });
|
|
156
|
+
status = await manager.getTemplateStatus(templatePath);
|
|
157
|
+
expect(status.buildState.lastBuildHash).toBeDefined();
|
|
158
|
+
expect(status.buildState.lastBuildDate).toBeDefined();
|
|
159
|
+
// After apply
|
|
160
|
+
await manager.processTemplates({ apply: true });
|
|
161
|
+
status = await manager.getTemplateStatus(templatePath);
|
|
162
|
+
expect(status.buildState.lastAppliedHash).toBeDefined();
|
|
163
|
+
expect(status.buildState.lastAppliedDate).toBeDefined();
|
|
164
|
+
});
|
|
165
|
+
it('should handle rapid template changes', async () => {
|
|
166
|
+
const templatePath = join(testContext.testDir, 'test-templates', `test-${testContext.timestamp}.sql`);
|
|
167
|
+
const baseContent = `CREATE OR REPLACE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`;
|
|
168
|
+
await fs.writeFile(templatePath, baseContent);
|
|
169
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
170
|
+
const changes = [];
|
|
171
|
+
manager.on('templateChanged', async (template) => {
|
|
172
|
+
changes.push(template.currentHash);
|
|
173
|
+
});
|
|
174
|
+
const watcher = await manager.watch();
|
|
175
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
176
|
+
// Make rapid changes
|
|
177
|
+
for (let i = 0; i < 5; i++) {
|
|
178
|
+
await fs.writeFile(templatePath, `${baseContent}\n-- Change ${i}`);
|
|
179
|
+
await new Promise(resolve => setTimeout(resolve, 51));
|
|
180
|
+
}
|
|
181
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
182
|
+
watcher.close();
|
|
183
|
+
expect(changes.length).toBeGreaterThanOrEqual(1);
|
|
184
|
+
expect(new Set(changes).size).toBe(changes.length); // All changes should be unique
|
|
185
|
+
}, 10000);
|
|
186
|
+
it('should apply WIP templates directly to database', async () => {
|
|
187
|
+
const templatePath = join(testContext.testDir, 'test-templates', `test-${testContext.timestamp}.wip.sql`);
|
|
188
|
+
const templateContent = `CREATE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`;
|
|
189
|
+
await fs.writeFile(templatePath, templateContent);
|
|
190
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
191
|
+
const result = await manager.processTemplates({ apply: true });
|
|
192
|
+
expect(result.errors).toHaveLength(0);
|
|
193
|
+
const client = await connect();
|
|
194
|
+
try {
|
|
195
|
+
const res = await client.query(`SELECT COUNT(*) FROM pg_proc WHERE proname = $1`, [
|
|
196
|
+
testContext.testFunctionName,
|
|
197
|
+
]);
|
|
198
|
+
expect(Number.parseInt(res.rows[0].count)).toBe(1);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
client.release();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
it('should handle sequential template operations', async () => {
|
|
205
|
+
const templates = await Promise.all([...Array(5)].map(async (_, i) => {
|
|
206
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
207
|
+
return createTemplateWithFunc(`sequencetest-${i}-${testContext.timestamp}.sql`, `_sequence_test_${i}`);
|
|
208
|
+
}));
|
|
209
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
210
|
+
// Apply templates one by one
|
|
211
|
+
for (const _templatePath of templates) {
|
|
212
|
+
const result = await manager.processTemplates({ apply: true });
|
|
213
|
+
expect(result.errors).toHaveLength(0);
|
|
214
|
+
}
|
|
215
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
216
|
+
const client = await connect();
|
|
217
|
+
try {
|
|
218
|
+
for (let i = 0; i < 5; i++) {
|
|
219
|
+
const res = await client.query(`SELECT proname FROM pg_proc WHERE proname = $1`, [
|
|
220
|
+
`${testContext.testFunctionName}_sequence_test_${i}`,
|
|
221
|
+
]);
|
|
222
|
+
expect(res.rows).toHaveLength(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
client.release();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
it('should generate unique timestamps for multiple templates', async () => {
|
|
230
|
+
const templates = await Promise.all([...Array(10)].map((_, i) => createTemplateWithFunc(`timestamptest-${i}-${testContext.timestamp}.sql`, `_${i}`)));
|
|
231
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
232
|
+
await manager.processTemplates({ generateFiles: true });
|
|
233
|
+
const migrations = await fs.readdir(join(testContext.testDir, 'test-migrations'));
|
|
234
|
+
const timestamps = migrations.map(m => m.split('_')[0]);
|
|
235
|
+
const uniqueTimestamps = new Set(timestamps);
|
|
236
|
+
expect(uniqueTimestamps.size).toBe(templates.length);
|
|
237
|
+
expect(timestamps).toEqual([...timestamps].sort());
|
|
238
|
+
});
|
|
239
|
+
it('should handle mix of working and broken templates', async () => {
|
|
240
|
+
await createTemplateWithFunc(`a-test-good-${testContext.timestamp}.sql`, '_good');
|
|
241
|
+
await createTemplate(`a-test-bad-${testContext.timestamp}.sql`, 'INVALID SQL SYNTAX;');
|
|
242
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
243
|
+
const result = await manager.processTemplates({ apply: true });
|
|
244
|
+
expect(result.errors).toHaveLength(1);
|
|
245
|
+
expect(result.applied).toHaveLength(1);
|
|
246
|
+
const client = await connect();
|
|
247
|
+
try {
|
|
248
|
+
const res = await client.query(`SELECT COUNT(*) FROM pg_proc WHERE proname = $1`, [
|
|
249
|
+
`${testContext.testFunctionName}_good`,
|
|
250
|
+
]);
|
|
251
|
+
expect(Number.parseInt(res.rows[0].count)).toBe(1);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
client.release();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
it('should handle database errors gracefully', async () => {
|
|
258
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
259
|
+
await createTemplate(`test-error-${testContext.timestamp}.sql`, 'SELECT 1/0;'); // Division by zero error
|
|
260
|
+
const result = await manager.processTemplates({ apply: true });
|
|
261
|
+
expect(result.errors).toHaveLength(1);
|
|
262
|
+
expect(result.errors[0]?.error).toMatch(/division by zero/i);
|
|
263
|
+
});
|
|
264
|
+
it('should handle file system errors', async () => {
|
|
265
|
+
const errorPath = join(testContext.testDir, 'test-templates', `test-error-${testContext.timestamp}.sql`);
|
|
266
|
+
try {
|
|
267
|
+
await createTemplate(`test-error-${testContext.timestamp}.sql`, 'SELECT 1;');
|
|
268
|
+
await fs.chmod(errorPath, 0o000);
|
|
269
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
270
|
+
await manager.processTemplates({ generateFiles: true });
|
|
271
|
+
// Cleanup for afterEach
|
|
272
|
+
await fs.chmod(errorPath, 0o644);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
expect(error).toBeDefined();
|
|
276
|
+
expect(error).toMatchObject({
|
|
277
|
+
errno: -13,
|
|
278
|
+
code: 'EACCES',
|
|
279
|
+
syscall: 'open',
|
|
280
|
+
path: expect.stringContaining('test-error'),
|
|
281
|
+
});
|
|
282
|
+
// expect(error.length).toBeGreaterThan(0);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
it('should handle large batches of templates', async () => {
|
|
286
|
+
// Create 50 templates
|
|
287
|
+
await Promise.all([...Array(50)].map((_, i) => createTemplateWithFunc(`test-${i}-${testContext.timestamp}.sql`, `_${i}`)));
|
|
288
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
289
|
+
const result = await manager.processTemplates({ generateFiles: true });
|
|
290
|
+
expect(result.errors).toHaveLength(0);
|
|
291
|
+
const migrations = await fs.readdir(join(testContext.testDir, 'test-migrations'));
|
|
292
|
+
expect(migrations.length).toBe(50);
|
|
293
|
+
});
|
|
294
|
+
it('should handle templates with complex SQL', async () => {
|
|
295
|
+
const complexSQL = `
|
|
296
|
+
CREATE OR REPLACE FUNCTION ${testContext.testFunctionName}(
|
|
297
|
+
param1 integer DEFAULT 100,
|
|
298
|
+
OUT result1 integer,
|
|
299
|
+
OUT result2 text
|
|
300
|
+
) RETURNS record AS $$
|
|
301
|
+
DECLARE
|
|
302
|
+
temp_var integer;
|
|
303
|
+
BEGIN
|
|
304
|
+
-- Complex logic with multiple statements
|
|
305
|
+
SELECT CASE
|
|
306
|
+
WHEN param1 > 100 THEN param1 * 2
|
|
307
|
+
ELSE param1 / 2
|
|
308
|
+
END INTO temp_var;
|
|
309
|
+
|
|
310
|
+
result1 := temp_var;
|
|
311
|
+
result2 := 'Processed: ' || temp_var::text;
|
|
312
|
+
|
|
313
|
+
-- Exception handling
|
|
314
|
+
EXCEPTION WHEN OTHERS THEN
|
|
315
|
+
result1 := -1;
|
|
316
|
+
result2 := SQLERRM;
|
|
317
|
+
END;
|
|
318
|
+
$$ LANGUAGE plpgsql;
|
|
319
|
+
`;
|
|
320
|
+
await createTemplate(`test-complex-${testContext.timestamp}.sql`, complexSQL);
|
|
321
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
322
|
+
const result = await manager.processTemplates({ apply: true });
|
|
323
|
+
expect(result.errors).toHaveLength(0);
|
|
324
|
+
const client = await connect();
|
|
325
|
+
try {
|
|
326
|
+
const res = await client.query(`
|
|
327
|
+
SELECT proname, pronargs, prorettype::regtype::text as return_type
|
|
328
|
+
FROM pg_proc
|
|
329
|
+
WHERE proname = $1
|
|
330
|
+
`, [testContext.testFunctionName]);
|
|
331
|
+
expect(res.rows).toHaveLength(1);
|
|
332
|
+
expect(res.rows[0].return_type).toBe('record');
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
client.release();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
it('should maintain template state across manager instances', async () => {
|
|
339
|
+
const template = await createTemplateWithFunc(`test-${testContext.timestamp}.sql`);
|
|
340
|
+
// First manager instance
|
|
341
|
+
const manager1 = await TemplateManager.create(testContext.testDir);
|
|
342
|
+
await manager1.processTemplates({ generateFiles: true });
|
|
343
|
+
// Second manager instance should see the state
|
|
344
|
+
const manager2 = await TemplateManager.create(testContext.testDir);
|
|
345
|
+
const status = await manager2.getTemplateStatus(template);
|
|
346
|
+
expect(status.buildState.lastBuildHash).toBeDefined();
|
|
347
|
+
});
|
|
348
|
+
it('should handle template additions in watch mode', async () => {
|
|
349
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
350
|
+
const changes = [];
|
|
351
|
+
manager.on('templateChanged', template => {
|
|
352
|
+
changes.push(template.name);
|
|
353
|
+
});
|
|
354
|
+
const watcher = await manager.watch();
|
|
355
|
+
// Add new template after watch started
|
|
356
|
+
await createTemplateWithFunc(`test-new-${testContext.timestamp}.sql`);
|
|
357
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
358
|
+
watcher.close();
|
|
359
|
+
expect(changes).toContain(`test-new-${testContext.timestamp}`);
|
|
360
|
+
});
|
|
361
|
+
it('should handle templates in deep subdirectories', async () => {
|
|
362
|
+
// Create nested directory structure
|
|
363
|
+
const depth = 5;
|
|
364
|
+
const templatePaths = [];
|
|
365
|
+
for (let i = 1; i <= depth; i++) {
|
|
366
|
+
const dir = [...Array(i)].map((_, idx) => `level${idx + 1}`).join('/');
|
|
367
|
+
const templatePath = await createTemplateWithFunc(`depth-test-${i}-${testContext.timestamp}.sql`, `_depth_${i}`, dir);
|
|
368
|
+
templatePaths.push(templatePath);
|
|
369
|
+
}
|
|
370
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
371
|
+
const changes = [];
|
|
372
|
+
manager.on('templateChanged', template => {
|
|
373
|
+
changes.push(template.name);
|
|
374
|
+
});
|
|
375
|
+
const watcher = await manager.watch();
|
|
376
|
+
await new Promise(resolve => setTimeout(resolve, depth * 100 * 1.1));
|
|
377
|
+
watcher.close();
|
|
378
|
+
expect(changes.length).toBe(depth);
|
|
379
|
+
// Verify each template was detected
|
|
380
|
+
for (let i = 1; i <= depth; i++) {
|
|
381
|
+
expect(changes).toContain(`depth-test-${i}-${testContext.timestamp}`);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
it('should only watch SQL files', async () => {
|
|
385
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
386
|
+
const changes = [];
|
|
387
|
+
manager.on('templateChanged', template => {
|
|
388
|
+
changes.push(template.name);
|
|
389
|
+
});
|
|
390
|
+
const watcher = await manager.watch();
|
|
391
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
392
|
+
// Create various file types
|
|
393
|
+
await fs.writeFile(join(testContext.testDir, 'test-templates/test.txt'), 'not sql');
|
|
394
|
+
await fs.writeFile(join(testContext.testDir, 'test-templates/test.md'), 'not sql');
|
|
395
|
+
await createTemplateWithFunc(`test-sql-${testContext.timestamp}.sql`);
|
|
396
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
397
|
+
watcher.close();
|
|
398
|
+
expect(changes).toHaveLength(1);
|
|
399
|
+
expect(changes[0]).toBe(`test-sql-${testContext.timestamp}`);
|
|
400
|
+
});
|
|
401
|
+
it('should handle multiple template changes simultaneously', async () => {
|
|
402
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
403
|
+
const changes = new Set();
|
|
404
|
+
const count = 5;
|
|
405
|
+
manager.on('templateChanged', template => {
|
|
406
|
+
changes.add(template.name);
|
|
407
|
+
});
|
|
408
|
+
const watcher = await manager.watch();
|
|
409
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
410
|
+
// Create multiple templates simultaneously
|
|
411
|
+
await Promise.all([
|
|
412
|
+
createTemplateWithFunc(`rapid_test-1-${testContext.timestamp}.sql`, '_batch_changes_1'),
|
|
413
|
+
createTemplateWithFunc(`rapid_test-2-${testContext.timestamp}.sql`, '_batch_changes_2'),
|
|
414
|
+
createTemplateWithFunc(`rapid_test-3-${testContext.timestamp}.sql`, '_batch_changes_3'),
|
|
415
|
+
createTemplateWithFunc(`rapid_test-4-${testContext.timestamp}.sql`, '_batch_changes_4', 'deep'),
|
|
416
|
+
createTemplateWithFunc(`rapid_test-5-${testContext.timestamp}.sql`, '_batch_changes_5', 'deep/nested'),
|
|
417
|
+
]);
|
|
418
|
+
// Give enough time for all changes to be detected
|
|
419
|
+
await new Promise(resolve => setTimeout(resolve, count * 100 * 1.1));
|
|
420
|
+
watcher.close();
|
|
421
|
+
expect(changes.size).toBe(count); // Should detect all 5 templates
|
|
422
|
+
for (let i = 1; i <= count; i++) {
|
|
423
|
+
expect(changes.has(`rapid_test-${i}-${testContext.timestamp}`)).toBe(true);
|
|
424
|
+
}
|
|
425
|
+
// Verify all templates were processed
|
|
426
|
+
const client = await connect();
|
|
427
|
+
try {
|
|
428
|
+
for (let i = 1; i <= count; i++) {
|
|
429
|
+
const res = await client.query(`SELECT proname FROM pg_proc WHERE proname = $1`, [
|
|
430
|
+
`${testContext.testFunctionName}_batch_changes_${i}`,
|
|
431
|
+
]);
|
|
432
|
+
expect(res.rows).toHaveLength(1);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
client.release();
|
|
437
|
+
}
|
|
438
|
+
}, 15000);
|
|
439
|
+
it('should handle rapid bulk template creation realistically', async () => {
|
|
440
|
+
const TEMPLATE_COUNT = 50;
|
|
441
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
442
|
+
const processed = new Set();
|
|
443
|
+
const failed = new Set();
|
|
444
|
+
const inProgress = new Set();
|
|
445
|
+
const events = [];
|
|
446
|
+
let resolveProcessing;
|
|
447
|
+
const processingComplete = new Promise(resolve => {
|
|
448
|
+
resolveProcessing = resolve;
|
|
449
|
+
});
|
|
450
|
+
manager.on('templateChanged', ({ name }) => {
|
|
451
|
+
events.push({ event: 'changed', template: name, time: Date.now() });
|
|
452
|
+
inProgress.add(name);
|
|
453
|
+
});
|
|
454
|
+
manager.on('templateApplied', ({ name }) => {
|
|
455
|
+
events.push({ event: 'applied', template: name, time: Date.now() });
|
|
456
|
+
processed.add(name);
|
|
457
|
+
inProgress.delete(name);
|
|
458
|
+
if (processed.size + failed.size === TEMPLATE_COUNT) {
|
|
459
|
+
resolveProcessing();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
manager.on('templateError', ({ template: { name }, error }) => {
|
|
463
|
+
events.push({ event: 'error', template: name, time: Date.now() });
|
|
464
|
+
failed.add(name);
|
|
465
|
+
inProgress.delete(name);
|
|
466
|
+
console.error('Template error:', { name, error });
|
|
467
|
+
if (processed.size + failed.size === TEMPLATE_COUNT) {
|
|
468
|
+
resolveProcessing();
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
const watcher = await manager.watch();
|
|
472
|
+
// Create all templates
|
|
473
|
+
await Promise.all(Array.from({ length: TEMPLATE_COUNT }, (_, i) => createTemplateWithFunc(`bulk_created_template_${i + 1}.sql`, `_bulk_${i + 1}`)));
|
|
474
|
+
await processingComplete;
|
|
475
|
+
watcher.close();
|
|
476
|
+
expect(processed.size + failed.size).toBe(TEMPLATE_COUNT);
|
|
477
|
+
expect(inProgress.size).toBe(0);
|
|
478
|
+
expect(failed.size).toBe(0);
|
|
479
|
+
});
|
|
480
|
+
it('should cleanup resources when disposed', async () => {
|
|
481
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
482
|
+
const changes = [];
|
|
483
|
+
manager.on('templateChanged', template => {
|
|
484
|
+
changes.push(template.name);
|
|
485
|
+
});
|
|
486
|
+
await manager.watch();
|
|
487
|
+
// Create template before disposal
|
|
488
|
+
await createTemplateWithFunc(`test-before-dispose-${testContext.timestamp}.sql`);
|
|
489
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
490
|
+
// Dispose and verify cleanup
|
|
491
|
+
manager[Symbol.dispose]();
|
|
492
|
+
// Try creating template after disposal
|
|
493
|
+
await createTemplateWithFunc(`test-after-dispose-${testContext.timestamp}.sql`);
|
|
494
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
495
|
+
expect(changes).toHaveLength(1);
|
|
496
|
+
expect(changes[0]).toBe(`test-before-dispose-${testContext.timestamp}`);
|
|
497
|
+
});
|
|
498
|
+
it('should auto-cleanup with using statement', async () => {
|
|
499
|
+
const changes = [];
|
|
500
|
+
await (async () => {
|
|
501
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
502
|
+
try {
|
|
503
|
+
const manager = __addDisposableResource(env_1, await TemplateManager.create(testContext.testDir), false);
|
|
504
|
+
manager.on('templateChanged', template => {
|
|
505
|
+
changes.push(template.name);
|
|
506
|
+
});
|
|
507
|
+
await manager.watch();
|
|
508
|
+
await createTemplateWithFunc(`test-during-scope-${testContext.timestamp}.sql`);
|
|
509
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
510
|
+
}
|
|
511
|
+
catch (e_1) {
|
|
512
|
+
env_1.error = e_1;
|
|
513
|
+
env_1.hasError = true;
|
|
514
|
+
}
|
|
515
|
+
finally {
|
|
516
|
+
__disposeResources(env_1);
|
|
517
|
+
}
|
|
518
|
+
})();
|
|
519
|
+
// After scope exit, create another template
|
|
520
|
+
await createTemplateWithFunc(`test-after-scope-${testContext.timestamp}.sql`);
|
|
521
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
522
|
+
expect(changes).toHaveLength(1);
|
|
523
|
+
expect(changes[0]).toBe(`test-during-scope-${testContext.timestamp}`);
|
|
524
|
+
});
|
|
525
|
+
it('should not process unchanged templates', async () => {
|
|
526
|
+
const templatePath = await createTemplateWithFunc(`test-unchanged-${testContext.timestamp}.sql`);
|
|
527
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
528
|
+
await manager.watch();
|
|
529
|
+
// First processing
|
|
530
|
+
await manager.processTemplates({ apply: true });
|
|
531
|
+
// Get the status after first processing
|
|
532
|
+
const statusAfterFirstRun = await manager.getTemplateStatus(templatePath);
|
|
533
|
+
const changes = [];
|
|
534
|
+
manager.on('templateChanged', template => {
|
|
535
|
+
changes.push(template.name);
|
|
536
|
+
});
|
|
537
|
+
// Process again without changes
|
|
538
|
+
await manager.processTemplates({ apply: true });
|
|
539
|
+
// Get status after second run
|
|
540
|
+
const statusAfterSecondRun = await manager.getTemplateStatus(templatePath);
|
|
541
|
+
expect(changes).toHaveLength(0);
|
|
542
|
+
expect(statusAfterSecondRun.buildState.lastBuildHash).toBe(statusAfterFirstRun.buildState.lastBuildHash);
|
|
543
|
+
expect(statusAfterSecondRun.buildState.lastAppliedHash).toBe(statusAfterFirstRun.buildState.lastAppliedHash);
|
|
544
|
+
});
|
|
545
|
+
it('should only process modified templates in batch', async () => {
|
|
546
|
+
// Create two templates
|
|
547
|
+
const template1 = await createTemplateWithFunc(`modified_tmpl_1-${testContext.timestamp}.sql`, 'mod_1');
|
|
548
|
+
await createTemplateWithFunc(`modified_tmpl_2-${testContext.timestamp}.sql`, 'mod_2');
|
|
549
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
550
|
+
// First processing of both
|
|
551
|
+
await manager.processTemplates({ apply: true });
|
|
552
|
+
const changes = [];
|
|
553
|
+
manager.on('templateChanged', template => {
|
|
554
|
+
changes.push(template.name);
|
|
555
|
+
});
|
|
556
|
+
// Modify only template1
|
|
557
|
+
await fs.writeFile(template1, `${await fs.readFile(template1, 'utf-8')}\n-- Modified`);
|
|
558
|
+
// Process both templates again
|
|
559
|
+
await manager.processTemplates({ apply: true });
|
|
560
|
+
expect(changes).toHaveLength(1);
|
|
561
|
+
expect(changes[0]).toBe(`modified_tmpl_1-${testContext.timestamp}`);
|
|
562
|
+
});
|
|
563
|
+
it('should correctly update local buildlog on apply', async () => {
|
|
564
|
+
const templatePath = await createTemplateWithFunc(`test-buildlog-${testContext.timestamp}.sql`);
|
|
565
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
566
|
+
const localBuildlogPath = join(testContext.testDir, '.buildlog-test.local.json');
|
|
567
|
+
// Initial apply
|
|
568
|
+
await manager.processTemplates({ apply: true });
|
|
569
|
+
const initialLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8'));
|
|
570
|
+
const relPath = relative(testContext.testDir, templatePath);
|
|
571
|
+
const initialHash = initialLog.templates[relPath].lastAppliedHash;
|
|
572
|
+
const initialContent = await fs.readFile(templatePath, 'utf-8');
|
|
573
|
+
expect(initialHash).toBeDefined();
|
|
574
|
+
// Modify template
|
|
575
|
+
await fs.writeFile(templatePath, `${initialContent}\n-- Modified`);
|
|
576
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
577
|
+
const changedContent = await fs.readFile(templatePath, 'utf-8');
|
|
578
|
+
// Second apply
|
|
579
|
+
await manager.processTemplates({ apply: true });
|
|
580
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
581
|
+
const updatedLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8'));
|
|
582
|
+
const newHash = updatedLog.templates[relPath].lastAppliedHash;
|
|
583
|
+
const manualMd5 = await calculateMD5(changedContent);
|
|
584
|
+
expect(newHash).toBeDefined();
|
|
585
|
+
expect(newHash).toBe(manualMd5);
|
|
586
|
+
expect(newHash).not.toBe(initialHash);
|
|
587
|
+
});
|
|
588
|
+
it('should skip apply if template hash matches local buildlog', async () => {
|
|
589
|
+
const templatePath = await createTemplateWithFunc(`test-skip-${testContext.timestamp}.sql`);
|
|
590
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
591
|
+
const localBuildlogPath = join(testContext.testDir, '.buildlog-test.local.json');
|
|
592
|
+
// Initial apply
|
|
593
|
+
await manager.processTemplates({ apply: true });
|
|
594
|
+
const initialLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8'));
|
|
595
|
+
const relPath = relative(testContext.testDir, templatePath);
|
|
596
|
+
const initialHash = initialLog.templates[relPath].lastAppliedHash;
|
|
597
|
+
const initialDate = initialLog.templates[relPath].lastAppliedDate;
|
|
598
|
+
// Wait a bit to ensure timestamp would be different
|
|
599
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
600
|
+
// Apply again without changes
|
|
601
|
+
await manager.processTemplates({ apply: true });
|
|
602
|
+
const updatedLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8'));
|
|
603
|
+
// Hash and date should remain exactly the same since no changes were made
|
|
604
|
+
expect(updatedLog.templates[relPath].lastAppliedHash).toBe(initialHash);
|
|
605
|
+
expect(updatedLog.templates[relPath].lastAppliedDate).toBe(initialDate);
|
|
606
|
+
});
|
|
607
|
+
it('should not reapply unchanged templates in watch mode', async () => {
|
|
608
|
+
// Create multiple templates
|
|
609
|
+
const templates = await Promise.all([
|
|
610
|
+
createTemplateWithFunc(`watch-stable-1-${testContext.timestamp}.sql`, '_watch_1'),
|
|
611
|
+
createTemplateWithFunc(`watch-stable-2-${testContext.timestamp}.sql`, '_watch_2'),
|
|
612
|
+
]);
|
|
613
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
614
|
+
const applied = [];
|
|
615
|
+
const changed = [];
|
|
616
|
+
manager.on('templateChanged', template => {
|
|
617
|
+
changed.push(template.name);
|
|
618
|
+
});
|
|
619
|
+
manager.on('templateApplied', template => {
|
|
620
|
+
applied.push(template.name);
|
|
621
|
+
});
|
|
622
|
+
// First watch session
|
|
623
|
+
const watcher1 = await manager.watch();
|
|
624
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
625
|
+
await watcher1.close();
|
|
626
|
+
// Record initial state
|
|
627
|
+
const initialApplied = [...applied];
|
|
628
|
+
const initialChanged = [...changed];
|
|
629
|
+
applied.length = 0;
|
|
630
|
+
changed.length = 0;
|
|
631
|
+
// Second watch session without any changes
|
|
632
|
+
const watcher2 = await manager.watch();
|
|
633
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
634
|
+
await watcher2.close();
|
|
635
|
+
expect(initialApplied).toHaveLength(2); // First run should apply both
|
|
636
|
+
expect(initialChanged).toHaveLength(2); // First run should detect both
|
|
637
|
+
expect(applied).toHaveLength(0); // Second run should apply none
|
|
638
|
+
expect(changed).toHaveLength(0); // Second run should detect none
|
|
639
|
+
// Verify the buildlog state
|
|
640
|
+
const localBuildlogPath = join(testContext.testDir, '.buildlog-test.local.json');
|
|
641
|
+
const buildLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8'));
|
|
642
|
+
for (const templatePath of templates) {
|
|
643
|
+
const relPath = relative(testContext.testDir, templatePath);
|
|
644
|
+
const content = await fs.readFile(templatePath, 'utf-8');
|
|
645
|
+
const hash = await calculateMD5(content);
|
|
646
|
+
expect(buildLog.templates[relPath].lastAppliedHash).toBe(hash);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
it('should process unapplied templates on startup', async () => {
|
|
650
|
+
// Create template but don't process it
|
|
651
|
+
await createTemplateWithFunc(`startup-test-${testContext.timestamp}.sql`);
|
|
652
|
+
// Create a new manager instance
|
|
653
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
654
|
+
const changes = [];
|
|
655
|
+
const applied = [];
|
|
656
|
+
manager.on('templateChanged', t => changes.push(t.name));
|
|
657
|
+
manager.on('templateApplied', t => applied.push(t.name));
|
|
658
|
+
// Start watching - this should process the template
|
|
659
|
+
await manager.watch();
|
|
660
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
661
|
+
expect(changes).toHaveLength(1);
|
|
662
|
+
expect(applied).toHaveLength(1);
|
|
663
|
+
expect(changes[0]).toBe(`startup-test-${testContext.timestamp}`);
|
|
664
|
+
expect(applied[0]).toBe(`startup-test-${testContext.timestamp}`);
|
|
665
|
+
});
|
|
666
|
+
it('should handle error state transitions correctly', async () => {
|
|
667
|
+
const templatePath = await createTemplateWithFunc(`error-state-${testContext.timestamp}.sql`, '_error_test');
|
|
668
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
669
|
+
const states = [];
|
|
670
|
+
manager.on('templateChanged', () => states.push({ type: 'changed' }));
|
|
671
|
+
manager.on('templateApplied', () => states.push({ type: 'applied' }));
|
|
672
|
+
manager.on('templateError', ({ error }) => states.push({ type: 'error', error: String(error) }));
|
|
673
|
+
// First apply should succeed
|
|
674
|
+
await manager.processTemplates({ apply: true });
|
|
675
|
+
// Modify template to be invalid
|
|
676
|
+
await fs.writeFile(templatePath, 'INVALID SQL;');
|
|
677
|
+
await manager.processTemplates({ apply: true });
|
|
678
|
+
// Fix template with valid SQL
|
|
679
|
+
await fs.writeFile(templatePath, `CREATE OR REPLACE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`);
|
|
680
|
+
await manager.processTemplates({ apply: true });
|
|
681
|
+
expect(states).toEqual([
|
|
682
|
+
{ type: 'changed' },
|
|
683
|
+
{ type: 'applied' },
|
|
684
|
+
{ type: 'changed' },
|
|
685
|
+
{ type: 'error', error: expect.stringMatching(/syntax error/) },
|
|
686
|
+
{ type: 'changed' },
|
|
687
|
+
{ type: 'applied' },
|
|
688
|
+
]);
|
|
689
|
+
});
|
|
690
|
+
it('should maintain correct state through manager restarts', async () => {
|
|
691
|
+
const templatePath = await createTemplateWithFunc(`restart-test-${testContext.timestamp}.sql`);
|
|
692
|
+
// First manager instance
|
|
693
|
+
const manager1 = await TemplateManager.create(testContext.testDir);
|
|
694
|
+
await manager1.processTemplates({ apply: true });
|
|
695
|
+
// Get initial state
|
|
696
|
+
const status1 = await manager1.getTemplateStatus(templatePath);
|
|
697
|
+
const initialHash = status1.buildState.lastAppliedHash;
|
|
698
|
+
// Modify template
|
|
699
|
+
await fs.writeFile(templatePath, `${await fs.readFile(templatePath, 'utf-8')}\n-- Modified`);
|
|
700
|
+
// Create new manager instance
|
|
701
|
+
const manager2 = await TemplateManager.create(testContext.testDir);
|
|
702
|
+
const changes = [];
|
|
703
|
+
const applied = [];
|
|
704
|
+
manager2.on('templateChanged', t => changes.push(t.name));
|
|
705
|
+
manager2.on('templateApplied', t => applied.push(t.name));
|
|
706
|
+
await manager2.watch();
|
|
707
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
708
|
+
// Verify state was maintained and change was detected
|
|
709
|
+
const status2 = await manager2.getTemplateStatus(templatePath);
|
|
710
|
+
expect(status2.buildState.lastAppliedHash).not.toBe(initialHash);
|
|
711
|
+
expect(changes).toContain(`restart-test-${testContext.timestamp}`);
|
|
712
|
+
expect(applied).toContain(`restart-test-${testContext.timestamp}`);
|
|
713
|
+
});
|
|
714
|
+
it('should properly format and propagate error messages', async () => {
|
|
715
|
+
const templatePath = await createTemplateWithFunc(`error-format-${testContext.timestamp}.sql`);
|
|
716
|
+
const manager = await TemplateManager.create(testContext.testDir);
|
|
717
|
+
const errors = [];
|
|
718
|
+
manager.on('templateError', err => errors.push(err));
|
|
719
|
+
// Create invalid SQL
|
|
720
|
+
await fs.writeFile(templatePath, 'SELECT * FROM nonexistent_table;');
|
|
721
|
+
await manager.processTemplates({ apply: true });
|
|
722
|
+
expect(errors).toHaveLength(1);
|
|
723
|
+
const error = errors[0]?.error;
|
|
724
|
+
expect(typeof error).toBe('string');
|
|
725
|
+
expect(error).not.toMatch(/\[object Object\]/);
|
|
726
|
+
expect(error).toMatch(/relation.*does not exist/i);
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
//# sourceMappingURL=templateManager.test.js.map
|