@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
@@ -1,5 +1,4 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from 'vitest';
2
- import { runInInjectionContext } from '../../injector/index.js';
3
2
  import { setupIntegrationTest, truncateTables } from '../../testing/index.js';
4
3
  import { NotificationTypeService } from '../server/services/notification-type.service.js';
5
4
  describe('NotificationTypeService', () => {
@@ -21,24 +20,22 @@ describe('NotificationTypeService', () => {
21
20
  [type1]: { label: 'Type 1' },
22
21
  [type2]: { label: 'Type 2', throttling: { limit: 1, interval: 1000 } }
23
22
  };
24
- await runInInjectionContext(injector, async () => {
25
- const result = await service.initializeTypes(typeData);
26
- expect(result[type1]?.label).toBe('Type 1');
27
- expect(result[type2]?.key).toBe(type2);
28
- expect(result[type2]?.throttling?.limit).toBe(1);
29
- // Verify persistence
30
- const dbTypes = await service.repository.loadManyByQuery({ key: { $in: [type1, type2] } });
31
- expect(dbTypes).toHaveLength(2);
32
- // Update
33
- const updatedData = {
34
- [type1]: { label: 'Type 1 Updated' },
35
- [type2]: { label: 'Type 2', throttling: { limit: 1, interval: 1000 } }
36
- };
37
- const resultUpdated = await service.initializeTypes(updatedData);
38
- expect(resultUpdated[type1]?.label).toBe('Type 1 Updated');
39
- const dbTypesUpdated = await service.repository.loadManyByQuery({ key: { $in: [type1, type2] } });
40
- expect(dbTypesUpdated).toHaveLength(2);
41
- expect(dbTypesUpdated.find((c) => c.key == type1)?.label).toBe('Type 1 Updated');
42
- });
23
+ const result = await service.initializeTypes(typeData);
24
+ expect(result[type1]?.label).toBe('Type 1');
25
+ expect(result[type2]?.key).toBe(type2);
26
+ expect(result[type2]?.throttling?.limit).toBe(1);
27
+ // Verify persistence
28
+ const dbTypes = await service.repository.loadManyByQuery({ key: { $in: [type1, type2] } });
29
+ expect(dbTypes).toHaveLength(2);
30
+ // Update
31
+ const updatedData = {
32
+ [type1]: { label: 'Type 1 Updated' },
33
+ [type2]: { label: 'Type 2', throttling: { limit: 1, interval: 1000 } }
34
+ };
35
+ const resultUpdated = await service.initializeTypes(updatedData);
36
+ expect(resultUpdated[type1]?.label).toBe('Type 1 Updated');
37
+ const dbTypesUpdated = await service.repository.loadManyByQuery({ key: { $in: [type1, type2] } });
38
+ expect(dbTypesUpdated).toHaveLength(2);
39
+ expect(dbTypesUpdated.find((c) => c.key == type1)?.label).toBe('Type 1 Updated');
43
40
  });
44
41
  });
@@ -9,15 +9,15 @@ 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 { Integer, 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 Query Complex (Integration)', () => {
19
18
  let injector;
20
19
  let db;
20
+ let repo;
21
21
  const schema = 'test_orm_query_complex';
22
22
  let QueryEntity = class QueryEntity extends Entity {
23
23
  tag;
@@ -40,14 +40,10 @@ describe('ORM Query Complex (Integration)', () => {
40
40
  Table('query_entities', { schema })
41
41
  ], QueryEntity);
42
42
  beforeAll(async () => {
43
- injector = new Injector('Test');
44
- configureOrm({
45
- repositoryConfig: { schema },
46
- connection: {
47
- host: '127.0.0.1', port: 5432, user: 'tstdl', password: 'wf7rq6glrk5jykne', database: 'tstdl',
48
- },
49
- });
50
- db = injector.resolve(Database);
43
+ ({ injector, database: db } = await setupIntegrationTest({
44
+ orm: { schema },
45
+ }));
46
+ repo = injector.resolve(getRepository(QueryEntity));
51
47
  await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
52
48
  await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('query_entities')} CASCADE`);
53
49
  await db.execute(sql `
@@ -66,138 +62,111 @@ describe('ORM Query Complex (Integration)', () => {
66
62
  });
67
63
  test('should support nested AND/OR logic', async () => {
68
64
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('query_entities')} CASCADE`);
69
- await runInInjectionContext(injector, async () => {
70
- const repo = injectRepository(QueryEntity);
71
- await repo.insertMany([
72
- Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }),
73
- Object.assign(new QueryEntity(), { tag: 'A', score: 20, status: 'inactive' }),
74
- Object.assign(new QueryEntity(), { tag: 'B', score: 10, status: 'active' }),
75
- Object.assign(new QueryEntity(), { tag: 'B', score: 20, status: 'inactive' }),
76
- ]);
77
- // (Tag A AND Active) OR (Tag B AND Inactive)
78
- const results = await repo.loadManyByQuery({
79
- $or: [
80
- { $and: [{ tag: 'A' }, { status: 'active' }] },
81
- { $and: [{ tag: 'B' }, { status: 'inactive' }] },
82
- ],
83
- });
84
- expect(results).toHaveLength(2);
85
- expect(results.some((r) => r.tag === 'A' && r.status === 'active')).toBe(true);
86
- expect(results.some((r) => r.tag === 'B' && r.status === 'inactive')).toBe(true);
65
+ await repo.insertMany([
66
+ Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }),
67
+ Object.assign(new QueryEntity(), { tag: 'A', score: 20, status: 'inactive' }),
68
+ Object.assign(new QueryEntity(), { tag: 'B', score: 10, status: 'active' }),
69
+ Object.assign(new QueryEntity(), { tag: 'B', score: 20, status: 'inactive' }),
70
+ ]);
71
+ // (Tag A AND Active) OR (Tag B AND Inactive)
72
+ const results = await repo.loadManyByQuery({
73
+ $or: [
74
+ { $and: [{ tag: 'A' }, { status: 'active' }] },
75
+ { $and: [{ tag: 'B' }, { status: 'inactive' }] },
76
+ ],
87
77
  });
78
+ expect(results).toHaveLength(2);
79
+ expect(results.some((r) => r.tag === 'A' && r.status === 'active')).toBe(true);
80
+ expect(results.some((r) => r.tag === 'B' && r.status === 'inactive')).toBe(true);
88
81
  });
89
82
  test('should support implicit AND', async () => {
90
83
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('query_entities')} CASCADE`);
91
- await runInInjectionContext(injector, async () => {
92
- const repo = injectRepository(QueryEntity);
93
- await repo.insert(Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }));
94
- const results = await repo.loadManyByQuery({
95
- tag: 'A',
96
- score: 10,
97
- status: 'active',
98
- });
99
- expect(results).toHaveLength(1);
84
+ await repo.insert(Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }));
85
+ const results = await repo.loadManyByQuery({
86
+ tag: 'A',
87
+ score: 10,
88
+ status: 'active',
100
89
  });
90
+ expect(results).toHaveLength(1);
101
91
  });
102
92
  test('should support NOT operator', async () => {
103
93
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('query_entities')} CASCADE`);
104
- await runInInjectionContext(injector, async () => {
105
- const repo = injectRepository(QueryEntity);
106
- await repo.insertMany([
107
- Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }),
108
- Object.assign(new QueryEntity(), { tag: 'B', score: 20, status: 'inactive' }),
109
- ]);
110
- const results = await repo.loadManyByQuery({
111
- tag: { $not: { $eq: 'A' } },
112
- });
113
- expect(results).toHaveLength(1);
114
- expect(results[0].tag).toBe('B');
94
+ await repo.insertMany([
95
+ Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }),
96
+ Object.assign(new QueryEntity(), { tag: 'B', score: 20, status: 'inactive' }),
97
+ ]);
98
+ const results = await repo.loadManyByQuery({
99
+ tag: { $not: { $eq: 'A' } },
115
100
  });
101
+ expect(results).toHaveLength(1);
102
+ expect(results[0].tag).toBe('B');
116
103
  });
117
104
  test('should support IN operator with empty array (should match nothing)', async () => {
118
105
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('query_entities')} CASCADE`);
119
- await runInInjectionContext(injector, async () => {
120
- const repo = injectRepository(QueryEntity);
121
- await repo.insert(Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }));
122
- const results = await repo.loadManyByQuery({
123
- tag: { $in: [] },
124
- });
125
- expect(results).toHaveLength(0);
106
+ await repo.insert(Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }));
107
+ const results = await repo.loadManyByQuery({
108
+ tag: { $in: [] },
126
109
  });
110
+ expect(results).toHaveLength(0);
127
111
  });
128
112
  test('should support NOT IN operator with empty array (should match everything)', async () => {
129
113
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('query_entities')} CASCADE`);
130
- await runInInjectionContext(injector, async () => {
131
- const repo = injectRepository(QueryEntity);
132
- await repo.insert(Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }));
133
- const results = await repo.loadManyByQuery({
134
- tag: { $nin: [] },
135
- });
136
- expect(results).toHaveLength(1);
114
+ await repo.insert(Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }));
115
+ const results = await repo.loadManyByQuery({
116
+ tag: { $nin: [] },
137
117
  });
118
+ expect(results).toHaveLength(1);
138
119
  });
139
120
  test('should support REGEX operator', async () => {
140
121
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('query_entities')} CASCADE`);
141
- await runInInjectionContext(injector, async () => {
142
- const repo = injectRepository(QueryEntity);
143
- await repo.insertMany([
144
- Object.assign(new QueryEntity(), { tag: 'apple', score: 10, status: '' }),
145
- Object.assign(new QueryEntity(), { tag: 'banana', score: 10, status: '' }),
146
- Object.assign(new QueryEntity(), { tag: 'apricot', score: 10, status: '' }),
147
- ]);
148
- const results = await repo.loadManyByQuery({
149
- tag: { $regex: '^ap' },
150
- });
151
- expect(results).toHaveLength(2); // apple, apricot
122
+ await repo.insertMany([
123
+ Object.assign(new QueryEntity(), { tag: 'apple', score: 10, status: '' }),
124
+ Object.assign(new QueryEntity(), { tag: 'banana', score: 10, status: '' }),
125
+ Object.assign(new QueryEntity(), { tag: 'apricot', score: 10, status: '' }),
126
+ ]);
127
+ const results = await repo.loadManyByQuery({
128
+ tag: { $regex: '^ap' },
152
129
  });
130
+ expect(results).toHaveLength(2); // apple, apricot
153
131
  });
154
132
  test('should support complex combination of logic and comparison', async () => {
155
133
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('query_entities')} CASCADE`);
156
- await runInInjectionContext(injector, async () => {
157
- const repo = injectRepository(QueryEntity);
158
- await repo.insertMany([
159
- Object.assign(new QueryEntity(), { tag: 'A', score: 5, status: 'ok' }),
160
- Object.assign(new QueryEntity(), { tag: 'A', score: 15, status: 'ok' }),
161
- Object.assign(new QueryEntity(), { tag: 'A', score: 25, status: 'fail' }),
162
- Object.assign(new QueryEntity(), { tag: 'B', score: 5, status: 'ok' }),
163
- ]);
164
- // Tag A AND (Score > 10 AND Score < 20) AND Status != fail
165
- const results = await repo.loadManyByQuery({
166
- tag: 'A',
167
- $and: [{ score: { $gt: 10 } }, { score: { $lt: 20 } }],
168
- status: { $neq: 'fail' },
169
- });
170
- expect(results).toHaveLength(1);
171
- expect(results[0].score).toBe(15);
134
+ await repo.insertMany([
135
+ Object.assign(new QueryEntity(), { tag: 'A', score: 5, status: 'ok' }),
136
+ Object.assign(new QueryEntity(), { tag: 'A', score: 15, status: 'ok' }),
137
+ Object.assign(new QueryEntity(), { tag: 'A', score: 25, status: 'fail' }),
138
+ Object.assign(new QueryEntity(), { tag: 'B', score: 5, status: 'ok' }),
139
+ ]);
140
+ // Tag A AND (Score > 10 AND Score < 20) AND Status != fail
141
+ const results = await repo.loadManyByQuery({
142
+ tag: 'A',
143
+ $and: [{ score: { $gt: 10 } }, { score: { $lt: 20 } }],
144
+ status: { $neq: 'fail' },
172
145
  });
146
+ expect(results).toHaveLength(1);
147
+ expect(results[0].score).toBe(15);
173
148
  });
174
149
  test('should support NULL check', async () => {
175
150
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('query_entities')} CASCADE`);
176
- await runInInjectionContext(injector, async () => {
177
- const repo = injectRepository(QueryEntity);
178
- // Manually insert null status (violates NOT NULL usually, but let's assume nullable if we could change schema,
179
- // but Entity defines it as string. We can query for null even if no rows can be null (result 0) OR if we use a nullable column).
180
- // 'attributes' is JSONB and can be queried.
181
- // But query-converter maps properties.
182
- // Let's use a non-existent value for $eq which is different from null.
183
- // But checking $eq: null generates IS NULL.
184
- // Since our columns are NOT NULL, it should return empty.
185
- await repo.insert(Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }));
186
- const results = await repo.loadManyByQuery({
187
- status: { $eq: null },
188
- });
189
- expect(results).toHaveLength(0);
151
+ // Manually insert null status (violates NOT NULL usually, but let's assume nullable if we could change schema,
152
+ // but Entity defines it as string. We can query for null even if no rows can be null (result 0) OR if we use a nullable column).
153
+ // 'attributes' is JSONB and can be queried.
154
+ // But query-converter maps properties.
155
+ // Let's use a non-existent value for $eq which is different from null.
156
+ // But checking $eq: null generates IS NULL.
157
+ // Since our columns are NOT NULL, it should return empty.
158
+ await repo.insert(Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }));
159
+ const results = await repo.loadManyByQuery({
160
+ status: { $eq: null },
190
161
  });
162
+ expect(results).toHaveLength(0);
191
163
  });
192
164
  test('should support NOT NULL check', async () => {
193
165
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('query_entities')} CASCADE`);
194
- await runInInjectionContext(injector, async () => {
195
- const repo = injectRepository(QueryEntity);
196
- await repo.insert(Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }));
197
- const results = await repo.loadManyByQuery({
198
- status: { $neq: null },
199
- });
200
- expect(results).toHaveLength(1);
166
+ await repo.insert(Object.assign(new QueryEntity(), { tag: 'A', score: 10, status: 'active' }));
167
+ const results = await repo.loadManyByQuery({
168
+ status: { $neq: null },
201
169
  });
170
+ expect(results).toHaveLength(1);
202
171
  });
203
172
  });
@@ -7,18 +7,18 @@ 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 { Integer, StringProperty } from '../../schema/index.js';
12
- import { toArrayAsync } from '../../utils/async-iterable-helpers/to-array.js';
13
10
  import { sql } from 'drizzle-orm';
14
11
  import { beforeAll, describe, expect, test } from 'vitest';
12
+ import { Integer, StringProperty } from '../../schema/index.js';
13
+ import { setupIntegrationTest } from '../../testing/index.js';
14
+ import { toArrayAsync } from '../../utils/async-iterable-helpers/to-array.js';
15
15
  import { Table } from '../decorators.js';
16
16
  import { Entity } from '../entity.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 Advanced (Integration)', () => {
20
19
  let injector;
21
20
  let db;
21
+ let repository;
22
22
  const schema = 'test_orm_advanced';
23
23
  let AdvancedEntity = class AdvancedEntity extends Entity {
24
24
  name;
@@ -36,14 +36,10 @@ describe('ORM Repository Advanced (Integration)', () => {
36
36
  Table('advanced_entities', { schema })
37
37
  ], AdvancedEntity);
38
38
  beforeAll(async () => {
39
- injector = new Injector('Test');
40
- configureOrm({
41
- repositoryConfig: { schema },
42
- connection: {
43
- host: '127.0.0.1', port: 5432, user: 'tstdl', password: 'wf7rq6glrk5jykne', database: 'tstdl',
44
- },
45
- });
46
- db = injector.resolve(Database);
39
+ ({ injector, database: db } = await setupIntegrationTest({
40
+ orm: { schema },
41
+ }));
42
+ repository = injector.resolve(getRepository(AdvancedEntity));
47
43
  await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
48
44
  await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
49
45
  await db.execute(sql `
@@ -61,172 +57,133 @@ describe('ORM Repository Advanced (Integration)', () => {
61
57
  });
62
58
  test('should support loadManyCursor', async () => {
63
59
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
64
- await runInInjectionContext(injector, async () => {
65
- const repository = injectRepository(AdvancedEntity);
66
- const e1 = await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }));
67
- const e2 = await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E2', value: 2 }));
68
- const cursor = repository.loadManyCursor([e1.id, e2.id]);
69
- const results = await toArrayAsync(cursor);
70
- expect(results).toHaveLength(2);
71
- expect(results.map((r) => r.name).sort()).toEqual(['E1', 'E2']);
72
- });
60
+ const e1 = await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }));
61
+ const e2 = await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E2', value: 2 }));
62
+ const cursor = repository.loadManyCursor([e1.id, e2.id]);
63
+ const results = await toArrayAsync(cursor);
64
+ expect(results).toHaveLength(2);
65
+ expect(results.map((r) => r.name).sort()).toEqual(['E1', 'E2']);
73
66
  });
74
67
  test('should support loadAllCursor', async () => {
75
68
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
76
- await runInInjectionContext(injector, async () => {
77
- const repository = injectRepository(AdvancedEntity);
78
- await repository.insertMany([
79
- Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }),
80
- Object.assign(new AdvancedEntity(), { name: 'E2', value: 2 }),
81
- ]);
82
- const cursor = repository.loadAllCursor();
83
- const results = await toArrayAsync(cursor);
84
- expect(results).toHaveLength(2);
85
- });
69
+ await repository.insertMany([
70
+ Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }),
71
+ Object.assign(new AdvancedEntity(), { name: 'E2', value: 2 }),
72
+ ]);
73
+ const cursor = repository.loadAllCursor();
74
+ const results = await toArrayAsync(cursor);
75
+ expect(results).toHaveLength(2);
86
76
  });
87
77
  test('should support countByQuery with various filters', async () => {
88
78
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
89
- await runInInjectionContext(injector, async () => {
90
- const repository = injectRepository(AdvancedEntity);
91
- await repository.insertMany([
92
- Object.assign(new AdvancedEntity(), { name: 'A', value: 10 }),
93
- Object.assign(new AdvancedEntity(), { name: 'B', value: 20 }),
94
- Object.assign(new AdvancedEntity(), { name: 'C', value: 30 }),
95
- ]);
96
- expect(await repository.countByQuery({ value: { $gt: 15 } })).toBe(2);
97
- expect(await repository.countByQuery({ value: { $lt: 25 } })).toBe(2);
98
- expect(await repository.countByQuery({ name: 'A' })).toBe(1);
99
- });
79
+ await repository.insertMany([
80
+ Object.assign(new AdvancedEntity(), { name: 'A', value: 10 }),
81
+ Object.assign(new AdvancedEntity(), { name: 'B', value: 20 }),
82
+ Object.assign(new AdvancedEntity(), { name: 'C', value: 30 }),
83
+ ]);
84
+ expect(await repository.countByQuery({ value: { $gt: 15 } })).toBe(2);
85
+ expect(await repository.countByQuery({ value: { $lt: 25 } })).toBe(2);
86
+ expect(await repository.countByQuery({ name: 'A' })).toBe(1);
100
87
  });
101
88
  test('should support has and hasByQuery', async () => {
102
89
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
103
- await runInInjectionContext(injector, async () => {
104
- const repository = injectRepository(AdvancedEntity);
105
- const e1 = await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }));
106
- expect(await repository.has(e1.id)).toBe(true);
107
- expect(await repository.has('00000000-0000-0000-0000-000000000000')).toBe(false);
108
- expect(await repository.hasByQuery({ name: 'E1' })).toBe(true);
109
- expect(await repository.hasByQuery({ name: 'E2' })).toBe(false);
110
- });
90
+ const e1 = await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }));
91
+ expect(await repository.has(e1.id)).toBe(true);
92
+ expect(await repository.has('00000000-0000-0000-0000-000000000000')).toBe(false);
93
+ expect(await repository.hasByQuery({ name: 'E1' })).toBe(true);
94
+ expect(await repository.hasByQuery({ name: 'E2' })).toBe(false);
111
95
  });
112
96
  test('should support tryUpdate', async () => {
113
97
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
114
- await runInInjectionContext(injector, async () => {
115
- const repository = injectRepository(AdvancedEntity);
116
- const e1 = await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }));
117
- const updated = await repository.tryUpdate(e1.id, { name: 'E1-Updated' });
118
- expect(updated.name).toBe('E1-Updated');
119
- const missing = await repository.tryUpdate('00000000-0000-0000-0000-000000000000', { name: 'X' });
120
- expect(missing).toBeUndefined();
121
- });
98
+ const e1 = await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }));
99
+ const updated = await repository.tryUpdate(e1.id, { name: 'E1-Updated' });
100
+ expect(updated.name).toBe('E1-Updated');
101
+ const missing = await repository.tryUpdate('00000000-0000-0000-0000-000000000000', { name: 'X' });
102
+ expect(missing).toBeUndefined();
122
103
  });
123
104
  test('should support tryUpdateByQuery', async () => {
124
105
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
125
- await runInInjectionContext(injector, async () => {
126
- const repository = injectRepository(AdvancedEntity);
127
- await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }));
128
- const updated = await repository.tryUpdateByQuery({ name: 'E1' }, { value: 100 });
129
- expect(updated.value).toBe(100);
130
- const missing = await repository.tryUpdateByQuery({ name: 'Missing' }, { value: 0 });
131
- expect(missing).toBeUndefined();
132
- });
106
+ await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }));
107
+ const updated = await repository.tryUpdateByQuery({ name: 'E1' }, { value: 100 });
108
+ expect(updated.value).toBe(100);
109
+ const missing = await repository.tryUpdateByQuery({ name: 'Missing' }, { value: 0 });
110
+ expect(missing).toBeUndefined();
133
111
  });
134
112
  test('should support insertIfNotExists', async () => {
135
113
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
136
- await runInInjectionContext(injector, async () => {
137
- const repository = injectRepository(AdvancedEntity);
138
- const e1 = await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }));
139
- // Conflict on ID
140
- const conflict = await repository.insertIfNotExists('id', Object.assign(new AdvancedEntity(), { id: e1.id, name: 'Conflict', value: 2 }));
141
- expect(conflict).toBeUndefined();
142
- // No conflict
143
- const success = await repository.insertIfNotExists('id', Object.assign(new AdvancedEntity(), { name: 'E2', value: 2 }));
144
- expect(success).toBeDefined();
145
- expect(success.name).toBe('E2');
146
- });
114
+ const e1 = await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }));
115
+ // Conflict on ID
116
+ const conflict = await repository.insertIfNotExists('id', Object.assign(new AdvancedEntity(), { id: e1.id, name: 'Conflict', value: 2 }));
117
+ expect(conflict).toBeUndefined();
118
+ // No conflict
119
+ const success = await repository.insertIfNotExists('id', Object.assign(new AdvancedEntity(), { name: 'E2', value: 2 }));
120
+ expect(success).toBeDefined();
121
+ expect(success.name).toBe('E2');
147
122
  });
148
123
  test('should support pagination (limit/offset)', async () => {
149
124
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
150
- await runInInjectionContext(injector, async () => {
151
- const repository = injectRepository(AdvancedEntity);
152
- await repository.insertMany([
153
- Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }),
154
- Object.assign(new AdvancedEntity(), { name: 'E2', value: 2 }),
155
- Object.assign(new AdvancedEntity(), { name: 'E3', value: 3 }),
156
- ]);
157
- const p1 = await repository.loadAll({ limit: 2, order: { value: 'asc' } });
158
- expect(p1).toHaveLength(2);
159
- expect(p1[0].name).toBe('E1');
160
- expect(p1[1].name).toBe('E2');
161
- const p2 = await repository.loadAll({ limit: 2, offset: 2, order: { value: 'asc' } });
162
- expect(p2).toHaveLength(1);
163
- expect(p2[0].name).toBe('E3');
164
- });
125
+ await repository.insertMany([
126
+ Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }),
127
+ Object.assign(new AdvancedEntity(), { name: 'E2', value: 2 }),
128
+ Object.assign(new AdvancedEntity(), { name: 'E3', value: 3 }),
129
+ ]);
130
+ const p1 = await repository.loadAll({ limit: 2, order: { value: 'asc' } });
131
+ expect(p1).toHaveLength(2);
132
+ expect(p1[0].name).toBe('E1');
133
+ expect(p1[1].name).toBe('E2');
134
+ const p2 = await repository.loadAll({ limit: 2, offset: 2, order: { value: 'asc' } });
135
+ expect(p2).toHaveLength(1);
136
+ expect(p2[0].name).toBe('E3');
165
137
  });
166
138
  test('should support complex ordering', async () => {
167
139
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
168
- await runInInjectionContext(injector, async () => {
169
- const repository = injectRepository(AdvancedEntity);
170
- await repository.insertMany([
171
- Object.assign(new AdvancedEntity(), { name: 'B', value: 10 }),
172
- Object.assign(new AdvancedEntity(), { name: 'A', value: 20 }),
173
- Object.assign(new AdvancedEntity(), { name: 'A', value: 10 }),
174
- ]);
175
- const results = await repository.loadAll({
176
- order: [
177
- ['name', 'asc'],
178
- ['value', 'desc'],
179
- ],
180
- });
181
- expect(results[0].name).toBe('A');
182
- expect(results[0].value).toBe(20);
183
- expect(results[1].name).toBe('A');
184
- expect(results[1].value).toBe(10);
185
- expect(results[2].name).toBe('B');
140
+ await repository.insertMany([
141
+ Object.assign(new AdvancedEntity(), { name: 'B', value: 10 }),
142
+ Object.assign(new AdvancedEntity(), { name: 'A', value: 20 }),
143
+ Object.assign(new AdvancedEntity(), { name: 'A', value: 10 }),
144
+ ]);
145
+ const results = await repository.loadAll({
146
+ order: [
147
+ ['name', 'asc'],
148
+ ['value', 'desc'],
149
+ ],
186
150
  });
151
+ expect(results[0].name).toBe('A');
152
+ expect(results[0].value).toBe(20);
153
+ expect(results[1].name).toBe('A');
154
+ expect(results[1].value).toBe(10);
155
+ expect(results[2].name).toBe('B');
187
156
  });
188
157
  test('should return empty array for empty loadMany', async () => {
189
- await runInInjectionContext(injector, async () => {
190
- const repository = injectRepository(AdvancedEntity);
191
- const results = await repository.loadMany([]);
192
- expect(results).toEqual([]);
193
- });
158
+ const results = await repository.loadMany([]);
159
+ expect(results).toEqual([]);
194
160
  });
195
161
  test('should fail on duplicate ID insert', async () => {
196
162
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
197
- await runInInjectionContext(injector, async () => {
198
- const repository = injectRepository(AdvancedEntity);
199
- const id = '00000000-0000-0000-0000-000000000001';
200
- await repository.insert(Object.assign(new AdvancedEntity(), { id, name: 'E1', value: 1 }));
201
- await expect(repository.insert(Object.assign(new AdvancedEntity(), { id, name: 'E2', value: 2 }))).rejects.toThrow();
202
- });
163
+ const id = '00000000-0000-0000-0000-000000000001';
164
+ await repository.insert(Object.assign(new AdvancedEntity(), { id, name: 'E1', value: 1 }));
165
+ await expect(repository.insert(Object.assign(new AdvancedEntity(), { id, name: 'E2', value: 2 }))).rejects.toThrow();
203
166
  });
204
167
  test('should support explicit transaction usage', async () => {
205
168
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
206
- await runInInjectionContext(injector, async () => {
207
- const repository = injectRepository(AdvancedEntity);
208
- await repository.transaction(async (tx) => {
209
- const txRepo = repository.withTransaction(tx);
210
- await txRepo.insert(Object.assign(new AdvancedEntity(), { name: 'TxEntity', value: 100 }));
211
- // Should be visible in tx
212
- expect(await txRepo.count()).toBe(1);
213
- });
214
- // Should be committed
215
- expect(await repository.count()).toBe(1);
169
+ await repository.transaction(async (tx) => {
170
+ const txRepo = repository.withTransaction(tx);
171
+ await txRepo.insert(Object.assign(new AdvancedEntity(), { name: 'TxEntity', value: 100 }));
172
+ // Should be visible in tx
173
+ expect(await txRepo.count()).toBe(1);
216
174
  });
175
+ // Should be committed
176
+ expect(await repository.count()).toBe(1);
217
177
  });
218
178
  test('should exclude soft-deleted entities by default', async () => {
219
179
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('advanced_entities')} CASCADE`);
220
- await runInInjectionContext(injector, async () => {
221
- const repository = injectRepository(AdvancedEntity);
222
- const e1 = await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }));
223
- await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E2', value: 2 }));
224
- await repository.delete(e1.id);
225
- const all = await repository.loadAll();
226
- expect(all).toHaveLength(1);
227
- expect(all[0].name).toBe('E2');
228
- const withDeleted = await repository.loadManyByQuery({}, { withDeleted: true });
229
- expect(withDeleted).toHaveLength(2);
230
- });
180
+ const e1 = await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E1', value: 1 }));
181
+ await repository.insert(Object.assign(new AdvancedEntity(), { name: 'E2', value: 2 }));
182
+ await repository.delete(e1.id);
183
+ const all = await repository.loadAll();
184
+ expect(all).toHaveLength(1);
185
+ expect(all[0].name).toBe('E2');
186
+ const withDeleted = await repository.loadManyByQuery({}, { withDeleted: true });
187
+ expect(withDeleted).toHaveLength(2);
231
188
  });
232
189
  });