@tstdl/base 0.93.145 → 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 (39) hide show
  1. package/authentication/tests/authentication.client-service.test.js +15 -19
  2. package/authentication/tests/authentication.service.test.js +92 -119
  3. package/notification/tests/notification-client.test.js +39 -50
  4. package/notification/tests/notification-flow.test.js +204 -238
  5. package/notification/tests/notification-sse.service.test.js +20 -27
  6. package/notification/tests/notification-type.service.test.js +17 -20
  7. package/orm/tests/query-complex.test.js +80 -111
  8. package/orm/tests/repository-advanced.test.js +100 -143
  9. package/orm/tests/repository-attributes.test.js +30 -39
  10. package/orm/tests/repository-compound-primary-key.test.js +67 -75
  11. package/orm/tests/repository-comprehensive.test.js +76 -101
  12. package/orm/tests/repository-coverage.test.d.ts +1 -0
  13. package/orm/tests/repository-coverage.test.js +88 -149
  14. package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
  15. package/orm/tests/repository-cti-extensive.test.js +118 -147
  16. package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
  17. package/orm/tests/repository-cti-mapping.test.js +29 -42
  18. package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
  19. package/orm/tests/repository-cti-soft-delete.test.js +25 -37
  20. package/orm/tests/repository-cti-transactions.test.js +19 -33
  21. package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
  22. package/orm/tests/repository-cti-upsert-many.test.js +38 -50
  23. package/orm/tests/repository-cti.test.d.ts +1 -0
  24. package/orm/tests/repository-cti.test.js +195 -247
  25. package/orm/tests/repository-expiration.test.d.ts +1 -0
  26. package/orm/tests/repository-expiration.test.js +46 -59
  27. package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
  28. package/orm/tests/repository-extra-coverage.test.js +195 -337
  29. package/orm/tests/repository-mapping.test.d.ts +1 -0
  30. package/orm/tests/repository-mapping.test.js +20 -20
  31. package/orm/tests/repository-regression.test.js +124 -163
  32. package/orm/tests/repository-search.test.js +30 -44
  33. package/orm/tests/repository-soft-delete.test.js +54 -79
  34. package/orm/tests/repository-types.test.js +77 -111
  35. package/package.json +1 -1
  36. package/task-queue/tests/worker.test.js +5 -5
  37. package/testing/README.md +38 -16
  38. package/testing/integration-setup.d.ts +11 -0
  39. package/testing/integration-setup.js +57 -30
@@ -7,17 +7,17 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
- import { Injector, runInInjectionContext } from '../../injector/index.js';
11
- import { StringProperty } from '../../schema/index.js';
12
10
  import { sql } from 'drizzle-orm';
13
11
  import { beforeAll, describe, expect, test } from 'vitest';
12
+ import { StringProperty } from '../../schema/index.js';
13
+ import { setupIntegrationTest } from '../../testing/index.js';
14
14
  import { Table } from '../decorators.js';
15
15
  import { Entity } from '../entity.js';
16
- import { configureOrm, Database } from '../server/index.js';
17
- import { injectRepository } from '../server/repository.js';
16
+ import { getRepository } from '../server/index.js';
18
17
  describe('ORM Repository Soft Delete (Integration)', () => {
19
18
  let injector;
20
19
  let db;
20
+ let repository;
21
21
  const schema = 'test_orm_soft_delete';
22
22
  let SoftDeleteEntity = class SoftDeleteEntity extends Entity {
23
23
  name;
@@ -30,14 +30,10 @@ describe('ORM Repository Soft Delete (Integration)', () => {
30
30
  Table('soft_delete_entities', { schema })
31
31
  ], SoftDeleteEntity);
32
32
  beforeAll(async () => {
33
- injector = new Injector('Test');
34
- configureOrm({
35
- repositoryConfig: { schema },
36
- connection: {
37
- host: '127.0.0.1', port: 5432, user: 'tstdl', password: 'wf7rq6glrk5jykne', database: 'tstdl',
38
- }
39
- });
40
- db = injector.resolve(Database);
33
+ ({ injector, database: db } = await setupIntegrationTest({
34
+ orm: { schema },
35
+ }));
36
+ repository = injector.resolve(getRepository(SoftDeleteEntity));
41
37
  await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
42
38
  await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
43
39
  await db.execute(sql `
@@ -54,90 +50,69 @@ describe('ORM Repository Soft Delete (Integration)', () => {
54
50
  });
55
51
  test('should soft delete an entity by ID', async () => {
56
52
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
57
- await runInInjectionContext(injector, async () => {
58
- const repository = injectRepository(SoftDeleteEntity);
59
- const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
60
- const deleted = await repository.delete(e1.id);
61
- expect(deleted.id).toBe(e1.id);
62
- expect(deleted.metadata.deleteTimestamp).toBeDefined();
63
- expect(await repository.has(e1.id)).toBe(false);
64
- expect(await repository.tryLoad(e1.id)).toBeUndefined();
65
- expect(await repository.count()).toBe(0);
66
- const allWithDeleted = await repository.loadAll({ withDeleted: true });
67
- expect(allWithDeleted).toHaveLength(1);
68
- expect(allWithDeleted[0].id).toBe(e1.id);
69
- });
53
+ const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
54
+ const deleted = await repository.delete(e1.id);
55
+ expect(deleted.id).toBe(e1.id);
56
+ expect(deleted.metadata.deleteTimestamp).toBeDefined();
57
+ expect(await repository.has(e1.id)).toBe(false);
58
+ expect(await repository.tryLoad(e1.id)).toBeUndefined();
59
+ expect(await repository.count()).toBe(0);
60
+ const allWithDeleted = await repository.loadAll({ withDeleted: true });
61
+ expect(allWithDeleted).toHaveLength(1);
62
+ expect(allWithDeleted[0].id).toBe(e1.id);
70
63
  });
71
64
  test('should soft delete multiple entities by IDs', async () => {
72
65
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
73
- await runInInjectionContext(injector, async () => {
74
- const repository = injectRepository(SoftDeleteEntity);
75
- const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
76
- const e2 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E2' }));
77
- const deleted = await repository.deleteMany([e1.id, e2.id]);
78
- expect(deleted).toHaveLength(2);
79
- expect(await repository.count()).toBe(0);
80
- expect(await repository.countByQuery({}, { withDeleted: true })).toBe(2);
81
- });
66
+ const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
67
+ const e2 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E2' }));
68
+ const deleted = await repository.deleteMany([e1.id, e2.id]);
69
+ expect(deleted).toHaveLength(2);
70
+ expect(await repository.count()).toBe(0);
71
+ expect(await repository.countByQuery({}, { withDeleted: true })).toBe(2);
82
72
  });
83
73
  test('should soft delete entities by query', async () => {
84
74
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
85
- await runInInjectionContext(injector, async () => {
86
- const repository = injectRepository(SoftDeleteEntity);
87
- await repository.insertMany([
88
- Object.assign(new SoftDeleteEntity(), { name: 'A' }),
89
- Object.assign(new SoftDeleteEntity(), { name: 'B' }),
90
- Object.assign(new SoftDeleteEntity(), { name: 'A' }),
91
- ]);
92
- const deleted = await repository.deleteManyByQuery({ name: 'A' });
93
- expect(deleted).toHaveLength(2);
94
- expect(await repository.count()).toBe(1);
95
- expect(await repository.countByQuery({}, { withDeleted: true })).toBe(3);
96
- });
75
+ await repository.insertMany([
76
+ Object.assign(new SoftDeleteEntity(), { name: 'A' }),
77
+ Object.assign(new SoftDeleteEntity(), { name: 'B' }),
78
+ Object.assign(new SoftDeleteEntity(), { name: 'A' }),
79
+ ]);
80
+ const deleted = await repository.deleteManyByQuery({ name: 'A' });
81
+ expect(deleted).toHaveLength(2);
82
+ expect(await repository.count()).toBe(1);
83
+ expect(await repository.countByQuery({}, { withDeleted: true })).toBe(3);
97
84
  });
98
85
  test('should not update soft-deleted entities by default', async () => {
99
86
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
100
- await runInInjectionContext(injector, async () => {
101
- const repository = injectRepository(SoftDeleteEntity);
102
- const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
103
- await repository.delete(e1.id);
104
- const updated = await repository.tryUpdate(e1.id, { name: 'Updated' });
105
- expect(updated).toBeUndefined();
106
- const loaded = await repository.tryLoad(e1.id, { withDeleted: true });
107
- expect(loaded.name).toBe('E1');
108
- });
87
+ const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
88
+ await repository.delete(e1.id);
89
+ const updated = await repository.tryUpdate(e1.id, { name: 'Updated' });
90
+ expect(updated).toBeUndefined();
91
+ const loaded = await repository.tryLoad(e1.id, { withDeleted: true });
92
+ expect(loaded.name).toBe('E1');
109
93
  });
110
94
  test('should support hard delete on soft-deleted entities', async () => {
111
95
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
112
- await runInInjectionContext(injector, async () => {
113
- const repository = injectRepository(SoftDeleteEntity);
114
- const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
115
- await repository.delete(e1.id);
116
- const hardDeleted = await repository.hardDelete(e1.id);
117
- expect(hardDeleted.id).toBe(e1.id);
118
- expect(await repository.countByQuery({}, { withDeleted: true })).toBe(0);
119
- });
96
+ const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
97
+ await repository.delete(e1.id);
98
+ const hardDeleted = await repository.hardDelete(e1.id);
99
+ expect(hardDeleted.id).toBe(e1.id);
100
+ expect(await repository.countByQuery({}, { withDeleted: true })).toBe(0);
120
101
  });
121
102
  test('should ignore already soft-deleted entities when deleting again', async () => {
122
103
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
123
- await runInInjectionContext(injector, async () => {
124
- const repository = injectRepository(SoftDeleteEntity);
125
- const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
126
- await repository.delete(e1.id);
127
- const secondDelete = await repository.tryDelete(e1.id);
128
- expect(secondDelete).toBeUndefined();
129
- });
104
+ const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
105
+ await repository.delete(e1.id);
106
+ const secondDelete = await repository.tryDelete(e1.id);
107
+ expect(secondDelete).toBeUndefined();
130
108
  });
131
109
  test('should hard delete multiple entities by query (including soft-deleted)', async () => {
132
110
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('soft_delete_entities')} CASCADE`);
133
- await runInInjectionContext(injector, async () => {
134
- const repository = injectRepository(SoftDeleteEntity);
135
- const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
136
- const e2 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E2' }));
137
- await repository.delete(e1.id); // Soft delete E1
138
- const hardDeleted = await repository.hardDeleteManyByQuery({});
139
- expect(hardDeleted).toHaveLength(2); // Should hard delete both
140
- expect(await repository.countByQuery({}, { withDeleted: true })).toBe(0);
141
- });
111
+ const e1 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E1' }));
112
+ const e2 = await repository.insert(Object.assign(new SoftDeleteEntity(), { name: 'E2' }));
113
+ await repository.delete(e1.id); // Soft delete E1
114
+ const hardDeleted = await repository.hardDeleteManyByQuery({});
115
+ expect(hardDeleted).toHaveLength(2); // Should hard delete both
116
+ expect(await repository.countByQuery({}, { withDeleted: true })).toBe(0);
142
117
  });
143
118
  });
@@ -9,16 +9,16 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  };
10
10
  import { sql } from 'drizzle-orm';
11
11
  import { beforeAll, describe, expect, test } from 'vitest';
12
- import { Injector, runInInjectionContext } from '../../injector/index.js';
13
12
  import { Array, BooleanProperty, Integer, NumberProperty, string, StringProperty } from '../../schema/index.js';
13
+ import { setupIntegrationTest } from '../../testing/index.js';
14
14
  import { Table } from '../decorators.js';
15
15
  import { Entity } from '../entity.js';
16
16
  import { JsonProperty, NumericDateProperty, TimestampProperty } from '../schemas/index.js';
17
- import { configureOrm, Database } from '../server/index.js';
18
- import { injectRepository } from '../server/repository.js';
17
+ import { getRepository } from '../server/index.js';
19
18
  describe('ORM Repository Types (Integration)', () => {
20
19
  let injector;
21
20
  let db;
21
+ let repo;
22
22
  const schema = 'test_orm_types';
23
23
  let TypeEntity = class TypeEntity extends Entity {
24
24
  text;
@@ -66,14 +66,10 @@ describe('ORM Repository Types (Integration)', () => {
66
66
  Table('type_entities', { schema })
67
67
  ], TypeEntity);
68
68
  beforeAll(async () => {
69
- injector = new Injector('Test');
70
- configureOrm({
71
- repositoryConfig: { schema },
72
- connection: {
73
- host: '127.0.0.1', port: 5432, user: 'tstdl', password: 'wf7rq6glrk5jykne', database: 'tstdl',
74
- },
75
- });
76
- db = injector.resolve(Database);
69
+ ({ injector, database: db } = await setupIntegrationTest({
70
+ orm: { schema },
71
+ }));
72
+ repo = injector.resolve(getRepository(TypeEntity));
77
73
  await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
78
74
  await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('type_entities')} CASCADE`);
79
75
  await db.execute(sql `
@@ -96,123 +92,93 @@ describe('ORM Repository Types (Integration)', () => {
96
92
  `);
97
93
  });
98
94
  test('should persist and retrieve boolean values (true)', async () => {
99
- await runInInjectionContext(injector, async () => {
100
- const repo = injectRepository(TypeEntity);
101
- const entity = await repo.insert(Object.assign(new TypeEntity(), {
102
- text: 't', int: 1, float: 1.1, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
103
- }));
104
- const loaded = await repo.load(entity.id);
105
- expect(loaded.bool).toBe(true);
106
- });
95
+ const entity = await repo.insert(Object.assign(new TypeEntity(), {
96
+ text: 't', int: 1, float: 1.1, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
97
+ }));
98
+ const loaded = await repo.load(entity.id);
99
+ expect(loaded.bool).toBe(true);
107
100
  });
108
101
  test('should persist and retrieve boolean values (false)', async () => {
109
- await runInInjectionContext(injector, async () => {
110
- const repo = injectRepository(TypeEntity);
111
- const entity = await repo.insert(Object.assign(new TypeEntity(), {
112
- text: 'f', int: 1, float: 1.1, bool: false, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
113
- }));
114
- const loaded = await repo.load(entity.id);
115
- expect(loaded.bool).toBe(false);
116
- });
102
+ const entity = await repo.insert(Object.assign(new TypeEntity(), {
103
+ text: 'f', int: 1, float: 1.1, bool: false, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
104
+ }));
105
+ const loaded = await repo.load(entity.id);
106
+ expect(loaded.bool).toBe(false);
117
107
  });
118
108
  test('should persist and retrieve integer boundaries', async () => {
119
- await runInInjectionContext(injector, async () => {
120
- const repo = injectRepository(TypeEntity);
121
- const maxInt = 2147483647;
122
- const minInt = -2147483648;
123
- const e1 = await repo.insert(Object.assign(new TypeEntity(), {
124
- text: 'max', int: maxInt, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
125
- }));
126
- const l1 = await repo.load(e1.id);
127
- expect(l1.int).toBe(maxInt);
128
- const e2 = await repo.insert(Object.assign(new TypeEntity(), {
129
- text: 'min', int: minInt, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
130
- }));
131
- const l2 = await repo.load(e2.id);
132
- expect(l2.int).toBe(minInt);
133
- });
109
+ const maxInt = 2147483647;
110
+ const minInt = -2147483648;
111
+ const e1 = await repo.insert(Object.assign(new TypeEntity(), {
112
+ text: 'max', int: maxInt, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
113
+ }));
114
+ const l1 = await repo.load(e1.id);
115
+ expect(l1.int).toBe(maxInt);
116
+ const e2 = await repo.insert(Object.assign(new TypeEntity(), {
117
+ text: 'min', int: minInt, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
118
+ }));
119
+ const l2 = await repo.load(e2.id);
120
+ expect(l2.int).toBe(minInt);
134
121
  });
135
122
  test('should persist and retrieve float precision', async () => {
136
- await runInInjectionContext(injector, async () => {
137
- const repo = injectRepository(TypeEntity);
138
- const pi = 3.1415926535;
139
- const entity = await repo.insert(Object.assign(new TypeEntity(), {
140
- text: 'pi', int: 0, float: pi, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
141
- }));
142
- const loaded = await repo.load(entity.id);
143
- expect(loaded.float).toBeCloseTo(pi);
144
- });
123
+ const pi = 3.1415926535;
124
+ const entity = await repo.insert(Object.assign(new TypeEntity(), {
125
+ text: 'pi', int: 0, float: pi, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
126
+ }));
127
+ const loaded = await repo.load(entity.id);
128
+ expect(loaded.float).toBeCloseTo(pi);
145
129
  });
146
130
  test('should persist and retrieve numeric dates', async () => {
147
- await runInInjectionContext(injector, async () => {
148
- const repo = injectRepository(TypeEntity);
149
- const today = 19723; // 2024-01-01
150
- const entity = await repo.insert(Object.assign(new TypeEntity(), {
151
- text: 'date', int: 0, float: 0, bool: true, numericDate: today, timestamp: new Date(), json: {}, tags: [],
152
- }));
153
- const loaded = await repo.load(entity.id);
154
- expect(loaded.numericDate).toBe(today);
155
- });
131
+ const today = 19723; // 2024-01-01
132
+ const entity = await repo.insert(Object.assign(new TypeEntity(), {
133
+ text: 'date', int: 0, float: 0, bool: true, numericDate: today, timestamp: new Date(), json: {}, tags: [],
134
+ }));
135
+ const loaded = await repo.load(entity.id);
136
+ expect(loaded.numericDate).toBe(today);
156
137
  });
157
138
  test('should persist and retrieve timestamps', async () => {
158
- await runInInjectionContext(injector, async () => {
159
- const repo = injectRepository(TypeEntity);
160
- const now = new Date();
161
- const entity = await repo.insert(Object.assign(new TypeEntity(), {
162
- text: 'ts', int: 0, float: 0, bool: true, numericDate: 0, timestamp: now, json: {}, tags: [],
163
- }));
164
- const loaded = await repo.load(entity.id);
165
- // Postgres stores microsecond precision, JS Date has millisecond.
166
- // Drizzle custom type returns number (milliseconds).
167
- // We accept a small difference due to precision loss or round-trip.
168
- expect(typeof loaded.timestamp).toBe('number');
169
- expect(Math.abs(loaded.timestamp - now.getTime())).toBeLessThan(2);
170
- });
139
+ const now = new Date();
140
+ const entity = await repo.insert(Object.assign(new TypeEntity(), {
141
+ text: 'ts', int: 0, float: 0, bool: true, numericDate: 0, timestamp: now, json: {}, tags: [],
142
+ }));
143
+ const loaded = await repo.load(entity.id);
144
+ // Postgres stores microsecond precision, JS Date has millisecond.
145
+ // Drizzle custom type returns number (milliseconds).
146
+ // We accept a small difference due to precision loss or round-trip.
147
+ expect(typeof loaded.timestamp).toBe('number');
148
+ expect(Math.abs(loaded.timestamp - now.getTime())).toBeLessThan(2);
171
149
  });
172
150
  test('should persist and retrieve complex JSON', async () => {
173
- await runInInjectionContext(injector, async () => {
174
- const repo = injectRepository(TypeEntity);
175
- const data = {
176
- foo: 'bar',
177
- nested: { a: 1, b: [true, false] },
178
- nullVal: null,
179
- };
180
- const entity = await repo.insert(Object.assign(new TypeEntity(), {
181
- text: 'json', int: 0, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: data, tags: [],
182
- }));
183
- const loaded = await repo.load(entity.id);
184
- expect(loaded.json).toEqual(data);
185
- });
151
+ const data = {
152
+ foo: 'bar',
153
+ nested: { a: 1, b: [true, false] },
154
+ nullVal: null,
155
+ };
156
+ const entity = await repo.insert(Object.assign(new TypeEntity(), {
157
+ text: 'json', int: 0, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: data, tags: [],
158
+ }));
159
+ const loaded = await repo.load(entity.id);
160
+ expect(loaded.json).toEqual(data);
186
161
  });
187
162
  test('should persist and retrieve arrays', async () => {
188
- await runInInjectionContext(injector, async () => {
189
- const repo = injectRepository(TypeEntity);
190
- const tags = ['a', 'b', 'c'];
191
- const entity = await repo.insert(Object.assign(new TypeEntity(), {
192
- text: 'array', int: 0, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags,
193
- }));
194
- const loaded = await repo.load(entity.id);
195
- expect(loaded.tags).toEqual(tags);
196
- });
163
+ const tags = ['a', 'b', 'c'];
164
+ const entity = await repo.insert(Object.assign(new TypeEntity(), {
165
+ text: 'array', int: 0, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags,
166
+ }));
167
+ const loaded = await repo.load(entity.id);
168
+ expect(loaded.tags).toEqual(tags);
197
169
  });
198
170
  test('should persist and retrieve empty arrays', async () => {
199
- await runInInjectionContext(injector, async () => {
200
- const repo = injectRepository(TypeEntity);
201
- const entity = await repo.insert(Object.assign(new TypeEntity(), {
202
- text: 'empty-array', int: 0, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
203
- }));
204
- const loaded = await repo.load(entity.id);
205
- expect(loaded.tags).toEqual([]);
206
- });
171
+ const entity = await repo.insert(Object.assign(new TypeEntity(), {
172
+ text: 'empty-array', int: 0, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
173
+ }));
174
+ const loaded = await repo.load(entity.id);
175
+ expect(loaded.tags).toEqual([]);
207
176
  });
208
177
  test('should fail on integer overflow', async () => {
209
- await runInInjectionContext(injector, async () => {
210
- const repo = injectRepository(TypeEntity);
211
- // Max int is 2147483647
212
- const overflow = 3000000000;
213
- await expect(repo.insert(Object.assign(new TypeEntity(), {
214
- text: 'overflow', int: overflow, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
215
- }))).rejects.toThrow(); // Postgres should reject
216
- });
178
+ // Max int is 2147483647
179
+ const overflow = 3000000000;
180
+ await expect(repo.insert(Object.assign(new TypeEntity(), {
181
+ text: 'overflow', int: overflow, float: 0, bool: true, numericDate: 0, timestamp: new Date(), json: {}, tags: [],
182
+ }))).rejects.toThrow(); // Postgres should reject
217
183
  });
218
184
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.145",
3
+ "version": "0.93.146",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,7 +1,6 @@
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
+ import { getRepository } from '../../orm/server/index.js';
5
4
  import { TaskProcessResult, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
6
5
  import { PostgresTask } from '../../task-queue/postgres/index.js';
7
6
  import { setupIntegrationTest } from '../../testing/index.js';
@@ -11,8 +10,10 @@ describe('Worker & Base Class Tests', () => {
11
10
  let queue;
12
11
  let token;
13
12
  let otherQueueName;
13
+ let taskRepository;
14
14
  beforeAll(async () => {
15
15
  ({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
16
+ taskRepository = injector.resolve(getRepository(PostgresTask));
16
17
  otherQueueName = `other-queue-${crypto.randomUUID()}`;
17
18
  });
18
19
  beforeEach(() => {
@@ -25,11 +26,10 @@ describe('Worker & Base Class Tests', () => {
25
26
  });
26
27
  afterEach(async () => {
27
28
  token.set();
28
- const repository = runInInjectionContext(injector, () => injectRepository(PostgresTask));
29
29
  const namespace = queue.getTransactionalContextData().namespace;
30
30
  // Clear foreign keys
31
- await repository.updateManyByQuery({ namespace }, { parentId: null });
32
- await repository.updateManyByQuery({ namespace: otherQueueName }, { parentId: null });
31
+ await taskRepository.updateManyByQuery({ namespace }, { parentId: null });
32
+ await taskRepository.updateManyByQuery({ namespace: otherQueueName }, { parentId: null });
33
33
  await queue.clear();
34
34
  const queueProvider = injector.resolve(TaskQueueProvider);
35
35
  await queueProvider.get(otherQueueName).clear();
package/testing/README.md CHANGED
@@ -80,11 +80,16 @@ The `setupIntegrationTest` utility (found in `source/testing/integration-setup.t
80
80
  4. **Schema Isolation**: Automatically creates and uses PostgreSQL schemas for isolation.
81
81
  5. **Modules**: Optional configuration for `taskQueue`, `authentication`, `objectStorage`, `notification`, etc.
82
82
 
83
- ### Integration Test Example
83
+ Integration tests typically require an injection context to resolve services and repositories. You can use the `testInInjector` (or `itInInjector`) helper to automatically wrap your test body.
84
+
85
+ > [!IMPORTANT]
86
+ > `testInInjector` is designed for test files where a **single injector** is shared across all tests (typically initialized in `beforeAll`). It accepts a `ValueOrProvider<Injector>`, allowing you to pass a getter function (e.g., `() => injector`) if the injector variable is initialized late.
87
+ >
88
+ > If your tests require a fresh injector per test case (e.g. `setupIntegrationTest` in `beforeEach`), you can still use `testInInjector` by passing a provider.
84
89
 
85
90
  ```typescript
86
- import { beforeAll, describe, expect, it } from 'vitest';
87
- import { setupIntegrationTest } from '#/testing/index.js';
91
+ import { beforeAll, describe, expect } from 'vitest';
92
+ import { setupIntegrationTest, testInInjector } from '#/testing/index.js';
88
93
  import { MyService } from '../my-service.js';
89
94
 
90
95
  describe('MyService Integration', () => {
@@ -98,19 +103,32 @@ describe('MyService Integration', () => {
98
103
  }));
99
104
  });
100
105
 
101
- it('should process data through the database', async () => {
106
+ testInInjector('should process data through the database', injector, async () => {
102
107
  const service = injector.resolve(MyService);
103
108
  const result = await service.process();
104
109
  expect(result.success).toBe(true);
105
110
  });
111
+
112
+ // Using a provider function to defer resolution of the 'injector' variable when using `beforeEach` for injector setup
113
+ testInInjector('should process data through the database', () => injector, async () => { ... });
106
114
  });
107
115
  ```
108
116
 
109
117
  ## Best Practices & Guidelines
110
118
 
111
- ### 1. Isolation
112
-
113
- - **Schemas**: Always use a unique PostgreSQL schema for your integration test suite via `orm: { schema: '...' }`.
119
+ ### 1. Isolation & Performance
120
+
121
+ - **`beforeAll` vs `beforeEach`**:
122
+ - **Use `beforeAll` (Recommended)**: For most integration tests. It's significantly faster as it creates the schema and initializes the injector once.
123
+ - **Use `beforeEach`**: Use this only when:
124
+ - You need a completely clean Injector state for every test (e.g. if singletons are being mutated and cannot be reset).
125
+ - You are testing the initialization or disposal logic of your services.
126
+ - Each test requires a different module configuration in `setupIntegrationTest`.
127
+ - **Data Reset**:
128
+ - **`clearTenantData` (Best for Parallelism)**: If your tests use random `tenantId`s, use this in `afterEach` or `beforeEach`. It allows multiple tests within the same schema to run without interfering with each other's data.
129
+ - **`truncateTables` (Use with Caution)**: Only suitable if you are certain that no other tests are concurrently using the same database tables (even with different tenant IDs), as `TRUNCATE` affects the entire table and can cause flakiness in parallel environments.
130
+ - **Unit vs. Integration**: Prefer integration tests over unit tests when reasonably possible to cover more code paths and reduce the need for extensive mocking.
131
+ - **Parallelism**: Vitest runs test files in parallel. Always use unique resource names (e.g., using `crypto.randomUUID()`) to avoid flaky tests and cross-file interference.
114
132
  - **Resource Naming**: When testing shared infrastructure (Task Queues, S3 Buckets, Rate Limiters), append a random suffix or timestamp to the resource name (e.g., `const queueName = \`test-queue-${crypto.randomUUID()}\``).
115
133
  - **Cleanup**: Use `truncateTables` or `dropTables` in `beforeAll` or `beforeEach` to ensure a clean state.
116
134
  - **Tenant Data**: Use `clearTenantData` if your tests are multi-tenant and use random tenant IDs to avoid collisions when concurrently running tests.
@@ -124,24 +142,26 @@ describe('MyService Integration', () => {
124
142
  const mailServiceMock = { send: vi.fn() };
125
143
  injector.register(MailService, { useValue: mailServiceMock });
126
144
  ```
127
- - **Injection Context**: Any code that uses repositories or services relying on `inject()` outside of a class constructor/initializer **must** be wrapped in `runInInjectionContext(injector, async () => { ... })`.
145
+ - **Injection Context**: Any code that uses repositories or services relying on `inject()` outside of a class constructor/initializer **must** be wrapped in an injection context.
146
+ - **Preferred**: Use `testInInjector` (or `itInInjector`) for the entire test body.
147
+ - **Alternative**: Use `runInInjectionContext(injector, async () => { ... })` for specific blocks within a test or hook.
128
148
  - **Mock Reset**: Always call `vi.clearAllMocks()` in `beforeEach` if using spys or mocks to ensure test independence.
129
149
 
130
150
  ### 3. Integration Test Patterns
131
151
 
132
152
  - **Schema Management**:
133
153
  - **Preferred**: Use the `modules` option in `setupIntegrationTest`. This automatically runs the necessary migrations for standard modules (e.g., `taskQueue`, `authentication`, `notification`).
134
- - **Fallback**: Use Manual DDL via `database.execute(sql\`...\`)`**only** for test-specific entities (like local`@Table`classes defined inside your test file) that are not part of a standard module. Always call`dropTables` before creating them to ensure a fresh start.
154
+ - **Fallback**: Use Manual DDL via `database.execute(sql\`...\`)` **only** for test-specific entities (like local `@Table` classes defined inside your test file) that are not part of a standard module. Always call `dropTables` before creating them to ensure a fresh start.
155
+ - **Repositories**: You can inject ad-hoc repositories inside an injection context:
156
+ ```typescript
157
+ const repo = injectRepository(MyEntity);
158
+ ```
135
159
  - **Background Workers**: When testing workers or loops, use a `CancellationToken` to stop them in `afterEach` or `afterAll`.
160
+ - **Polling & Waiting**: When testing asynchronous background effects (like a task queue processing a task), avoid arbitrary `timeout()` calls if possible. Instead, use a polling loop or wait for a specific database state.
136
161
  - **Timeouts**: Use the `timeout(ms)` helper from `#/utils/timing.js` for tests involving refills, retries, or background processing. Prefer `vi.useFakeTimers()` only when real-time passage is too slow for the test suite.
137
162
  - **Logging**: Test should generally not output logs unless for active debugging. Use try-catch for expected errors and assert on the error message instead of relying on logs.
138
163
 
139
- ### 4. Performance
140
-
141
- - Use `beforeAll` for heavy setup (like schema and table creation) and `beforeEach` for lightweight data resetting (truncation).
142
- - Prefer integration tests over unit tests when reasonably possible to cover more code paths and reduce the need for extensive mocking and stubbing.
143
-
144
- ### 5. Coding Style
164
+ ### 4. Coding Style
145
165
 
146
166
  - **Explicit Types**: Use explicit return types for methods and functions.
147
167
  - **Async/Await**: Always use `async/await` for asynchronous code to improve readability and error handling.
@@ -149,9 +169,11 @@ describe('MyService Integration', () => {
149
169
  - **Avoid `any`**: Strive to use specific types or `unknown` if required to maintain type safety.
150
170
  - **Descriptive Names**: Use descriptive names for test files, cases and variables to improve readability.
151
171
 
152
- ### 6. Common Helpers (from `source/testing/`)
172
+ ### 5. Common Helpers (from `source/testing/`)
153
173
 
154
174
  - `setupIntegrationTest(options)`: Main entry point for integration tests.
175
+ - `testInInjector(name, injector, fn, options)`: Runs a test within an injection context.
176
+ - `itInInjector`: Alias for `testInInjector`.
155
177
  - `truncateTables(database, schema, tables)`: Clears data from specified tables.
156
178
  - `dropTables(database, schema, tables)`: Drops specified tables.
157
179
  - `clearTenantData(database, schema, tables, tenantId)`: Deletes data for a specific tenant.
@@ -1,4 +1,6 @@
1
+ /** biome-ignore-all lint/nursery/useExpect: helper file */
1
2
  import type { PoolConfig } from 'pg';
3
+ import { type TestOptions } from 'vitest';
2
4
  import { type AuthenticationAncillaryService } from '../authentication/server/index.js';
3
5
  import { Injector } from '../injector/index.js';
4
6
  import { LogLevel } from '../logger/index.js';
@@ -6,6 +8,7 @@ import { type S3ObjectStorageProviderConfig } from '../object-storage/s3/index.j
6
8
  import type { EntityType } from '../orm/entity.js';
7
9
  import { Database } from '../orm/server/index.js';
8
10
  import type { Type } from '../types/index.js';
11
+ import { type ValueOrProvider } from '../utils/value-or-provider.js';
9
12
  export type IntegrationTestOptions = {
10
13
  dbConfig?: Partial<PoolConfig>;
11
14
  orm?: {
@@ -43,6 +46,14 @@ export type TestContext = {
43
46
  * Standard setup for integration tests.
44
47
  */
45
48
  export declare function setupIntegrationTest(options?: IntegrationTestOptions): Promise<TestContext>;
49
+ /**
50
+ * A wrapper for vitest's `test` that automatically runs the test function in the provided injector's context.
51
+ * @param name The name of the test.
52
+ * @param injector The injector to use for the context.
53
+ * @param fn The test function.
54
+ * @param options Vitest test options.
55
+ */
56
+ export declare function testInInjector(name: string, injector: ValueOrProvider<Injector>, fn: () => any, options?: number | TestOptions): void;
46
57
  /**
47
58
  * Helper to truncate specific tables in a schema.
48
59
  * Useful in beforeEach() to reset state.