@hexaijs/postgres 0.3.0 → 0.5.0

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.
Files changed (59) hide show
  1. package/README.md +78 -24
  2. package/dist/helpers-vPAudN_S.d.ts +125 -0
  3. package/dist/index.d.ts +64 -7
  4. package/dist/index.js +828 -28
  5. package/dist/index.js.map +1 -1
  6. package/dist/test.d.ts +13 -12
  7. package/dist/test.js +683 -246
  8. package/dist/test.js.map +1 -1
  9. package/package.json +8 -7
  10. package/dist/config/index.d.ts +0 -3
  11. package/dist/config/index.d.ts.map +0 -1
  12. package/dist/config/index.js +0 -19
  13. package/dist/config/index.js.map +0 -1
  14. package/dist/config/postgres-config-spec.d.ts +0 -32
  15. package/dist/config/postgres-config-spec.d.ts.map +0 -1
  16. package/dist/config/postgres-config-spec.js +0 -49
  17. package/dist/config/postgres-config-spec.js.map +0 -1
  18. package/dist/config/postgres-config.d.ts +0 -59
  19. package/dist/config/postgres-config.d.ts.map +0 -1
  20. package/dist/config/postgres-config.js +0 -181
  21. package/dist/config/postgres-config.js.map +0 -1
  22. package/dist/helpers.d.ts +0 -57
  23. package/dist/helpers.d.ts.map +0 -1
  24. package/dist/helpers.js +0 -276
  25. package/dist/helpers.js.map +0 -1
  26. package/dist/index.d.ts.map +0 -1
  27. package/dist/postgres-event-store.d.ts +0 -18
  28. package/dist/postgres-event-store.d.ts.map +0 -1
  29. package/dist/postgres-event-store.js +0 -83
  30. package/dist/postgres-event-store.js.map +0 -1
  31. package/dist/postgres-unit-of-work.d.ts +0 -18
  32. package/dist/postgres-unit-of-work.d.ts.map +0 -1
  33. package/dist/postgres-unit-of-work.js +0 -265
  34. package/dist/postgres-unit-of-work.js.map +0 -1
  35. package/dist/run-hexai-migrations.d.ts +0 -3
  36. package/dist/run-hexai-migrations.d.ts.map +0 -1
  37. package/dist/run-hexai-migrations.js +0 -17
  38. package/dist/run-hexai-migrations.js.map +0 -1
  39. package/dist/run-migrations.d.ts +0 -11
  40. package/dist/run-migrations.d.ts.map +0 -1
  41. package/dist/run-migrations.js +0 -202
  42. package/dist/run-migrations.js.map +0 -1
  43. package/dist/test-fixtures/config.d.ts +0 -5
  44. package/dist/test-fixtures/config.d.ts.map +0 -1
  45. package/dist/test-fixtures/config.js +0 -14
  46. package/dist/test-fixtures/config.js.map +0 -1
  47. package/dist/test-fixtures/hooks.d.ts +0 -8
  48. package/dist/test-fixtures/hooks.d.ts.map +0 -1
  49. package/dist/test-fixtures/hooks.js +0 -77
  50. package/dist/test-fixtures/hooks.js.map +0 -1
  51. package/dist/test-fixtures/index.d.ts +0 -3
  52. package/dist/test-fixtures/index.d.ts.map +0 -1
  53. package/dist/test-fixtures/index.js +0 -19
  54. package/dist/test-fixtures/index.js.map +0 -1
  55. package/dist/test.d.ts.map +0 -1
  56. package/dist/types.d.ts +0 -14
  57. package/dist/types.d.ts.map +0 -1
  58. package/dist/types.js +0 -11
  59. package/dist/types.js.map +0 -1
package/dist/test.js CHANGED
@@ -1,259 +1,696 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PostgresUnitOfWorkForTesting = void 0;
4
- exports.createTestContext = createTestContext;
5
- const node_async_hooks_1 = require("node:async_hooks");
6
- const pg_1 = require("pg");
7
- const core_1 = require("@hexaijs/core");
8
- const helpers_1 = require("./helpers");
9
- const config_1 = require("./config");
10
- const run_hexai_migrations_1 = require("./run-hexai-migrations");
11
- function createTestContext(dbUrl) {
12
- const config = typeof dbUrl === "string" ? config_1.PostgresConfig.fromUrl(dbUrl) : dbUrl;
13
- const dbName = config.database;
14
- const databaseManager = new helpers_1.DatabaseManager(config.withDatabase("postgres"));
15
- const tableManager = new helpers_1.TableManager(config);
16
- async function setup() {
17
- try {
18
- await databaseManager.dropDatabase(dbName);
19
- }
20
- catch (e) {
21
- if ((0, helpers_1.isDatabaseError)(e) && e.code === "3D000") {
22
- // ignore
23
- }
24
- else {
25
- throw e;
26
- }
27
- }
28
- await databaseManager.createDatabase(dbName);
29
- await (0, run_hexai_migrations_1.runHexaiMigrations)(config);
30
- }
31
- async function teardown() {
32
- await tableManager.close();
33
- await databaseManager.dropDatabase(dbName);
34
- await databaseManager.close();
35
- }
1
+ import { AsyncLocalStorage } from 'async_hooks';
2
+ import * as pg from 'pg';
3
+ import { Client } from 'pg';
4
+ import { Propagation } from '@hexaijs/core';
5
+ import * as fs2 from 'fs/promises';
6
+ import * as path2 from 'path';
7
+ import path2__default from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import runner from 'node-pg-migrate';
10
+
11
+ // src/test.ts
12
+
13
+ // src/config/postgres-config.ts
14
+ var PostgresConfig = class _PostgresConfig {
15
+ host;
16
+ database;
17
+ user;
18
+ port;
19
+ password;
20
+ pool;
21
+ constructor(config) {
22
+ this.database = config.database;
23
+ this.password = config.password;
24
+ this.host = config.host ?? "localhost";
25
+ this.user = config.user ?? "postgres";
26
+ this.port = config.port ?? 5432;
27
+ this.pool = config.pool;
28
+ }
29
+ static fromUrl(value) {
30
+ return new _PostgresConfig(_PostgresConfig.parseUrl(value));
31
+ }
32
+ /**
33
+ * Creates a PostgresConfig from environment variables.
34
+ *
35
+ * @param prefix - Environment variable prefix
36
+ * @param options - Loading options (mode: "url" | "fields")
37
+ * @throws Error if required environment variables are not set
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * // URL mode (default): reads ASSIGNMENT_DB_URL
42
+ * const config = PostgresConfig.fromEnv("ASSIGNMENT_DB");
43
+ *
44
+ * // Fields mode: reads POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DATABASE, POSTGRES_USER, POSTGRES_PASSWORD
45
+ * const config = PostgresConfig.fromEnv("POSTGRES", { mode: "fields" });
46
+ * ```
47
+ */
48
+ static fromEnv(prefix, options) {
49
+ const mode = options?.mode ?? "url";
50
+ if (mode === "url") {
51
+ const envKey = `${prefix}_URL`;
52
+ const url = process.env[envKey];
53
+ if (!url) {
54
+ throw new Error(`Environment variable ${envKey} is not set`);
55
+ }
56
+ return _PostgresConfig.fromUrl(url);
57
+ }
58
+ const database = process.env[`${prefix}_DATABASE`];
59
+ if (!database) {
60
+ throw new Error(
61
+ `Environment variable ${prefix}_DATABASE is not set`
62
+ );
63
+ }
64
+ return new _PostgresConfig({
65
+ database,
66
+ host: process.env[`${prefix}_HOST`],
67
+ port: process.env[`${prefix}_PORT`] ? parseInt(process.env[`${prefix}_PORT`]) : void 0,
68
+ user: process.env[`${prefix}_USER`],
69
+ password: process.env[`${prefix}_PASSWORD`]
70
+ });
71
+ }
72
+ static parseUrl(value) {
73
+ const regex = /postgres(ql)?:\/\/(?<user>[^:/]+)(:(?<password>[^@]+))?@(?<host>[^:/]+)(:(?<port>\d+))?\/(?<database>.+)/;
74
+ const matches = value.match(regex);
75
+ if (!matches?.groups) {
76
+ throw new Error(`Invalid postgres url: ${value}`);
77
+ }
78
+ const { user, password, host, port, database } = matches.groups;
36
79
  return {
37
- client: tableManager.getClient(),
38
- newClient: () => new pg_1.Client(dbUrl),
39
- tableManager,
40
- setup,
41
- teardown,
80
+ host,
81
+ database,
82
+ user,
83
+ port: port ? parseInt(port) : 5432,
84
+ password
42
85
  };
86
+ }
87
+ withDatabase(database) {
88
+ return new _PostgresConfig({
89
+ host: this.host,
90
+ database,
91
+ user: this.user,
92
+ port: this.port,
93
+ password: this.password,
94
+ pool: this.pool
95
+ });
96
+ }
97
+ withUser(user) {
98
+ return new _PostgresConfig({
99
+ host: this.host,
100
+ database: this.database,
101
+ user,
102
+ port: this.port,
103
+ password: this.password,
104
+ pool: this.pool
105
+ });
106
+ }
107
+ withPassword(password) {
108
+ return new _PostgresConfig({
109
+ host: this.host,
110
+ database: this.database,
111
+ user: this.user,
112
+ port: this.port,
113
+ password,
114
+ pool: this.pool
115
+ });
116
+ }
117
+ withHost(host) {
118
+ return new _PostgresConfig({
119
+ host,
120
+ database: this.database,
121
+ user: this.user,
122
+ port: this.port,
123
+ password: this.password,
124
+ pool: this.pool
125
+ });
126
+ }
127
+ withPort(port) {
128
+ return new _PostgresConfig({
129
+ host: this.host,
130
+ database: this.database,
131
+ user: this.user,
132
+ port,
133
+ password: this.password,
134
+ pool: this.pool
135
+ });
136
+ }
137
+ withPoolSize(size) {
138
+ return new _PostgresConfig({
139
+ host: this.host,
140
+ database: this.database,
141
+ user: this.user,
142
+ port: this.port,
143
+ password: this.password,
144
+ pool: { ...this.pool, size }
145
+ });
146
+ }
147
+ withConnectionTimeout(connectionTimeout) {
148
+ return new _PostgresConfig({
149
+ host: this.host,
150
+ database: this.database,
151
+ user: this.user,
152
+ port: this.port,
153
+ password: this.password,
154
+ pool: { ...this.pool, connectionTimeout }
155
+ });
156
+ }
157
+ withIdleTimeout(idleTimeout) {
158
+ return new _PostgresConfig({
159
+ host: this.host,
160
+ database: this.database,
161
+ user: this.user,
162
+ port: this.port,
163
+ password: this.password,
164
+ pool: { ...this.pool, idleTimeout }
165
+ });
166
+ }
167
+ toString() {
168
+ let url = `postgres://${this.user}`;
169
+ if (this.password) {
170
+ url += `:${this.password}`;
171
+ }
172
+ url += `@${this.host}:${this.port}/${this.database}`;
173
+ const queryParams = [];
174
+ if (this.pool?.size !== void 0) {
175
+ queryParams.push(`pool_size=${this.pool.size}`);
176
+ }
177
+ if (this.pool?.connectionTimeout !== void 0) {
178
+ queryParams.push(
179
+ `connection_timeout=${this.pool.connectionTimeout}`
180
+ );
181
+ }
182
+ if (this.pool?.idleTimeout !== void 0) {
183
+ queryParams.push(`idle_timeout=${this.pool.idleTimeout}`);
184
+ }
185
+ if (queryParams.length > 0) {
186
+ url += `?${queryParams.join("&")}`;
187
+ }
188
+ return url;
189
+ }
190
+ };
191
+
192
+ // src/helpers.ts
193
+ var ClientWrapper = class {
194
+ client;
195
+ getClient() {
196
+ return this.client;
197
+ }
198
+ constructor(urlOrClient) {
199
+ if (urlOrClient instanceof PostgresConfig || typeof urlOrClient === "string") {
200
+ this.client = new pg.Client({
201
+ connectionString: urlOrClient.toString()
202
+ });
203
+ } else {
204
+ this.client = urlOrClient;
205
+ }
206
+ }
207
+ async withClient(work) {
208
+ await ensureConnection(this.client);
209
+ return work(this.client);
210
+ }
211
+ async query(query, params) {
212
+ const result = await this.withClient(
213
+ (client) => client.query(query, params)
214
+ );
215
+ return result.rows;
216
+ }
217
+ async close() {
218
+ await this.client.end();
219
+ }
220
+ };
221
+ var DatabaseManager = class extends ClientWrapper {
222
+ async createDatabase(name) {
223
+ const exists = await this.query(
224
+ `SELECT 1 FROM pg_database WHERE datname = '${name}'`
225
+ );
226
+ if (exists.length === 0) {
227
+ await this.client.query(`CREATE DATABASE ${name}`);
228
+ }
229
+ }
230
+ async dropDatabase(name) {
231
+ await this.query(`DROP DATABASE IF EXISTS ${name}`);
232
+ }
233
+ };
234
+ var TableManager = class extends ClientWrapper {
235
+ async getTableSchema(tableName) {
236
+ const result = await this.query(`
237
+ SELECT
238
+ column_name AS column,
239
+ data_type AS type
240
+ FROM information_schema.columns
241
+ WHERE table_name = '${tableName}';
242
+ `);
243
+ return result.map((row) => ({
244
+ column: row.column,
245
+ type: row.type
246
+ }));
247
+ }
248
+ async tableExists(tableName) {
249
+ const result = await this.query(`
250
+ SELECT
251
+ table_name
252
+ FROM information_schema.tables
253
+ WHERE table_name = '${tableName}';
254
+ `);
255
+ return result.length > 0;
256
+ }
257
+ async createTable(name, columns) {
258
+ if (await this.tableExists(name)) {
259
+ return;
260
+ }
261
+ const query = `
262
+ CREATE TABLE ${name} (
263
+ ${columns.map((column) => `${column.name} ${column.property}`).join(", ")}
264
+ );
265
+ `;
266
+ await this.query(query);
267
+ }
268
+ async dropTable(name) {
269
+ await this.query(`DROP TABLE IF EXISTS "${name}";`);
270
+ }
271
+ async truncateTable(name) {
272
+ await this.query(`TRUNCATE TABLE "${name}" RESTART IDENTITY CASCADE;`);
273
+ }
274
+ async truncateAllTables() {
275
+ const tables = await this.getTableNames();
276
+ await Promise.all(tables.map((table) => this.truncateTable(table)));
277
+ }
278
+ async dropAllTables() {
279
+ const tables = await this.getTableNames();
280
+ await Promise.all(tables.map((table) => this.dropTable(table)));
281
+ }
282
+ async getTableNames() {
283
+ const result = await this.query(`
284
+ SELECT table_name
285
+ FROM information_schema.tables
286
+ WHERE table_schema = 'public'
287
+ AND table_type = 'BASE TABLE';
288
+ `);
289
+ return result.map((row) => row.table_name);
290
+ }
291
+ };
292
+ async function ensureConnection(client) {
293
+ try {
294
+ await client.connect();
295
+ } catch (e) {
296
+ if (e.message.includes("already")) ; else {
297
+ throw e;
298
+ }
299
+ }
43
300
  }
44
- class PostgresUnitOfWorkForTesting {
45
- client;
46
- executorStorage = new node_async_hooks_1.AsyncLocalStorage();
47
- constructor(client) {
48
- this.client = client;
49
- }
50
- getClient() {
51
- const executor = this.getCurrentExecutor();
52
- if (!executor) {
53
- throw new Error("Unit of work not started");
54
- }
55
- return this.client;
56
- }
57
- async query(fn) {
58
- return fn(this.client);
59
- }
60
- async wrap(fn, options = {}) {
61
- const resolvedOptions = this.resolveOptions(options);
62
- const executor = this.resolveExecutor(resolvedOptions);
63
- return this.executeInContext(executor, (exec) => exec.execute(fn, resolvedOptions));
64
- }
65
- getCurrentExecutor() {
66
- return this.executorStorage.getStore() ?? null;
67
- }
68
- resolveOptions(options) {
69
- return {
70
- propagation: core_1.Propagation.EXISTING,
71
- ...options,
72
- };
73
- }
74
- resolveExecutor(options) {
75
- if (options.propagation === core_1.Propagation.NEW) {
76
- console.warn("[PostgresUnitOfWorkForTesting] Propagation.NEW is not fully supported in testing mode. Using savepoint instead.");
77
- return this.createExecutor();
78
- }
79
- return this.getCurrentExecutor() ?? this.createExecutor();
80
- }
81
- createExecutor() {
82
- return new TestTransactionExecutor(this.client);
83
- }
84
- executeInContext(executor, callback) {
85
- return this.executorStorage.run(executor, () => callback(executor));
86
- }
301
+ function isDatabaseError(e) {
302
+ return e instanceof Error && "code" in e;
87
303
  }
88
- exports.PostgresUnitOfWorkForTesting = PostgresUnitOfWorkForTesting;
89
- class TestTransactionExecutor {
90
- client;
91
- initialized = false;
92
- closed = false;
93
- abortError;
94
- nestingDepth = 0;
95
- savepointCounter = 0;
96
- savepoints = [];
97
- savepointName;
98
- constructor(client) {
99
- this.client = client;
100
- this.savepointName = `test_sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
101
- }
102
- async execute(fn, options) {
103
- await this.ensureStarted();
104
- const executor = this.resolveExecutor(options.propagation);
105
- return executor === this
106
- ? this.runWithLifecycle(fn)
107
- : executor.execute(fn, options);
108
- }
109
- getClient() {
110
- return this.client;
111
- }
112
- async ensureStarted() {
113
- if (this.initialized) {
114
- return;
115
- }
116
- this.initialized = true;
117
- await this.client.query(`SAVEPOINT ${this.savepointName}`);
118
- }
119
- async runWithLifecycle(fn) {
120
- try {
121
- return await this.executeWithNesting(fn);
122
- }
123
- catch (e) {
124
- this.markAsAborted(e);
125
- throw e;
126
- }
127
- finally {
128
- await this.finalizeIfRoot();
129
- }
130
- }
131
- async executeWithNesting(fn) {
132
- this.nestingDepth++;
133
- try {
134
- return await fn(this.client);
135
- }
136
- finally {
137
- this.nestingDepth--;
138
- }
139
- }
140
- markAsAborted(error) {
141
- this.abortError = error;
142
- }
143
- async finalizeIfRoot() {
144
- if (this.nestingDepth === 0) {
145
- await (this.isAborted() ? this.rollback() : this.commit());
146
- }
147
- }
148
- resolveExecutor(propagation) {
149
- if (this.nestingDepth === 0) {
150
- return this;
151
- }
152
- return propagation === core_1.Propagation.NESTED
153
- ? this.createSavepoint()
154
- : (this.findActiveSavepoint() ?? this);
155
- }
156
- createSavepoint() {
157
- this.savepointCounter++;
158
- const savepoint = new TestSavepoint(`${this.savepointName}_nested_${this.savepointCounter}`, this.client, () => this.removeSavepoint());
159
- this.savepoints.push(savepoint);
160
- return savepoint;
161
- }
162
- findActiveSavepoint() {
163
- for (let i = this.savepoints.length - 1; i >= 0; i--) {
164
- if (!this.savepoints[i].isClosed()) {
165
- return this.savepoints[i];
166
- }
167
- }
168
- }
169
- removeSavepoint() {
170
- this.savepoints.pop();
171
- }
172
- async commit() {
173
- if (this.closed) {
174
- return;
175
- }
176
- this.closed = true;
177
- await this.client.query(`RELEASE SAVEPOINT ${this.savepointName}`);
178
- }
179
- async rollback() {
180
- if (this.closed) {
181
- return;
182
- }
183
- this.closed = true;
184
- await this.client.query(`ROLLBACK TO SAVEPOINT ${this.savepointName}`);
185
- }
186
- isAborted() {
187
- return this.abortError !== undefined && !this.closed;
188
- }
304
+ function extractNumericPrefix(filename) {
305
+ const match = filename.match(/^(\d+)/);
306
+ return match ? parseInt(match[1], 10) : 0;
189
307
  }
190
- class TestSavepoint {
191
- name;
192
- client;
193
- onClose;
194
- initialized = false;
195
- closed = false;
196
- abortError;
197
- nestingDepth = 0;
198
- constructor(name, client, onClose) {
199
- this.name = name;
200
- this.client = client;
201
- this.onClose = onClose;
202
- }
203
- async execute(fn) {
204
- await this.ensureStarted();
205
- return this.runWithLifecycle(fn);
206
- }
207
- isClosed() {
208
- return this.closed;
209
- }
210
- async ensureStarted() {
211
- if (this.initialized) {
212
- return;
213
- }
214
- this.initialized = true;
215
- await this.client.query(`SAVEPOINT ${this.name}`);
216
- }
217
- async runWithLifecycle(fn) {
218
- this.nestingDepth++;
308
+ async function ensureTableCompatibility(client, tableName) {
309
+ const tableExists = await client.query(
310
+ `
311
+ SELECT 1 FROM information_schema.tables
312
+ WHERE table_name = $1
313
+ `,
314
+ [tableName]
315
+ );
316
+ if (tableExists.rows.length === 0) return;
317
+ const hasAppliedAt = await client.query(
318
+ `
319
+ SELECT 1 FROM information_schema.columns
320
+ WHERE table_name = $1 AND column_name = 'applied_at'
321
+ `,
322
+ [tableName]
323
+ );
324
+ if (hasAppliedAt.rows.length > 0) {
325
+ await client.query(`
326
+ ALTER TABLE "${tableName}"
327
+ RENAME COLUMN applied_at TO run_on
328
+ `);
329
+ console.log(`Migrated table ${tableName}: applied_at \u2192 run_on`);
330
+ }
331
+ }
332
+ async function isSqlMigrationFormat(dir) {
333
+ try {
334
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
335
+ for (const entry of entries) {
336
+ if (entry.isDirectory()) {
337
+ const sqlPath = path2.join(dir, entry.name, "migration.sql");
219
338
  try {
220
- return await fn(this.client);
339
+ await fs2.access(sqlPath);
340
+ return true;
341
+ } catch {
221
342
  }
222
- catch (e) {
223
- this.markAsAborted(e);
224
- throw e;
225
- }
226
- finally {
227
- this.nestingDepth--;
228
- await this.finalizeIfRoot();
229
- }
230
- }
231
- markAsAborted(error) {
232
- this.abortError = error;
233
- }
234
- async finalizeIfRoot() {
235
- if (this.nestingDepth === 0) {
236
- await (this.isAborted() ? this.rollback() : this.commit());
237
- }
238
- }
239
- async commit() {
240
- if (this.closed) {
241
- return;
242
- }
243
- this.closed = true;
244
- await this.client.query(`RELEASE SAVEPOINT ${this.name}`);
245
- this.onClose();
343
+ }
246
344
  }
247
- async rollback() {
248
- if (this.closed) {
249
- return;
345
+ return false;
346
+ } catch {
347
+ return false;
348
+ }
349
+ }
350
+ async function runSqlMigrations(client, dir, migrationsTable, dryRun) {
351
+ await client.query(`
352
+ CREATE TABLE IF NOT EXISTS "${migrationsTable}" (
353
+ id SERIAL PRIMARY KEY,
354
+ name VARCHAR(255) NOT NULL,
355
+ run_on TIMESTAMP NOT NULL DEFAULT NOW()
356
+ )
357
+ `);
358
+ const appliedResult = await client.query(
359
+ `SELECT name FROM "${migrationsTable}" ORDER BY run_on ASC`
360
+ );
361
+ const appliedMigrations = new Set(appliedResult.rows.map((r) => r.name));
362
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
363
+ const migrationDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => extractNumericPrefix(a) - extractNumericPrefix(b));
364
+ const migrationsToApply = [];
365
+ for (const migrationDir of migrationDirs) {
366
+ if (appliedMigrations.has(migrationDir)) {
367
+ continue;
368
+ }
369
+ const sqlPath = path2.join(dir, migrationDir, "migration.sql");
370
+ try {
371
+ const sql = await fs2.readFile(sqlPath, "utf-8");
372
+ migrationsToApply.push({ name: migrationDir, sql });
373
+ } catch {
374
+ }
375
+ }
376
+ if (migrationsToApply.length === 0) {
377
+ console.log("No migrations to run!");
378
+ return;
379
+ }
380
+ console.log(`> Migrating files:`);
381
+ for (const migration of migrationsToApply) {
382
+ console.log(`> - ${migration.name}`);
383
+ }
384
+ if (dryRun) {
385
+ console.log("Dry run - no migrations applied");
386
+ return;
387
+ }
388
+ for (const migration of migrationsToApply) {
389
+ console.log(`### MIGRATION ${migration.name} (UP) ###`);
390
+ await client.query(migration.sql);
391
+ await client.query(
392
+ `INSERT INTO "${migrationsTable}" (name) VALUES ($1)`,
393
+ [migration.name]
394
+ );
395
+ }
396
+ }
397
+ async function runMigrations({
398
+ namespace,
399
+ url,
400
+ dir,
401
+ direction = "up",
402
+ count,
403
+ dryRun = false
404
+ }) {
405
+ const migrationsTable = `hexai__migrations_${namespace}` ;
406
+ const client = new pg.Client(url);
407
+ try {
408
+ await client.connect();
409
+ await ensureTableCompatibility(client, migrationsTable);
410
+ const isSqlFormat = await isSqlMigrationFormat(dir);
411
+ if (isSqlFormat) {
412
+ await runSqlMigrations(client, dir, migrationsTable, dryRun);
413
+ } else {
414
+ await client.end();
415
+ await runner({
416
+ databaseUrl: url.toString(),
417
+ dir,
418
+ direction,
419
+ count,
420
+ migrationsTable,
421
+ dryRun,
422
+ singleTransaction: true,
423
+ log: (msg) => {
424
+ if (!msg.startsWith("Can't determine timestamp for")) {
425
+ console.log(msg);
426
+ }
250
427
  }
251
- this.closed = true;
252
- await this.client.query(`ROLLBACK TO SAVEPOINT ${this.name}`);
253
- this.onClose();
428
+ });
429
+ return;
254
430
  }
255
- isAborted() {
256
- return this.abortError !== undefined && !this.closed;
431
+ } finally {
432
+ try {
433
+ await client.end();
434
+ } catch {
257
435
  }
436
+ }
258
437
  }
438
+
439
+ // src/run-hexai-migrations.ts
440
+ var __dirname$1 = path2__default.dirname(fileURLToPath(import.meta.url));
441
+ var MIGRATIONS_DIR = path2__default.join(__dirname$1, "../migrations");
442
+ async function runHexaiMigrations(dbUrl) {
443
+ await runMigrations({
444
+ dir: MIGRATIONS_DIR,
445
+ url: dbUrl,
446
+ namespace: "hexai"
447
+ });
448
+ }
449
+
450
+ // src/test.ts
451
+ function createTestContext(dbUrl) {
452
+ const config = typeof dbUrl === "string" ? PostgresConfig.fromUrl(dbUrl) : dbUrl;
453
+ const dbName = config.database;
454
+ const databaseManager = new DatabaseManager(
455
+ config.withDatabase("postgres")
456
+ );
457
+ const tableManager = new TableManager(config);
458
+ async function setup() {
459
+ try {
460
+ await databaseManager.dropDatabase(dbName);
461
+ } catch (e) {
462
+ if (isDatabaseError(e) && e.code === "3D000") ; else {
463
+ throw e;
464
+ }
465
+ }
466
+ await databaseManager.createDatabase(dbName);
467
+ await runHexaiMigrations(config);
468
+ }
469
+ async function teardown() {
470
+ await tableManager.close();
471
+ await databaseManager.dropDatabase(dbName);
472
+ await databaseManager.close();
473
+ }
474
+ return {
475
+ client: tableManager.getClient(),
476
+ newClient: () => new Client(dbUrl),
477
+ tableManager,
478
+ setup,
479
+ teardown
480
+ };
481
+ }
482
+ var PostgresUnitOfWorkForTesting = class {
483
+ constructor(client) {
484
+ this.client = client;
485
+ }
486
+ executorStorage = new AsyncLocalStorage();
487
+ getClient() {
488
+ const executor = this.getCurrentExecutor();
489
+ if (!executor) {
490
+ throw new Error("Unit of work not started");
491
+ }
492
+ return this.client;
493
+ }
494
+ async withClient(fn) {
495
+ return fn(this.client);
496
+ }
497
+ async wrap(fn, options = {}) {
498
+ const resolvedOptions = this.resolveOptions(options);
499
+ const executor = this.resolveExecutor(resolvedOptions);
500
+ return this.executeInContext(
501
+ executor,
502
+ (exec) => exec.execute(fn, resolvedOptions)
503
+ );
504
+ }
505
+ getCurrentExecutor() {
506
+ return this.executorStorage.getStore() ?? null;
507
+ }
508
+ resolveOptions(options) {
509
+ return {
510
+ propagation: Propagation.EXISTING,
511
+ ...options
512
+ };
513
+ }
514
+ resolveExecutor(options) {
515
+ if (options.propagation === Propagation.NEW) {
516
+ console.warn(
517
+ "[PostgresUnitOfWorkForTesting] Propagation.NEW is not fully supported in testing mode. Using savepoint instead."
518
+ );
519
+ return this.createExecutor();
520
+ }
521
+ return this.getCurrentExecutor() ?? this.createExecutor();
522
+ }
523
+ createExecutor() {
524
+ return new TestTransactionExecutor(this.client);
525
+ }
526
+ executeInContext(executor, callback) {
527
+ return this.executorStorage.run(executor, () => callback(executor));
528
+ }
529
+ };
530
+ var TestTransactionExecutor = class {
531
+ constructor(client) {
532
+ this.client = client;
533
+ this.savepointName = `test_sp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
534
+ }
535
+ initialized = false;
536
+ closed = false;
537
+ abortError;
538
+ nestingDepth = 0;
539
+ savepointCounter = 0;
540
+ savepoints = [];
541
+ savepointName;
542
+ async execute(fn, options) {
543
+ await this.ensureStarted();
544
+ const executor = this.resolveExecutor(options.propagation);
545
+ return executor === this ? this.runWithLifecycle(fn) : executor.execute(fn, options);
546
+ }
547
+ getClient() {
548
+ return this.client;
549
+ }
550
+ async ensureStarted() {
551
+ if (this.initialized) {
552
+ return;
553
+ }
554
+ this.initialized = true;
555
+ await this.client.query(`SAVEPOINT ${this.savepointName}`);
556
+ }
557
+ async runWithLifecycle(fn) {
558
+ try {
559
+ return await this.executeWithNesting(fn);
560
+ } catch (e) {
561
+ this.markAsAborted(e);
562
+ throw e;
563
+ } finally {
564
+ await this.finalizeIfRoot();
565
+ }
566
+ }
567
+ async executeWithNesting(fn) {
568
+ this.nestingDepth++;
569
+ try {
570
+ return await fn(this.client);
571
+ } finally {
572
+ this.nestingDepth--;
573
+ }
574
+ }
575
+ markAsAborted(error) {
576
+ this.abortError = error;
577
+ }
578
+ async finalizeIfRoot() {
579
+ if (this.nestingDepth === 0) {
580
+ await (this.isAborted() ? this.rollback() : this.commit());
581
+ }
582
+ }
583
+ resolveExecutor(propagation) {
584
+ if (this.nestingDepth === 0) {
585
+ return this;
586
+ }
587
+ return propagation === Propagation.NESTED ? this.createSavepoint() : this.findActiveSavepoint() ?? this;
588
+ }
589
+ createSavepoint() {
590
+ this.savepointCounter++;
591
+ const savepoint = new TestSavepoint(
592
+ `${this.savepointName}_nested_${this.savepointCounter}`,
593
+ this.client,
594
+ () => this.removeSavepoint()
595
+ );
596
+ this.savepoints.push(savepoint);
597
+ return savepoint;
598
+ }
599
+ findActiveSavepoint() {
600
+ for (let i = this.savepoints.length - 1; i >= 0; i--) {
601
+ if (!this.savepoints[i].isClosed()) {
602
+ return this.savepoints[i];
603
+ }
604
+ }
605
+ }
606
+ removeSavepoint() {
607
+ this.savepoints.pop();
608
+ }
609
+ async commit() {
610
+ if (this.closed) {
611
+ return;
612
+ }
613
+ this.closed = true;
614
+ await this.client.query(`RELEASE SAVEPOINT ${this.savepointName}`);
615
+ }
616
+ async rollback() {
617
+ if (this.closed) {
618
+ return;
619
+ }
620
+ this.closed = true;
621
+ await this.client.query(
622
+ `ROLLBACK TO SAVEPOINT ${this.savepointName}`
623
+ );
624
+ }
625
+ isAborted() {
626
+ return this.abortError !== void 0 && !this.closed;
627
+ }
628
+ };
629
+ var TestSavepoint = class {
630
+ constructor(name, client, onClose) {
631
+ this.name = name;
632
+ this.client = client;
633
+ this.onClose = onClose;
634
+ }
635
+ initialized = false;
636
+ closed = false;
637
+ abortError;
638
+ nestingDepth = 0;
639
+ async execute(fn) {
640
+ await this.ensureStarted();
641
+ return this.runWithLifecycle(fn);
642
+ }
643
+ isClosed() {
644
+ return this.closed;
645
+ }
646
+ async ensureStarted() {
647
+ if (this.initialized) {
648
+ return;
649
+ }
650
+ this.initialized = true;
651
+ await this.client.query(`SAVEPOINT ${this.name}`);
652
+ }
653
+ async runWithLifecycle(fn) {
654
+ this.nestingDepth++;
655
+ try {
656
+ return await fn(this.client);
657
+ } catch (e) {
658
+ this.markAsAborted(e);
659
+ throw e;
660
+ } finally {
661
+ this.nestingDepth--;
662
+ await this.finalizeIfRoot();
663
+ }
664
+ }
665
+ markAsAborted(error) {
666
+ this.abortError = error;
667
+ }
668
+ async finalizeIfRoot() {
669
+ if (this.nestingDepth === 0) {
670
+ await (this.isAborted() ? this.rollback() : this.commit());
671
+ }
672
+ }
673
+ async commit() {
674
+ if (this.closed) {
675
+ return;
676
+ }
677
+ this.closed = true;
678
+ await this.client.query(`RELEASE SAVEPOINT ${this.name}`);
679
+ this.onClose();
680
+ }
681
+ async rollback() {
682
+ if (this.closed) {
683
+ return;
684
+ }
685
+ this.closed = true;
686
+ await this.client.query(`ROLLBACK TO SAVEPOINT ${this.name}`);
687
+ this.onClose();
688
+ }
689
+ isAborted() {
690
+ return this.abortError !== void 0 && !this.closed;
691
+ }
692
+ };
693
+
694
+ export { PostgresUnitOfWorkForTesting, createTestContext };
695
+ //# sourceMappingURL=test.js.map
259
696
  //# sourceMappingURL=test.js.map