@tstdl/base 0.93.144 → 0.93.146

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 (45) hide show
  1. package/authentication/authentication.api.d.ts +9 -0
  2. package/authentication/authentication.api.js +3 -0
  3. package/authentication/client/authentication.service.js +5 -5
  4. package/authentication/client/http-client.middleware.js +6 -2
  5. package/authentication/tests/authentication.client-middleware.test.js +35 -0
  6. package/authentication/tests/authentication.client-service-refresh.test.js +7 -0
  7. package/authentication/tests/authentication.client-service.test.js +15 -19
  8. package/authentication/tests/authentication.service.test.js +92 -119
  9. package/notification/tests/notification-client.test.js +39 -50
  10. package/notification/tests/notification-flow.test.js +204 -238
  11. package/notification/tests/notification-sse.service.test.js +20 -27
  12. package/notification/tests/notification-type.service.test.js +17 -20
  13. package/orm/tests/query-complex.test.js +80 -111
  14. package/orm/tests/repository-advanced.test.js +100 -143
  15. package/orm/tests/repository-attributes.test.js +30 -39
  16. package/orm/tests/repository-compound-primary-key.test.js +67 -75
  17. package/orm/tests/repository-comprehensive.test.js +76 -101
  18. package/orm/tests/repository-coverage.test.d.ts +1 -0
  19. package/orm/tests/repository-coverage.test.js +88 -149
  20. package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
  21. package/orm/tests/repository-cti-extensive.test.js +118 -147
  22. package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
  23. package/orm/tests/repository-cti-mapping.test.js +29 -42
  24. package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
  25. package/orm/tests/repository-cti-soft-delete.test.js +25 -37
  26. package/orm/tests/repository-cti-transactions.test.js +19 -33
  27. package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
  28. package/orm/tests/repository-cti-upsert-many.test.js +38 -50
  29. package/orm/tests/repository-cti.test.d.ts +1 -0
  30. package/orm/tests/repository-cti.test.js +195 -247
  31. package/orm/tests/repository-expiration.test.d.ts +1 -0
  32. package/orm/tests/repository-expiration.test.js +46 -59
  33. package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
  34. package/orm/tests/repository-extra-coverage.test.js +195 -337
  35. package/orm/tests/repository-mapping.test.d.ts +1 -0
  36. package/orm/tests/repository-mapping.test.js +20 -20
  37. package/orm/tests/repository-regression.test.js +124 -163
  38. package/orm/tests/repository-search.test.js +30 -44
  39. package/orm/tests/repository-soft-delete.test.js +54 -79
  40. package/orm/tests/repository-types.test.js +77 -111
  41. package/package.json +1 -1
  42. package/task-queue/tests/worker.test.js +5 -5
  43. package/testing/README.md +38 -16
  44. package/testing/integration-setup.d.ts +11 -0
  45. package/testing/integration-setup.js +57 -30
@@ -1,5 +1,6 @@
1
+ /** biome-ignore-all lint/nursery/useExpect: helper file */
1
2
  import { sql } from 'drizzle-orm';
2
- import { afterAll } from 'vitest';
3
+ import { afterAll, test } from 'vitest';
3
4
  import { configureApiServer } from '../api/server/index.js';
4
5
  import { configureAudit } from '../audit/index.js';
5
6
  import { AuthenticationApiClient } from '../authentication/client/api.client.js';
@@ -26,7 +27,8 @@ import { configureDefaultSignalsImplementation } from '../signals/implementation
26
27
  import { configurePostgresTaskQueue } from '../task-queue/postgres/index.js';
27
28
  import * as configParser from '../utils/config-parser.js';
28
29
  import { objectEntries } from '../utils/object/object.js';
29
- import { assertDefinedPass, isDefined, isNotNull, isString } from '../utils/type-guards.js';
30
+ import { assertDefinedPass, isDefined, isNotNull, isNumber, isString, isUndefined } from '../utils/type-guards.js';
31
+ import { resolveValueOrProvider } from '../utils/value-or-provider.js';
30
32
  /**
31
33
  * Standard setup for integration tests.
32
34
  */
@@ -167,16 +169,22 @@ export async function setupIntegrationTest(options = {}) {
167
169
  return { injector, database };
168
170
  }
169
171
  /**
170
- * Helper to run a migration safely with a database advisory lock to prevent race conditions.
172
+ * A wrapper for vitest's `test` that automatically runs the test function in the provided injector's context.
173
+ * @param name The name of the test.
174
+ * @param injector The injector to use for the context.
175
+ * @param fn The test function.
176
+ * @param options Vitest test options.
171
177
  */
172
- async function runMigrationSafely(database, migration) {
173
- const lockId = 123456789; // Fixed lock ID for migrations
174
- await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
175
- try {
176
- await migration();
178
+ export function testInInjector(name, injector, fn, options) {
179
+ if (isUndefined(options) || isNumber(options)) {
180
+ test(name, async () => {
181
+ await runInInjectionContext(resolveValueOrProvider(injector), fn);
182
+ }, options);
177
183
  }
178
- finally {
179
- await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
184
+ else {
185
+ test(name, options, async () => {
186
+ await runInInjectionContext(resolveValueOrProvider(injector), fn);
187
+ });
180
188
  }
181
189
  }
182
190
  /**
@@ -187,19 +195,15 @@ export async function truncateTables(database, schema, tables) {
187
195
  if (tables.length == 0) {
188
196
  return;
189
197
  }
190
- const lockId = 987654321; // Different lock ID for table maintenance
191
- await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
192
- try {
198
+ const lockName = `tstdl:maintenance:${schema}`;
199
+ await runWithAdvisoryLock(database, lockName, async () => {
193
200
  // Using CASCADE to handle foreign keys automatically
194
201
  for (const table of tables) {
195
202
  const tableName = isString(table) ? table : getEntityTableName(table);
196
203
  const tableSchema = isString(table) ? schema : getEntitySchema(table, schema);
197
204
  await database.execute(sql `TRUNCATE TABLE ${sql.identifier(tableSchema)}.${sql.identifier(tableName)} CASCADE`);
198
205
  }
199
- }
200
- finally {
201
- await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
202
- }
206
+ });
203
207
  }
204
208
  /**
205
209
  * Helper to delete data for a specific tenant from specific tables.
@@ -209,18 +213,15 @@ export async function clearTenantData(database, schema, tables, tenantId) {
209
213
  if (tables.length == 0) {
210
214
  return;
211
215
  }
212
- const lockId = 987654321;
213
- await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
214
- try {
216
+ // Use a lock that is specific to both the schema AND the tenant
217
+ const lockName = `tstdl:maintenance:${schema ?? 'default'}:${tenantId}`;
218
+ await runWithAdvisoryLock(database, lockName, async () => {
215
219
  for (const table of tables) {
216
220
  const tableName = isString(table) ? table : getEntityTableName(table);
217
221
  const tableSchema = isString(table) ? assertDefinedPass(schema, 'Schema not provided') : getEntitySchema(table, schema);
218
222
  await database.execute(sql `DELETE FROM ${sql.identifier(tableSchema)}.${sql.identifier(tableName)} WHERE tenant_id = ${tenantId}`);
219
223
  }
220
- }
221
- finally {
222
- await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
223
- }
224
+ });
224
225
  }
225
226
  /**
226
227
  * Helper to drop specific tables.
@@ -230,16 +231,42 @@ export async function dropTables(database, schema, tables) {
230
231
  if (tables.length == 0) {
231
232
  return;
232
233
  }
233
- const lockId = 987654321;
234
- await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
235
- try {
234
+ const lockName = `tstdl:maintenance:${schema ?? 'default'}`;
235
+ await runWithAdvisoryLock(database, lockName, async () => {
236
236
  for (const table of tables) {
237
237
  const tableName = isString(table) ? table : getEntityTableName(table);
238
238
  const tableSchema = isString(table) ? assertDefinedPass(schema, 'Schema not provided') : getEntitySchema(table, schema);
239
239
  await database.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(tableSchema)}.${sql.identifier(tableName)} CASCADE`);
240
240
  }
241
- }
242
- finally {
243
- await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
241
+ });
242
+ }
243
+ /**
244
+ * Helper to run a migration safely with a database advisory lock to prevent race conditions.
245
+ */
246
+ async function runMigrationSafely(database, migration) {
247
+ const lockName = 'tstdl:migration';
248
+ await runWithAdvisoryLock(database, lockName, migration);
249
+ }
250
+ async function runWithAdvisoryLock(database, lockName, fn) {
251
+ // We use pg_try_advisory_lock in a loop with small random delays to avoid deadlocks
252
+ // which sometimes happen with pg_advisory_lock under very high parallelism in Vitest
253
+ const start = Date.now();
254
+ const timeout = 30000; // 30s timeout for maintenance tasks
255
+ while (true) {
256
+ const { rows: [result] } = await database.execute(sql `SELECT pg_try_advisory_lock(hashtext(${lockName})) as locked`);
257
+ if (result?.locked) {
258
+ try {
259
+ await fn();
260
+ return;
261
+ }
262
+ finally {
263
+ await database.execute(sql `SELECT pg_advisory_unlock(hashtext(${lockName}))`);
264
+ }
265
+ }
266
+ if (Date.now() - start > timeout) {
267
+ throw new Error(`Timeout while waiting for advisory lock: ${lockName}`);
268
+ }
269
+ // Small random delay between 10ms and 100ms
270
+ await new Promise((resolve) => setTimeout(resolve, 10 + Math.random() * 90));
244
271
  }
245
272
  }