@tstdl/base 0.93.140 → 0.93.142

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 (123) hide show
  1. package/application/application.d.ts +1 -1
  2. package/application/application.js +1 -1
  3. package/application/providers.d.ts +20 -2
  4. package/application/providers.js +34 -7
  5. package/audit/module.d.ts +5 -0
  6. package/audit/module.js +9 -1
  7. package/authentication/client/authentication.service.d.ts +1 -0
  8. package/authentication/client/authentication.service.js +3 -2
  9. package/authentication/server/module.d.ts +5 -0
  10. package/authentication/server/module.js +9 -1
  11. package/authentication/tests/authentication.api-controller.test.js +1 -1
  12. package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
  13. package/authentication/tests/authentication.client-service.test.js +1 -1
  14. package/circuit-breaker/circuit-breaker.d.ts +6 -4
  15. package/circuit-breaker/postgres/circuit-breaker.d.ts +1 -0
  16. package/circuit-breaker/postgres/circuit-breaker.js +8 -5
  17. package/circuit-breaker/postgres/module.d.ts +1 -0
  18. package/circuit-breaker/postgres/module.js +5 -1
  19. package/circuit-breaker/tests/circuit-breaker.test.js +20 -0
  20. package/document-management/server/configure.js +5 -1
  21. package/document-management/server/module.d.ts +1 -1
  22. package/document-management/server/module.js +1 -1
  23. package/document-management/server/services/document-management-ancillary.service.js +1 -1
  24. package/document-management/tests/ai-config-hierarchy.test.js +0 -5
  25. package/document-management/tests/document-management-ai-overrides.test.js +0 -1
  26. package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
  27. package/examples/document-management/main.d.ts +1 -0
  28. package/examples/document-management/main.js +14 -11
  29. package/key-value-store/postgres/module.d.ts +1 -0
  30. package/key-value-store/postgres/module.js +5 -1
  31. package/lock/postgres/module.d.ts +1 -0
  32. package/lock/postgres/module.js +5 -1
  33. package/mail/module.d.ts +5 -1
  34. package/mail/module.js +11 -6
  35. package/module/modules/web-server.module.js +2 -3
  36. package/notification/server/module.d.ts +1 -0
  37. package/notification/server/module.js +5 -1
  38. package/notification/tests/notification-api.test.js +5 -1
  39. package/notification/tests/notification-flow.test.js +8 -5
  40. package/orm/decorators.d.ts +22 -5
  41. package/orm/decorators.js +10 -1
  42. package/orm/server/bootstrap.d.ts +11 -0
  43. package/orm/server/bootstrap.js +31 -0
  44. package/orm/server/drizzle/schema-converter.d.ts +3 -1
  45. package/orm/server/drizzle/schema-converter.js +85 -56
  46. package/orm/server/encryption.d.ts +0 -1
  47. package/orm/server/encryption.js +1 -4
  48. package/orm/server/extension.d.ts +14 -0
  49. package/orm/server/extension.js +27 -0
  50. package/orm/server/index.d.ts +3 -6
  51. package/orm/server/index.js +3 -6
  52. package/orm/server/migration.d.ts +18 -0
  53. package/orm/server/migration.js +58 -0
  54. package/orm/server/repository.d.ts +2 -1
  55. package/orm/server/repository.js +19 -9
  56. package/orm/server/transaction.d.ts +6 -10
  57. package/orm/server/transaction.js +25 -26
  58. package/orm/server/transactional.js +3 -3
  59. package/orm/tests/database-extension.test.js +63 -0
  60. package/orm/tests/database-migration.test.js +83 -0
  61. package/orm/tests/encryption.test.js +3 -4
  62. package/orm/tests/repository-compound-primary-key.test.d.ts +2 -0
  63. package/orm/tests/repository-compound-primary-key.test.js +234 -0
  64. package/orm/tests/schema-generation.test.d.ts +1 -0
  65. package/orm/tests/schema-generation.test.js +52 -5
  66. package/orm/utils.d.ts +17 -2
  67. package/orm/utils.js +49 -1
  68. package/package.json +5 -4
  69. package/rate-limit/postgres/module.d.ts +1 -0
  70. package/rate-limit/postgres/module.js +5 -1
  71. package/reflection/decorator-data.js +11 -12
  72. package/task-queue/README.md +2 -10
  73. package/task-queue/postgres/drizzle/0000_great_gwen_stacy.sql +84 -0
  74. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +250 -89
  75. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  76. package/task-queue/postgres/module.d.ts +1 -0
  77. package/task-queue/postgres/module.js +6 -1
  78. package/task-queue/postgres/schemas.d.ts +15 -6
  79. package/task-queue/postgres/schemas.js +4 -3
  80. package/task-queue/postgres/task-queue.d.ts +18 -15
  81. package/task-queue/postgres/task-queue.js +797 -499
  82. package/task-queue/postgres/task.model.d.ts +20 -9
  83. package/task-queue/postgres/task.model.js +65 -39
  84. package/task-queue/task-context.d.ts +12 -7
  85. package/task-queue/task-context.js +8 -6
  86. package/task-queue/task-queue.d.ts +364 -43
  87. package/task-queue/task-queue.js +153 -41
  88. package/task-queue/tests/coverage-branch.test.d.ts +1 -0
  89. package/task-queue/tests/coverage-branch.test.js +395 -0
  90. package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
  91. package/task-queue/tests/coverage-enhancement.test.js +150 -0
  92. package/task-queue/tests/dag.test.d.ts +1 -0
  93. package/task-queue/tests/dag.test.js +188 -0
  94. package/task-queue/tests/dependencies.test.js +165 -47
  95. package/task-queue/tests/enqueue-batch.test.d.ts +1 -0
  96. package/task-queue/tests/enqueue-batch.test.js +125 -0
  97. package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
  98. package/task-queue/tests/fan-out-spawning.test.js +94 -0
  99. package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
  100. package/task-queue/tests/idempotent-replacement.test.js +114 -0
  101. package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
  102. package/task-queue/tests/missing-idempotent-tasks.test.js +39 -0
  103. package/task-queue/tests/queue.test.js +294 -49
  104. package/task-queue/tests/shutdown.test.d.ts +1 -0
  105. package/task-queue/tests/shutdown.test.js +41 -0
  106. package/task-queue/tests/transactions.test.d.ts +1 -0
  107. package/task-queue/tests/transactions.test.js +47 -0
  108. package/task-queue/tests/worker.test.js +63 -15
  109. package/task-queue/tests/zombie-parent.test.d.ts +1 -0
  110. package/task-queue/tests/zombie-parent.test.js +45 -0
  111. package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
  112. package/task-queue/tests/zombie-recovery.test.js +51 -0
  113. package/test5.js +5 -5
  114. package/testing/integration-setup.d.ts +4 -4
  115. package/testing/integration-setup.js +56 -29
  116. package/text/localization.service.js +2 -2
  117. package/utils/file-reader.js +1 -2
  118. package/utils/timing.d.ts +2 -2
  119. package/task-queue/postgres/drizzle/0000_simple_invisible_woman.sql +0 -74
  120. package/task-queue/tests/complex.test.js +0 -306
  121. package/task-queue/tests/extensive-dependencies.test.js +0 -234
  122. /package/{task-queue/tests/complex.test.d.ts → orm/tests/database-extension.test.d.ts} +0 -0
  123. /package/{task-queue/tests/extensive-dependencies.test.d.ts → orm/tests/database-migration.test.d.ts} +0 -0
@@ -1,18 +1,23 @@
1
1
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
2
  import { CancellationToken } from '../../cancellation/index.js';
3
+ import { runInInjectionContext } from '../../injector/index.js';
4
+ import { injectRepository } from '../../orm/server/index.js';
3
5
  import { TaskProcessResult, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
6
+ import { PostgresTask } from '../../task-queue/postgres/index.js';
4
7
  import { setupIntegrationTest } from '../../testing/index.js';
5
8
  import { timeout } from '../../utils/timing.js';
6
9
  describe('Worker & Base Class Tests', () => {
7
10
  let injector;
8
11
  let queue;
9
12
  let token;
13
+ let otherQueueName;
10
14
  beforeAll(async () => {
11
15
  ({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
16
+ otherQueueName = `other-queue-${crypto.randomUUID()}`;
12
17
  });
13
18
  beforeEach(() => {
14
19
  const queueProvider = injector.resolve(TaskQueueProvider);
15
- const queueName = `worker-queue-${Date.now()}-${Math.random()}`;
20
+ const queueName = `worker-queue-${crypto.randomUUID()}`;
16
21
  queue = queueProvider.get(queueName, {
17
22
  visibilityTimeout: 200, // Short visibility for testing lease loss
18
23
  });
@@ -20,9 +25,14 @@ describe('Worker & Base Class Tests', () => {
20
25
  });
21
26
  afterEach(async () => {
22
27
  token.set();
28
+ const repository = runInInjectionContext(injector, () => injectRepository(PostgresTask));
29
+ const namespace = queue.getTransactionalContextData().namespace;
30
+ // Clear foreign keys
31
+ await repository.updateManyByQuery({ namespace }, { parentId: null });
32
+ await repository.updateManyByQuery({ namespace: otherQueueName }, { parentId: null });
23
33
  await queue.clear();
24
34
  const queueProvider = injector.resolve(TaskQueueProvider);
25
- await queueProvider.get('other-queue').clear();
35
+ await queueProvider.get(otherQueueName).clear();
26
36
  });
27
37
  afterAll(async () => {
28
38
  await injector?.dispose();
@@ -39,6 +49,7 @@ describe('Worker & Base Class Tests', () => {
39
49
  for (let i = 0; i < 50; i++) {
40
50
  if (processed.length == 2)
41
51
  break;
52
+ queue.notify();
42
53
  await timeout(20);
43
54
  }
44
55
  token.set(); // Stop worker
@@ -56,17 +67,18 @@ describe('Worker & Base Class Tests', () => {
56
67
  queue.process({ cancellationSignal: token }, async () => {
57
68
  throw new Error('worker error');
58
69
  });
59
- // Wait until task is processed (error recorded and status is Pending)
70
+ // Wait until task is processed (error recorded and status is Retrying)
60
71
  for (let i = 0; i < 50; i++) {
61
72
  const updated = await queue.getTask(task.id);
62
- if (updated?.tries == 1) {
73
+ if (updated?.tries == 1 && updated.status == TaskStatus.Retrying) {
63
74
  break;
64
75
  }
76
+ queue.notify();
65
77
  await timeout(20);
66
78
  }
67
79
  token.set();
68
80
  const updated = await queue.getTask(task.id);
69
- expect(updated?.status).toBe(TaskStatus.Pending); // Should retry
81
+ expect(updated?.status).toBe(TaskStatus.Retrying); // Should retry
70
82
  expect(updated?.tries).toBe(1);
71
83
  expect(updated?.error?.message).toBe('worker error');
72
84
  });
@@ -79,6 +91,7 @@ describe('Worker & Base Class Tests', () => {
79
91
  executed = true;
80
92
  return TaskProcessResult.Complete();
81
93
  });
94
+ queue.notify();
82
95
  await queue.waitForTasks([task.id], { timeout: 5000 });
83
96
  token.set();
84
97
  expect(executed).toBe(true);
@@ -99,18 +112,19 @@ describe('Worker & Base Class Tests', () => {
99
112
  }
100
113
  return TaskProcessResult.Complete();
101
114
  });
102
- // Wait until tasks are processed (error/reschedule recorded and status is Pending)
115
+ // Wait until tasks are processed (error/reschedule recorded and status is Retrying/Pending)
103
116
  for (let i = 0; i < 50; i++) {
104
117
  const uFail = await queue.getTask(tFail.id);
105
118
  const uResched = await queue.getTask(tResched.id);
106
- if (uFail?.tries == 1 && uResched?.status == TaskStatus.Pending && (uResched?.scheduleTimestamp ?? 0) > Date.now()) {
119
+ if (uFail?.tries == 1 && uFail.status == TaskStatus.Retrying && uResched?.status == TaskStatus.Pending && (uResched?.scheduleTimestamp ?? 0) > Date.now()) {
107
120
  break;
108
121
  }
122
+ queue.notify();
109
123
  await timeout(20);
110
124
  }
111
125
  token.set();
112
126
  const uFail = await queue.getTask(tFail.id);
113
- expect(uFail?.status).toBe(TaskStatus.Pending); // Retry
127
+ expect(uFail?.status).toBe(TaskStatus.Retrying); // Retry
114
128
  expect(uFail?.error?.message).toBe('explicit fail');
115
129
  const uResched = await queue.getTask(tResched.id);
116
130
  expect(uResched?.status).toBe(TaskStatus.Pending);
@@ -132,22 +146,48 @@ describe('Worker & Base Class Tests', () => {
132
146
  const children = await context.spawnMany([{ type: 'child', data: { c: 2 } }]);
133
147
  expect(children[0]?.parentId).toBe(task.id);
134
148
  // Other queue spawn
135
- const otherQueue = injector.resolve(TaskQueueProvider).get('other-queue');
149
+ const otherQueue = injector.resolve(TaskQueueProvider).get(otherQueueName);
136
150
  const otherChild = await context.spawn(otherQueue, 'other', { x: 1 });
137
- expect(otherChild.parentId).toBe(task.id);
138
- expect(otherChild.namespace).toBe('other-queue');
139
- const otherChildren = await context.spawnMany(otherQueue, [{ type: 'other', data: { x: 2 } }]);
140
- expect(otherChildren[0]?.parentId).toBe(task.id);
151
+ expect(otherChild.namespace).toBe(otherQueueName);
152
+ await context.spawnMany(otherQueue, [{ type: 'other', data: { x: 2 } }]);
141
153
  executed = true;
142
154
  return TaskProcessResult.Complete();
143
155
  });
144
- await queue.waitForTasks([task.id], { interval: 50, timeout: 1000 });
156
+ // Complete children so parent can finalize
157
+ void (async () => {
158
+ while (!executed) {
159
+ await timeout(50);
160
+ queue.notify();
161
+ }
162
+ // At this point parent should be WaitingChildren if children are not done
163
+ await queue.waitForTasks([task.id], { statuses: [TaskStatus.WaitingChildren], timeout: 2000 });
164
+ const midTask = await queue.getTask(task.id);
165
+ expect(midTask?.status).toBe(TaskStatus.WaitingChildren);
166
+ while (true) {
167
+ const dChild = await queue.dequeue({ types: ['child'] });
168
+ if (!dChild) {
169
+ // Check other-queue as well
170
+ const otherQueue = injector.resolve(TaskQueueProvider).get(otherQueueName);
171
+ const dOther = await otherQueue.dequeue();
172
+ if (dOther) {
173
+ await otherQueue.complete(dOther);
174
+ continue;
175
+ }
176
+ break;
177
+ }
178
+ await queue.complete(dChild);
179
+ }
180
+ })();
181
+ queue.notify();
182
+ await queue.waitForTasks([task.id], { interval: 50, timeout: 2000 });
145
183
  token.set();
146
184
  expect(executed).toBe(true);
185
+ const finalTask = await queue.getTask(task.id);
186
+ expect(finalTask?.status).toBe(TaskStatus.Completed);
147
187
  });
148
188
  it('should correctly report isFinalAttempt in TaskContext', async () => {
149
189
  const queueProvider = injector.resolve(TaskQueueProvider);
150
- const queueName = `final-try-test-${Date.now()}`;
190
+ const queueName = `final-try-test-${crypto.randomUUID()}`;
151
191
  const testQueue = queueProvider.get(queueName, {
152
192
  maxTries: 2,
153
193
  retryDelayMinimum: 0,
@@ -173,4 +213,12 @@ describe('Worker & Base Class Tests', () => {
173
213
  expect(finalAttemptValues).toEqual([false, true]);
174
214
  await testQueue.clear();
175
215
  });
216
+ it('should handle unsupported task result action', async () => {
217
+ const task = await queue.enqueue('unsupported-action', {});
218
+ const workerPromise = queue.processWorker(token, () => ({ payload: { action: 'magic' } }));
219
+ await expect(workerPromise).rejects.toThrow('Unsupported task result action');
220
+ const updated = await queue.getTask(task.id);
221
+ expect(updated?.status).toBe(TaskStatus.Retrying);
222
+ expect(updated?.error?.message).toContain('Unsupported task result action');
223
+ });
176
224
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+ import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
3
+ import { setupIntegrationTest } from '../../testing/index.js';
4
+ import { timeout } from '../../utils/timing.js';
5
+ describe('Zombie Parent Deadlock', () => {
6
+ let injector;
7
+ let queue;
8
+ beforeAll(async () => {
9
+ ({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
10
+ });
11
+ beforeEach(() => {
12
+ const queueProvider = injector.resolve(TaskQueueProvider);
13
+ const queueName = `zombie-queue-${crypto.randomUUID()}`;
14
+ queue = queueProvider.get(queueName, {
15
+ visibilityTimeout: 1000,
16
+ });
17
+ });
18
+ afterEach(async () => {
19
+ await queue.clear();
20
+ });
21
+ afterAll(async () => {
22
+ await injector?.dispose();
23
+ });
24
+ it('should resolve parent even if child fails (failFast: false)', async () => {
25
+ const parent = await queue.enqueue('parent', {});
26
+ const dParent = await queue.dequeue();
27
+ // Spawn a child that will fail. Parent has failFast: false by default.
28
+ const [child] = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, failFast: false }], { returnTasks: true });
29
+ await queue.complete(dParent);
30
+ // Parent should be WaitingChildren
31
+ const uParent = await queue.getTask(parent.id);
32
+ expect(uParent?.status).toBe(TaskStatus.WaitingChildren);
33
+ // Fail the child fatally
34
+ const dChild = await queue.dequeue();
35
+ await queue.fail(dChild, new Error('child failed'), { fatal: true });
36
+ // Verify child is Dead
37
+ const uChild = await queue.getTask(child.id);
38
+ expect(uChild?.status).toBe(TaskStatus.Dead);
39
+ // Wait a bit for dependency resolution
40
+ await timeout(200);
41
+ // Parent should NOT be stuck. It should transition to Completed because the child is terminal.
42
+ const fParent = await queue.getTask(parent.id);
43
+ expect(fParent?.status).toBe(TaskStatus.Completed);
44
+ });
45
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+ import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
3
+ import { setupIntegrationTest } from '../../testing/index.js';
4
+ import { timeout } from '../../utils/timing.js';
5
+ describe('Zombie Recovery Race Condition', () => {
6
+ let injector;
7
+ let queue;
8
+ beforeAll(async () => {
9
+ ({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
10
+ });
11
+ beforeEach(() => {
12
+ const queueProvider = injector.resolve(TaskQueueProvider);
13
+ const queueName = `zombie-recovery-queue-${crypto.randomUUID()}`;
14
+ queue = queueProvider.get(queueName, {
15
+ visibilityTimeout: 100, // Very short visibility timeout
16
+ retryDelayMinimum: 0,
17
+ });
18
+ });
19
+ afterEach(async () => {
20
+ await queue.clear();
21
+ });
22
+ afterAll(async () => {
23
+ await injector?.dispose();
24
+ });
25
+ it('should NOT prematurely complete a recovered parent when children finish', async () => {
26
+ // 1. Enqueue parent
27
+ const parent = await queue.enqueue('parent', {});
28
+ // 2. Dequeue parent (it is now Running, startTimestamp is set)
29
+ const dParent = await queue.dequeue();
30
+ expect(dParent?.id).toBe(parent.id);
31
+ expect(dParent?.startTimestamp).not.toBeNull();
32
+ // 3. Parent spawns a child that it waits for
33
+ const [child] = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id }], { returnTasks: true });
34
+ // 4. Simulate crash: Wait for visibility timeout so it becomes a zombie
35
+ await timeout(200);
36
+ // 5. Run maintenance to recover the zombie parent
37
+ await queue.maintenance();
38
+ // Verify it was recovered to Pending
39
+ const recoveredParent = await queue.getTask(parent.id);
40
+ expect(recoveredParent?.status).toBe(TaskStatus.Retrying);
41
+ // If the bug exists, startTimestamp is still set here.
42
+ // 6. Complete the child while parent is still Pending
43
+ const dChild = await queue.dequeue({ types: ['child'] });
44
+ await queue.complete(dChild);
45
+ // 7. Verify parent status
46
+ // BUG: evaluateTaskStatus will see unresolvedCompleteDependencies=0 and startTimestamp != null,
47
+ // and incorrectly transition the Pending parent to Completed.
48
+ const finalParent = await queue.getTask(parent.id);
49
+ expect(finalParent?.status).toBe(TaskStatus.Retrying); // It should still be pending execution!
50
+ });
51
+ });
package/test5.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import './polyfills.js';
2
- import { promptBuilder } from './ai/index.js';
3
2
  import { Application } from './application/application.js';
4
3
  import { provideModule, provideSignalHandler } from './application/index.js';
5
4
  import { PrettyPrintLogFormatter } from './logger/index.js';
6
5
  import { provideConsoleLogTransport } from './logger/transports/console.js';
7
6
  async function main(_cancellationSignal) {
8
- const x = promptBuilder();
9
- console.log(x.buildSystemPrompt()[0].text);
10
- console.log('\n\n---\n\n');
11
- console.log(x.buildUserPrompt()[0].text);
7
+ const arr = [1, 2, 3, 4, 5];
8
+ for (const item of arr) {
9
+ console.log(item);
10
+ arr.push(arr.at(-1) + 1);
11
+ }
12
12
  }
13
13
  Application.run('Test', [
14
14
  provideConsoleLogTransport(PrettyPrintLogFormatter),
@@ -3,6 +3,7 @@ import { type AuthenticationAncillaryService } from '../authentication/server/in
3
3
  import { Injector } from '../injector/index.js';
4
4
  import { LogLevel } from '../logger/index.js';
5
5
  import { type S3ObjectStorageProviderConfig } from '../object-storage/s3/index.js';
6
+ import type { EntityType } from '../orm/entity.js';
6
7
  import { Database } from '../orm/server/index.js';
7
8
  import type { Type } from '../types/index.js';
8
9
  export type IntegrationTestOptions = {
@@ -30,7 +31,6 @@ export type IntegrationTestOptions = {
30
31
  rateLimiter?: boolean;
31
32
  signals?: boolean;
32
33
  taskQueue?: boolean;
33
- test?: boolean;
34
34
  webServer?: boolean;
35
35
  };
36
36
  authenticationAncillaryService?: Type<AuthenticationAncillaryService>;
@@ -47,14 +47,14 @@ export declare function setupIntegrationTest(options?: IntegrationTestOptions):
47
47
  * Helper to truncate specific tables in a schema.
48
48
  * Useful in beforeEach() to reset state.
49
49
  */
50
- export declare function truncateTables(database: Database, schema: string, tables: string[]): Promise<void>;
50
+ export declare function truncateTables(database: Database, schema: string, tables: (string | EntityType)[]): Promise<void>;
51
51
  /**
52
52
  * Helper to delete data for a specific tenant from specific tables.
53
53
  * Useful for parallel tests to avoid interference.
54
54
  */
55
- export declare function clearTenantData(database: Database, schema: string, tables: string[], tenantId: string): Promise<void>;
55
+ export declare function clearTenantData(database: Database, schema: string | undefined, tables: (string | EntityType)[], tenantId: string): Promise<void>;
56
56
  /**
57
57
  * Helper to drop specific tables.
58
58
  * Useful in beforeAll() cleanups.
59
59
  */
60
- export declare function dropTables(database: Database, schema: string, tables: string[]): Promise<void>;
60
+ export declare function dropTables(database: Database, schema: string | undefined, tables: (string | EntityType)[]): Promise<void>;
@@ -1,30 +1,32 @@
1
1
  import { sql } from 'drizzle-orm';
2
+ import { afterAll } from 'vitest';
2
3
  import { configureApiServer } from '../api/server/index.js';
3
- import { configureAudit, migrateAuditSchema } from '../audit/index.js';
4
+ import { configureAudit } from '../audit/index.js';
4
5
  import { AuthenticationApiClient } from '../authentication/client/api.client.js';
5
6
  import { configureAuthenticationClient } from '../authentication/client/index.js';
6
- import { AuthenticationApiController, configureAuthenticationServer, migrateAuthenticationSchema } from '../authentication/server/index.js';
7
- import { configurePostgresCircuitBreaker, migratePostgresCircuitBreaker } from '../circuit-breaker/postgres/module.js';
8
- import { configureDocumentManagement, migrateDocumentManagementSchema } from '../document-management/server/index.js';
7
+ import { AuthenticationApiController, configureAuthenticationServer } from '../authentication/server/index.js';
8
+ import { configurePostgresCircuitBreaker } from '../circuit-breaker/postgres/module.js';
9
+ import { configureDocumentManagement } from '../document-management/server/index.js';
9
10
  import { configureUndiciHttpClientAdapter } from '../http/client/adapters/undici.adapter.js';
10
11
  import { configureHttpClient } from '../http/client/index.js';
11
12
  import { HttpServer } from '../http/server/index.js';
12
13
  import { configureNodeHttpServer } from '../http/server/node/module.js';
13
14
  import { Injector, runInInjectionContext } from '../injector/index.js';
14
- import { configurePostgresKeyValueStore, migratePostgresKeyValueStoreSchema } from '../key-value-store/postgres/module.js';
15
- import { configurePostgresLock, migratePostgresLockSchema } from '../lock/postgres/module.js';
15
+ import { configurePostgresKeyValueStore } from '../key-value-store/postgres/module.js';
16
+ import { configurePostgresLock } from '../lock/postgres/module.js';
16
17
  import { ConsoleLogTransport, DEFAULT_LOG_LEVEL, LogFormatter, LogLevel, LogManager, LogTransport, PrettyPrintLogFormatter } from '../logger/index.js';
17
18
  import { configureLocalMessageBus } from '../message-bus/index.js';
18
19
  import { configureWebServerModule, WebServerModule } from '../module/modules/web-server.module.js';
19
- import { configureNotification, migrateNotificationSchema } from '../notification/server/index.js';
20
+ import { configureNotification } from '../notification/server/index.js';
20
21
  import { configureS3ObjectStorage } from '../object-storage/s3/index.js';
21
- import { configureOrm, Database } from '../orm/server/index.js';
22
- import { configurePostgresRateLimiter, migratePostgresRateLimiterSchema } from '../rate-limit/postgres/module.js';
22
+ import { bootstrapOrm, configureOrm, Database } from '../orm/server/index.js';
23
+ import { getEntitySchema, getEntityTableName } from '../orm/utils.js';
24
+ import { configurePostgresRateLimiter } from '../rate-limit/postgres/module.js';
23
25
  import { configureDefaultSignalsImplementation } from '../signals/implementation/configure.js';
24
- import { configurePostgresTaskQueue, migratePostgresTaskQueueSchema } from '../task-queue/postgres/index.js';
26
+ import { configurePostgresTaskQueue } from '../task-queue/postgres/index.js';
25
27
  import * as configParser from '../utils/config-parser.js';
26
28
  import { objectEntries } from '../utils/object/object.js';
27
- import { isDefined, isNotNull } from '../utils/type-guards.js';
29
+ import { assertDefinedPass, isDefined, isNotNull, isString } from '../utils/type-guards.js';
28
30
  /**
29
31
  * Standard setup for integration tests.
30
32
  */
@@ -51,6 +53,8 @@ export async function setupIntegrationTest(options = {}) {
51
53
  ...options.dbConfig,
52
54
  };
53
55
  // 4. Configure ORM
56
+ // We disable autoMigrate here because APPLICATION_INITIALIZER is not used in integration tests
57
+ // We manually run migrations via bootstrapOrm below
54
58
  configureOrm({
55
59
  repositoryConfig: { schema: options.orm?.schema ?? 'test' },
56
60
  connection: dbConfig,
@@ -66,35 +70,29 @@ export async function setupIntegrationTest(options = {}) {
66
70
  await database.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(options.orm.schema)}`);
67
71
  }
68
72
  // 7. Optional Modules
69
- if (options.modules?.messageBus ?? options.modules?.taskQueue ?? options.modules?.authentication ?? options.modules?.test ?? options.modules?.notification) {
73
+ if (options.modules?.messageBus ?? options.modules?.taskQueue ?? options.modules?.authentication ?? options.modules?.notification) {
70
74
  configureLocalMessageBus({ injector });
71
75
  }
72
76
  if (options.modules?.taskQueue) {
73
77
  configurePostgresTaskQueue({ injector });
74
- await runInInjectionContext(injector, migratePostgresTaskQueueSchema);
75
78
  }
76
79
  if (options.modules?.circuitBreaker ?? options.modules?.taskQueue) {
77
80
  configurePostgresCircuitBreaker({ injector });
78
- await runInInjectionContext(injector, migratePostgresCircuitBreaker);
79
81
  }
80
82
  if (options.modules?.rateLimiter ?? options.modules?.taskQueue) {
81
83
  configurePostgresRateLimiter({ injector });
82
- await runInInjectionContext(injector, migratePostgresRateLimiterSchema);
83
84
  }
84
85
  if (options.modules?.keyValueStore ?? options.modules?.authentication) {
85
86
  configurePostgresKeyValueStore({ injector });
86
- await runInInjectionContext(injector, migratePostgresKeyValueStoreSchema);
87
87
  }
88
88
  if (options.modules?.lock ?? options.modules?.authentication) {
89
89
  configurePostgresLock({ injector });
90
- await runInInjectionContext(injector, migratePostgresLockSchema);
91
90
  }
92
- if (options.modules?.signals ?? options.modules?.authentication ?? options.modules?.test ?? options.modules?.notification) {
91
+ if (options.modules?.signals ?? options.modules?.authentication ?? options.modules?.notification) {
93
92
  configureDefaultSignalsImplementation();
94
93
  }
95
94
  if (options.modules?.audit ?? options.modules?.authentication) {
96
95
  configureAudit({ injector });
97
- await runInInjectionContext(injector, migrateAuditSchema);
98
96
  }
99
97
  if (options.modules?.authentication) {
100
98
  configureAuthenticationServer({
@@ -106,11 +104,9 @@ export async function setupIntegrationTest(options = {}) {
106
104
  authenticationAncillaryService: options.authenticationAncillaryService,
107
105
  injector,
108
106
  });
109
- await runInInjectionContext(injector, migrateAuthenticationSchema);
110
107
  }
111
108
  if (options.modules?.notification) {
112
109
  configureNotification({ injector });
113
- await runInInjectionContext(injector, migrateNotificationSchema);
114
110
  }
115
111
  if (options.modules?.documentManagement) {
116
112
  configureDocumentManagement({
@@ -122,8 +118,8 @@ export async function setupIntegrationTest(options = {}) {
122
118
  skipAi: true,
123
119
  injector,
124
120
  });
125
- await runInInjectionContext(injector, migrateDocumentManagementSchema);
126
121
  }
122
+ await runInInjectionContext(injector, bootstrapOrm);
127
123
  if (options.modules?.objectStorage) {
128
124
  const bucketPerModule = options.s3?.bucketPerModule ?? configParser.boolean('S3_BUCKET_PER_MODULE', true);
129
125
  configureS3ObjectStorage({
@@ -167,6 +163,7 @@ export async function setupIntegrationTest(options = {}) {
167
163
  }
168
164
  }
169
165
  });
166
+ afterAll(async () => await injector.dispose());
170
167
  return { injector, database };
171
168
  }
172
169
  /**
@@ -190,9 +187,18 @@ export async function truncateTables(database, schema, tables) {
190
187
  if (tables.length == 0) {
191
188
  return;
192
189
  }
193
- // Using CASCADE to handle foreign keys automatically
194
- for (const table of tables) {
195
- await database.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier(table)} CASCADE`);
190
+ const lockId = 987654321; // Different lock ID for table maintenance
191
+ await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
192
+ try {
193
+ // Using CASCADE to handle foreign keys automatically
194
+ for (const table of tables) {
195
+ const tableName = isString(table) ? table : getEntityTableName(table);
196
+ const tableSchema = isString(table) ? schema : getEntitySchema(table, schema);
197
+ await database.execute(sql `TRUNCATE TABLE ${sql.identifier(tableSchema)}.${sql.identifier(tableName)} CASCADE`);
198
+ }
199
+ }
200
+ finally {
201
+ await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
196
202
  }
197
203
  }
198
204
  /**
@@ -203,8 +209,17 @@ export async function clearTenantData(database, schema, tables, tenantId) {
203
209
  if (tables.length == 0) {
204
210
  return;
205
211
  }
206
- for (const table of tables) {
207
- await database.execute(sql `DELETE FROM ${sql.identifier(schema)}.${sql.identifier(table)} WHERE tenant_id = ${tenantId}`);
212
+ const lockId = 987654321;
213
+ await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
214
+ try {
215
+ for (const table of tables) {
216
+ const tableName = isString(table) ? table : getEntityTableName(table);
217
+ const tableSchema = isString(table) ? assertDefinedPass(schema, 'Schema not provided') : getEntitySchema(table, schema);
218
+ await database.execute(sql `DELETE FROM ${sql.identifier(tableSchema)}.${sql.identifier(tableName)} WHERE tenant_id = ${tenantId}`);
219
+ }
220
+ }
221
+ finally {
222
+ await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
208
223
  }
209
224
  }
210
225
  /**
@@ -212,7 +227,19 @@ export async function clearTenantData(database, schema, tables, tenantId) {
212
227
  * Useful in beforeAll() cleanups.
213
228
  */
214
229
  export async function dropTables(database, schema, tables) {
215
- for (const table of tables) {
216
- await database.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier(table)} CASCADE`);
230
+ if (tables.length == 0) {
231
+ return;
232
+ }
233
+ const lockId = 987654321;
234
+ await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
235
+ try {
236
+ for (const table of tables) {
237
+ const tableName = isString(table) ? table : getEntityTableName(table);
238
+ const tableSchema = isString(table) ? assertDefinedPass(schema, 'Schema not provided') : getEntitySchema(table, schema);
239
+ await database.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(tableSchema)}.${sql.identifier(tableName)} CASCADE`);
240
+ }
241
+ }
242
+ finally {
243
+ await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
217
244
  }
218
245
  }
@@ -192,7 +192,7 @@ function buildMappedLocalization({ language, keys, enums }) {
192
192
  const mappedLocalization = {
193
193
  language,
194
194
  keys: new Map(deepObjectEntries(keys)),
195
- enums: new Map(enumsEntries)
195
+ enums: new Map(enumsEntries),
196
196
  };
197
197
  return mappedLocalization;
198
198
  }
@@ -215,7 +215,7 @@ function mergeMappedLocalization(a, b, force = false) {
215
215
  return {
216
216
  language: b.language,
217
217
  keys: new Map([...a.keys, ...b.keys]),
218
- enums: new Map([...a.enums, ...b.enums])
218
+ enums: new Map([...a.enums, ...b.enums]),
219
219
  };
220
220
  }
221
221
  export function isLocalizeItem(value) {
@@ -1,4 +1,3 @@
1
- import { DetailsError } from '../errors/details.error.js';
2
1
  export async function readAsText(blob, encoding) {
3
2
  return await setup((reader) => reader.readAsText(blob, encoding));
4
3
  }
@@ -12,7 +11,7 @@ async function setup(dispatcher) {
12
11
  return await new Promise((resolve, reject) => {
13
12
  const reader = new FileReader();
14
13
  reader.onload = () => resolve(reader.result);
15
- reader.onerror = () => reject(new DetailsError(reader.error.message, reader.error));
14
+ reader.onerror = () => reject(new Error('FileReader error', { cause: reader.error }));
16
15
  dispatcher(reader);
17
16
  });
18
17
  }
package/utils/timing.d.ts CHANGED
@@ -8,9 +8,9 @@ export declare function timeout(milliseconds?: number, options?: {
8
8
  /** Timeout until specified time */
9
9
  export declare function timeoutUntil(timestamp: number | Date): Promise<void>;
10
10
  /** Timeout for specified duration */
11
- export declare function cancelableTimeout(milliseconds: number, cancelSignal: Observable<void> | CancellationSignal): Promise<'timeout' | 'canceled'>;
11
+ export declare function cancelableTimeout(milliseconds: number, cancelSignal: Observable<unknown> | CancellationSignal): Promise<'timeout' | 'canceled'>;
12
12
  /** Timeout until specified time */
13
- export declare function cancelableTimeoutUntil(timestamp: number | Date, cancelSignal: Observable<void> | CancellationSignal): Promise<'timeout' | 'canceled'>;
13
+ export declare function cancelableTimeoutUntil(timestamp: number | Date, cancelSignal: Observable<unknown> | CancellationSignal): Promise<'timeout' | 'canceled'>;
14
14
  export declare function withTimeout<T>(milliseconds: number, promiseOrProvider: ValueOrProvider<Promise<T>>, options?: {
15
15
  errorMessage?: string;
16
16
  }): Promise<T>;
@@ -1,74 +0,0 @@
1
- CREATE TYPE "task_queue"."dependency_join_mode" AS ENUM('and', 'or');--> statement-breakpoint
2
- CREATE TYPE "task_queue"."task_status" AS ENUM('pending', 'running', 'completed', 'cancelled', 'waiting', 'dead');--> statement-breakpoint
3
- CREATE TABLE "task_queue"."task" (
4
- "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
5
- "namespace" text NOT NULL,
6
- "type" text NOT NULL,
7
- "status" "task_queue"."task_status" NOT NULL,
8
- "idempotency_key" text,
9
- "trace_id" text,
10
- "tags" text[] NOT NULL,
11
- "complete_after_tags" text[] NOT NULL,
12
- "schedule_after_tags" text[] NOT NULL,
13
- "fail_fast" boolean NOT NULL,
14
- "dependency_join_mode" "task_queue"."dependency_join_mode" NOT NULL,
15
- "dependency_trigger_statuses" "task_queue"."task_status"[] NOT NULL,
16
- "priority" integer NOT NULL,
17
- "token" uuid,
18
- "creation_timestamp" timestamp with time zone NOT NULL,
19
- "priority_age_timestamp" timestamp with time zone NOT NULL,
20
- "schedule_timestamp" timestamp with time zone NOT NULL,
21
- "start_timestamp" timestamp with time zone,
22
- "time_to_live" timestamp with time zone,
23
- "visibility_deadline" timestamp with time zone,
24
- "complete_timestamp" timestamp with time zone,
25
- "tries" integer NOT NULL,
26
- "progress" double precision NOT NULL,
27
- "data" jsonb,
28
- "state" jsonb,
29
- "result" jsonb,
30
- "error" jsonb,
31
- "parent_id" uuid,
32
- CONSTRAINT "task_namespace_idempotency_key_unique" UNIQUE("namespace","idempotency_key")
33
- );
34
- --> statement-breakpoint
35
- CREATE TABLE "task_queue"."task_archive" (
36
- "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
37
- "namespace" text NOT NULL,
38
- "type" text NOT NULL,
39
- "status" "task_queue"."task_status" NOT NULL,
40
- "idempotency_key" text,
41
- "trace_id" text,
42
- "tags" text[] NOT NULL,
43
- "complete_after_tags" text[] NOT NULL,
44
- "schedule_after_tags" text[] NOT NULL,
45
- "fail_fast" boolean NOT NULL,
46
- "dependency_join_mode" "task_queue"."dependency_join_mode" NOT NULL,
47
- "dependency_trigger_statuses" "task_queue"."task_status"[] NOT NULL,
48
- "priority" integer NOT NULL,
49
- "token" uuid,
50
- "creation_timestamp" timestamp with time zone NOT NULL,
51
- "priority_age_timestamp" timestamp with time zone NOT NULL,
52
- "schedule_timestamp" timestamp with time zone NOT NULL,
53
- "start_timestamp" timestamp with time zone,
54
- "time_to_live" timestamp with time zone,
55
- "visibility_deadline" timestamp with time zone,
56
- "complete_timestamp" timestamp with time zone,
57
- "tries" integer NOT NULL,
58
- "progress" double precision NOT NULL,
59
- "data" jsonb,
60
- "state" jsonb,
61
- "result" jsonb,
62
- "error" jsonb,
63
- "parent_id" uuid
64
- );
65
- --> statement-breakpoint
66
- ALTER TABLE "task_queue"."task" ADD CONSTRAINT "task_parent_id_task_id_fk" FOREIGN KEY ("parent_id") REFERENCES "task_queue"."task"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
67
- CREATE INDEX "task_parent_id_idx" ON "task_queue"."task" USING btree ("parent_id");--> statement-breakpoint
68
- CREATE INDEX "task_status_visibility_deadline_idx" ON "task_queue"."task" USING btree ("status","visibility_deadline");--> statement-breakpoint
69
- CREATE INDEX "task_status_complete_timestamp_idx" ON "task_queue"."task" USING btree ("status","complete_timestamp");--> statement-breakpoint
70
- CREATE INDEX "task_schedule_after_tags_idx" ON "task_queue"."task" USING gin ("schedule_after_tags");--> statement-breakpoint
71
- CREATE INDEX "task_complete_after_tags_idx" ON "task_queue"."task" USING gin ("complete_after_tags");--> statement-breakpoint
72
- CREATE INDEX "task_tags_idx" ON "task_queue"."task" USING gin ("tags");--> statement-breakpoint
73
- CREATE INDEX "task_namespace_status_schedule_timestamp_priority_idx" ON "task_queue"."task" USING btree ("namespace","status","schedule_timestamp","priority");--> statement-breakpoint
74
- CREATE INDEX "task_archive_namespace_complete_timestamp_idx" ON "task_queue"."task_archive" USING btree ("namespace","complete_timestamp");