@hexaijs/postgres 0.4.0 → 0.5.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/dist/helpers-vPAudN_S.d.ts +125 -0
- package/dist/index.d.ts +64 -8
- package/dist/index.js +828 -29
- package/dist/index.js.map +1 -1
- package/dist/test.d.ts +9 -8
- package/dist/test.js +683 -246
- package/dist/test.js.map +1 -1
- package/package.json +7 -7
- package/dist/config/index.d.ts +0 -3
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js +0 -19
- package/dist/config/index.js.map +0 -1
- package/dist/config/postgres-config-spec.d.ts +0 -32
- package/dist/config/postgres-config-spec.d.ts.map +0 -1
- package/dist/config/postgres-config-spec.js +0 -49
- package/dist/config/postgres-config-spec.js.map +0 -1
- package/dist/config/postgres-config.d.ts +0 -59
- package/dist/config/postgres-config.d.ts.map +0 -1
- package/dist/config/postgres-config.js +0 -181
- package/dist/config/postgres-config.js.map +0 -1
- package/dist/helpers.d.ts +0 -57
- package/dist/helpers.d.ts.map +0 -1
- package/dist/helpers.js +0 -276
- package/dist/helpers.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/postgres-event-store.d.ts +0 -18
- package/dist/postgres-event-store.d.ts.map +0 -1
- package/dist/postgres-event-store.js +0 -83
- package/dist/postgres-event-store.js.map +0 -1
- package/dist/postgres-unit-of-work.d.ts +0 -24
- package/dist/postgres-unit-of-work.d.ts.map +0 -1
- package/dist/postgres-unit-of-work.js +0 -308
- package/dist/postgres-unit-of-work.js.map +0 -1
- package/dist/run-hexai-migrations.d.ts +0 -3
- package/dist/run-hexai-migrations.d.ts.map +0 -1
- package/dist/run-hexai-migrations.js +0 -17
- package/dist/run-hexai-migrations.js.map +0 -1
- package/dist/run-migrations.d.ts +0 -11
- package/dist/run-migrations.d.ts.map +0 -1
- package/dist/run-migrations.js +0 -202
- package/dist/run-migrations.js.map +0 -1
- package/dist/test-fixtures/config.d.ts +0 -5
- package/dist/test-fixtures/config.d.ts.map +0 -1
- package/dist/test-fixtures/config.js +0 -14
- package/dist/test-fixtures/config.js.map +0 -1
- package/dist/test-fixtures/hooks.d.ts +0 -8
- package/dist/test-fixtures/hooks.d.ts.map +0 -1
- package/dist/test-fixtures/hooks.js +0 -77
- package/dist/test-fixtures/hooks.js.map +0 -1
- package/dist/test-fixtures/index.d.ts +0 -3
- package/dist/test-fixtures/index.d.ts.map +0 -1
- package/dist/test-fixtures/index.js +0 -19
- package/dist/test-fixtures/index.js.map +0 -1
- package/dist/test.d.ts.map +0 -1
- package/dist/types.d.ts +0 -14
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -11
- package/dist/types.js.map +0 -1
package/dist/test.js
CHANGED
|
@@ -1,259 +1,696 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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 withClient(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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
339
|
+
await fs2.access(sqlPath);
|
|
340
|
+
return true;
|
|
341
|
+
} catch {
|
|
221
342
|
}
|
|
222
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
this.onClose();
|
|
428
|
+
});
|
|
429
|
+
return;
|
|
254
430
|
}
|
|
255
|
-
|
|
256
|
-
|
|
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
|