@tstdl/base 0.93.145 → 0.93.147

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 (44) 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/repository.types.d.ts +13 -2
  8. package/orm/server/repository.d.ts +60 -4
  9. package/orm/server/repository.js +126 -25
  10. package/orm/tests/query-complex.test.js +80 -111
  11. package/orm/tests/repository-advanced.test.js +100 -143
  12. package/orm/tests/repository-attributes.test.js +30 -39
  13. package/orm/tests/repository-compound-primary-key.test.js +67 -75
  14. package/orm/tests/repository-comprehensive.test.js +76 -101
  15. package/orm/tests/repository-coverage.test.d.ts +1 -0
  16. package/orm/tests/repository-coverage.test.js +88 -149
  17. package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
  18. package/orm/tests/repository-cti-extensive.test.js +118 -147
  19. package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
  20. package/orm/tests/repository-cti-mapping.test.js +29 -42
  21. package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
  22. package/orm/tests/repository-cti-soft-delete.test.js +25 -37
  23. package/orm/tests/repository-cti-transactions.test.js +19 -33
  24. package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
  25. package/orm/tests/repository-cti-upsert-many.test.js +38 -50
  26. package/orm/tests/repository-cti.test.d.ts +1 -0
  27. package/orm/tests/repository-cti.test.js +195 -247
  28. package/orm/tests/repository-expiration.test.d.ts +1 -0
  29. package/orm/tests/repository-expiration.test.js +46 -59
  30. package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
  31. package/orm/tests/repository-extra-coverage.test.js +195 -337
  32. package/orm/tests/repository-mapping.test.d.ts +1 -0
  33. package/orm/tests/repository-mapping.test.js +20 -20
  34. package/orm/tests/repository-regression.test.js +124 -163
  35. package/orm/tests/repository-search.test.js +30 -44
  36. package/orm/tests/repository-soft-delete.test.js +54 -79
  37. package/orm/tests/repository-types.test.js +77 -111
  38. package/orm/tests/repository-undelete.test.d.ts +2 -0
  39. package/orm/tests/repository-undelete.test.js +201 -0
  40. package/package.json +3 -3
  41. package/task-queue/tests/worker.test.js +5 -5
  42. package/testing/README.md +38 -16
  43. package/testing/integration-setup.d.ts +11 -0
  44. package/testing/integration-setup.js +57 -30
@@ -7,18 +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
- import { dropTables, setupIntegrationTest, truncateTables } from '../../testing/index.js';
13
10
  import { sql } from 'drizzle-orm';
14
11
  import { beforeAll, beforeEach, describe, expect, test } from 'vitest';
12
+ import { StringProperty } from '../../schema/index.js';
13
+ import { dropTables, setupIntegrationTest, truncateTables } from '../../testing/index.js';
15
14
  import { Table } from '../decorators.js';
16
15
  import { Entity } from '../entity.js';
17
- import { Database } from '../server/index.js';
18
- import { injectRepository } from '../server/repository.js';
16
+ import { getRepository } from '../server/index.js';
19
17
  describe('ORM Repository Attributes (Integration)', () => {
20
18
  let injector;
21
19
  let database;
20
+ let repository;
22
21
  const schema = 'test_orm_attributes';
23
22
  let AttributeEntity = class AttributeEntity extends Entity {
24
23
  name;
@@ -32,6 +31,7 @@ describe('ORM Repository Attributes (Integration)', () => {
32
31
  ], AttributeEntity);
33
32
  beforeAll(async () => {
34
33
  ({ injector, database } = await setupIntegrationTest({ orm: { schema } }));
34
+ repository = injector.resolve(getRepository(AttributeEntity));
35
35
  await dropTables(database, schema, ['attribute_entities']);
36
36
  await database.execute(sql `
37
37
  CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('attribute_entities')} (
@@ -49,44 +49,35 @@ describe('ORM Repository Attributes (Integration)', () => {
49
49
  await truncateTables(database, schema, ['attribute_entities']);
50
50
  });
51
51
  test('should support partial attribute updates', async () => {
52
- await runInInjectionContext(injector, async () => {
53
- const repository = injectRepository(AttributeEntity);
54
- const e1 = await repository.insert(Object.assign(new AttributeEntity(), {
55
- name: 'E1',
56
- metadata: { attributes: { a: 1, b: 2 } },
57
- }));
58
- expect(e1.metadata.attributes).toEqual({ a: 1, b: 2 });
59
- const updated = await repository.update(e1.id, {
60
- metadata: { attributes: { b: 3, c: 4 } },
61
- });
62
- expect(updated.metadata.attributes).toEqual({ a: 1, b: 3, c: 4 });
52
+ const e1 = await repository.insert(Object.assign(new AttributeEntity(), {
53
+ name: 'E1',
54
+ metadata: { attributes: { a: 1, b: 2 } },
55
+ }));
56
+ expect(e1.metadata.attributes).toEqual({ a: 1, b: 2 });
57
+ const updated = await repository.update(e1.id, {
58
+ metadata: { attributes: { b: 3, c: 4 } },
63
59
  });
60
+ expect(updated.metadata.attributes).toEqual({ a: 1, b: 3, c: 4 });
64
61
  });
65
62
  test('should update attributes during soft delete', async () => {
66
- await runInInjectionContext(injector, async () => {
67
- const repository = injectRepository(AttributeEntity);
68
- const e1 = await repository.insert(Object.assign(new AttributeEntity(), {
69
- name: 'E1',
70
- metadata: { attributes: { initial: true } },
71
- }));
72
- const deleted = await repository.delete(e1.id, undefined, { attributes: { deleted: true } });
73
- expect(deleted.metadata.attributes).toEqual({ initial: true, deleted: true });
74
- });
63
+ const e1 = await repository.insert(Object.assign(new AttributeEntity(), {
64
+ name: 'E1',
65
+ metadata: { attributes: { initial: true } },
66
+ }));
67
+ const deleted = await repository.delete(e1.id, undefined, { attributes: { deleted: true } });
68
+ expect(deleted.metadata.attributes).toEqual({ initial: true, deleted: true });
75
69
  });
76
70
  test('should support querying by raw SQL on attributes', async () => {
77
- await runInInjectionContext(injector, async () => {
78
- const repository = injectRepository(AttributeEntity);
79
- await repository.insert(Object.assign(new AttributeEntity(), {
80
- name: 'E1',
81
- metadata: { attributes: { key: 'value1' } },
82
- }));
83
- await repository.insert(Object.assign(new AttributeEntity(), {
84
- name: 'E2',
85
- metadata: { attributes: { key: 'value2' } },
86
- }));
87
- const results = await repository.loadManyByQuery(sql `attributes->>'key' = 'value1'`);
88
- expect(results).toHaveLength(1);
89
- expect(results[0].name).toBe('E1');
90
- });
71
+ await repository.insert(Object.assign(new AttributeEntity(), {
72
+ name: 'E1',
73
+ metadata: { attributes: { key: 'value1' } },
74
+ }));
75
+ await repository.insert(Object.assign(new AttributeEntity(), {
76
+ name: 'E2',
77
+ metadata: { attributes: { key: 'value2' } },
78
+ }));
79
+ const results = await repository.loadManyByQuery(sql `attributes->>'key' = 'value1'`);
80
+ expect(results).toHaveLength(1);
81
+ expect(results[0].name).toBe('E1');
91
82
  });
92
83
  });
@@ -8,19 +8,22 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
- import { Injector, runInInjectionContext } from '../../injector/index.js';
12
- import { StringProperty } from '../../schema/index.js';
13
- import { dropTables, setupIntegrationTest } from '../../testing/index.js';
14
11
  import { sql } from 'drizzle-orm';
15
12
  import { getTableConfig } from 'drizzle-orm/pg-core';
16
13
  import { beforeAll, beforeEach, describe, expect, test } from 'vitest';
14
+ import { StringProperty } from '../../schema/index.js';
15
+ import { dropTables, setupIntegrationTest } from '../../testing/index.js';
17
16
  import { ChildEntity, Inheritance, PrimaryKey, Table } from '../decorators.js';
18
17
  import { Entity } from '../entity.js';
19
18
  import { getDrizzleTableFromType } from '../server/drizzle/index.js';
20
- import { Database, injectRepository } from '../server/index.js';
19
+ import { getRepository } from '../server/index.js';
21
20
  describe('ORM Repository Compound Primary Key (Integration)', () => {
22
21
  let injector;
23
22
  let database;
23
+ let compoundRepo;
24
+ let parentRepo;
25
+ let childRepo;
26
+ let grandchildRepo;
24
27
  const schema = 'test_orm_compound_pk';
25
28
  let CompoundEntity = class CompoundEntity extends Entity {
26
29
  namespace;
@@ -79,6 +82,10 @@ describe('ORM Repository Compound Primary Key (Integration)', () => {
79
82
  ], GrandchildEntity);
80
83
  beforeAll(async () => {
81
84
  ({ injector, database } = await setupIntegrationTest({ orm: { schema } }));
85
+ compoundRepo = injector.resolve(getRepository(CompoundEntity));
86
+ parentRepo = injector.resolve(getRepository(ParentEntity));
87
+ childRepo = injector.resolve(getRepository(ChildEntityModel));
88
+ grandchildRepo = injector.resolve(getRepository(GrandchildEntity));
82
89
  await database.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
83
90
  await dropTables(database, schema, ['grandchild_entities', 'child_entities', 'parent_entities', 'compound_entities']);
84
91
  await database.execute(sql `
@@ -134,85 +141,70 @@ describe('ORM Repository Compound Primary Key (Integration)', () => {
134
141
  await database.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('grandchild_entities')}, ${sql.identifier(schema)}.${sql.identifier('child_entities')}, ${sql.identifier(schema)}.${sql.identifier('parent_entities')}, ${sql.identifier(schema)}.${sql.identifier('compound_entities')} CASCADE`);
135
142
  });
136
143
  test('should insert and load compound entity', async () => {
137
- await runInInjectionContext(injector, async () => {
138
- const repository = injectRepository(CompoundEntity);
139
- const entity = new CompoundEntity();
140
- entity.id = '00000000-0000-0000-0000-000000000001';
141
- entity.namespace = 'ns1';
142
- entity.data = 'test data';
143
- await repository.insert(entity);
144
- const loaded = await repository.loadByQuery({ namespace: 'ns1', id: '00000000-0000-0000-0000-000000000001' });
145
- expect(loaded.data).toBe('test data');
146
- expect(loaded.namespace).toBe('ns1');
147
- expect(loaded.id).toBe('00000000-0000-0000-0000-000000000001');
148
- });
144
+ const entity = new CompoundEntity();
145
+ entity.id = '00000000-0000-0000-0000-000000000001';
146
+ entity.namespace = 'ns1';
147
+ entity.data = 'test data';
148
+ await compoundRepo.insert(entity);
149
+ const loaded = await compoundRepo.loadByQuery({ namespace: 'ns1', id: '00000000-0000-0000-0000-000000000001' });
150
+ expect(loaded.data).toBe('test data');
151
+ expect(loaded.namespace).toBe('ns1');
152
+ expect(loaded.id).toBe('00000000-0000-0000-0000-000000000001');
149
153
  });
150
154
  test('should upsert compound entity', async () => {
151
- await runInInjectionContext(injector, async () => {
152
- const repository = injectRepository(CompoundEntity);
153
- const entity = new CompoundEntity();
154
- entity.id = '00000000-0000-0000-0000-000000000010';
155
- entity.namespace = 'ns1';
156
- entity.data = 'initial data';
157
- await repository.insert(entity);
158
- const updateEntity = new CompoundEntity();
159
- updateEntity.id = '00000000-0000-0000-0000-000000000010';
160
- updateEntity.namespace = 'ns1';
161
- updateEntity.data = 'updated data';
162
- await repository.upsert(['namespace', 'id'], updateEntity);
163
- const loaded = await repository.loadByQuery({ namespace: 'ns1', id: '00000000-0000-0000-0000-000000000010' });
164
- expect(loaded.data).toBe('updated data');
165
- });
155
+ const entity = new CompoundEntity();
156
+ entity.id = '00000000-0000-0000-0000-000000000010';
157
+ entity.namespace = 'ns1';
158
+ entity.data = 'initial data';
159
+ await compoundRepo.insert(entity);
160
+ const updateEntity = new CompoundEntity();
161
+ updateEntity.id = '00000000-0000-0000-0000-000000000010';
162
+ updateEntity.namespace = 'ns1';
163
+ updateEntity.data = 'updated data';
164
+ await compoundRepo.upsert(['namespace', 'id'], updateEntity);
165
+ const loaded = await compoundRepo.loadByQuery({ namespace: 'ns1', id: '00000000-0000-0000-0000-000000000010' });
166
+ expect(loaded.data).toBe('updated data');
166
167
  });
167
168
  test('should insert and load child entity with compound PK', async () => {
168
- await runInInjectionContext(injector, async () => {
169
- const repository = injectRepository(ChildEntityModel);
170
- const entity = new ChildEntityModel();
171
- entity.id = '00000000-0000-0000-0000-000000000020';
172
- entity.namespace = 'ns2';
173
- entity.type = 'child';
174
- entity.childData = 'child specific data';
175
- await repository.insert(entity);
176
- const loaded = await repository.loadByQuery({ namespace: 'ns2', id: '00000000-0000-0000-0000-000000000020' });
177
- expect(loaded.childData).toBe('child specific data');
178
- expect(loaded.namespace).toBe('ns2');
179
- expect(loaded.id).toBe('00000000-0000-0000-0000-000000000020');
180
- });
169
+ const entity = new ChildEntityModel();
170
+ entity.id = '00000000-0000-0000-0000-000000000020';
171
+ entity.namespace = 'ns2';
172
+ entity.type = 'child';
173
+ entity.childData = 'child specific data';
174
+ await childRepo.insert(entity);
175
+ const loaded = await childRepo.loadByQuery({ namespace: 'ns2', id: '00000000-0000-0000-0000-000000000020' });
176
+ expect(loaded.childData).toBe('child specific data');
177
+ expect(loaded.namespace).toBe('ns2');
178
+ expect(loaded.id).toBe('00000000-0000-0000-0000-000000000020');
181
179
  });
182
180
  test('should insert and load grandchild entity with compound PK', async () => {
183
- await runInInjectionContext(injector, async () => {
184
- const repository = injectRepository(GrandchildEntity);
185
- const entity = new GrandchildEntity();
186
- entity.id = '00000000-0000-0000-0000-000000000050';
187
- entity.namespace = 'ns5';
188
- entity.type = 'grandchild';
189
- entity.childData = 'child part';
190
- entity.grandchildData = 'grandchild part';
191
- await repository.insert(entity);
192
- const loaded = await repository.loadByQuery({ namespace: 'ns5', id: '00000000-0000-0000-0000-000000000050' });
193
- expect(loaded.grandchildData).toBe('grandchild part');
194
- expect(loaded.childData).toBe('child part');
195
- expect(loaded.namespace).toBe('ns5');
196
- expect(loaded.id).toBe('00000000-0000-0000-0000-000000000050');
197
- });
181
+ const entity = new GrandchildEntity();
182
+ entity.id = '00000000-0000-0000-0000-000000000050';
183
+ entity.namespace = 'ns5';
184
+ entity.type = 'grandchild';
185
+ entity.childData = 'child part';
186
+ entity.grandchildData = 'grandchild part';
187
+ await grandchildRepo.insert(entity);
188
+ const loaded = await grandchildRepo.loadByQuery({ namespace: 'ns5', id: '00000000-0000-0000-0000-000000000050' });
189
+ expect(loaded.grandchildData).toBe('grandchild part');
190
+ expect(loaded.childData).toBe('child part');
191
+ expect(loaded.namespace).toBe('ns5');
192
+ expect(loaded.id).toBe('00000000-0000-0000-0000-000000000050');
198
193
  });
199
194
  test('should updateManyByQuery with compound PK', async () => {
200
- await runInInjectionContext(injector, async () => {
201
- const repository = injectRepository(CompoundEntity);
202
- const entity1 = new CompoundEntity();
203
- entity1.id = '00000000-0000-0000-0000-000000000030';
204
- entity1.namespace = 'ns3';
205
- entity1.data = 'data 1';
206
- const entity2 = new CompoundEntity();
207
- entity2.id = '00000000-0000-0000-0000-000000000040';
208
- entity2.namespace = 'ns3';
209
- entity2.data = 'data 2';
210
- await repository.insertMany([entity1, entity2]);
211
- await repository.updateManyByQuery({ namespace: 'ns3' }, { data: 'batch updated' });
212
- const entities = await repository.loadManyByQuery({ namespace: 'ns3' });
213
- expect(entities).toHaveLength(2);
214
- expect(entities.every(e => e.data === 'batch updated')).toBe(true);
215
- });
195
+ const entity1 = new CompoundEntity();
196
+ entity1.id = '00000000-0000-0000-0000-000000000030';
197
+ entity1.namespace = 'ns3';
198
+ entity1.data = 'data 1';
199
+ const entity2 = new CompoundEntity();
200
+ entity2.id = '00000000-0000-0000-0000-000000000040';
201
+ entity2.namespace = 'ns3';
202
+ entity2.data = 'data 2';
203
+ await compoundRepo.insertMany([entity1, entity2]);
204
+ await compoundRepo.updateManyByQuery({ namespace: 'ns3' }, { data: 'batch updated' });
205
+ const entities = await compoundRepo.loadManyByQuery({ namespace: 'ns3' });
206
+ expect(entities).toHaveLength(2);
207
+ expect(entities.every(e => e.data === 'batch updated')).toBe(true);
216
208
  });
217
209
  test('should respect custom primary key name', () => {
218
210
  let CustomPkEntity = class CustomPkEntity extends Entity {
@@ -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 { GeneratedTsVector, 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, injectRepository } from '../server/index.js';
18
17
  describe('ORM Repository Comprehensive Options & Edge Cases', () => {
19
18
  let injector;
20
19
  let db;
20
+ let repository;
21
21
  const schema = 'test_orm_comprehensive';
22
22
  let ComprehensiveEntity = class ComprehensiveEntity extends Entity {
23
23
  name;
@@ -46,14 +46,10 @@ describe('ORM Repository Comprehensive Options & Edge Cases', () => {
46
46
  Table('comprehensive_entities', { schema })
47
47
  ], ComprehensiveEntity);
48
48
  beforeAll(async () => {
49
- injector = new Injector('Test');
50
- configureOrm({
51
- repositoryConfig: { schema },
52
- connection: {
53
- host: '127.0.0.1', port: 5432, user: 'tstdl', password: 'wf7rq6glrk5jykne', database: 'tstdl',
54
- },
55
- });
56
- db = injector.resolve(Database);
49
+ ({ injector, database: db } = await setupIntegrationTest({
50
+ orm: { schema },
51
+ }));
52
+ repository = injector.resolve(getRepository(ComprehensiveEntity));
57
53
  await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
58
54
  await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('comprehensive_entities')} CASCADE`);
59
55
  await db.execute(sql `
@@ -74,114 +70,93 @@ describe('ORM Repository Comprehensive Options & Edge Cases', () => {
74
70
  });
75
71
  test('should support DISTINCT ON loading', async () => {
76
72
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('comprehensive_entities')} CASCADE`);
77
- await runInInjectionContext(injector, async () => {
78
- const repository = injectRepository(ComprehensiveEntity);
79
- await repository.insertMany([
80
- Object.assign(new ComprehensiveEntity(), { name: 'A1', category: 'Cat1', value: 10 }),
81
- Object.assign(new ComprehensiveEntity(), { name: 'A2', category: 'Cat1', value: 20 }),
82
- Object.assign(new ComprehensiveEntity(), { name: 'B1', category: 'Cat2', value: 30 }),
83
- ]);
84
- // Load distinct by category, order by category and value desc to get the one with highest value per category
85
- const results = await repository.loadManyByQuery({}, {
86
- distinct: ['category'],
87
- order: [['category', 'asc'], ['value', 'desc']],
88
- });
89
- expect(results).toHaveLength(2);
90
- expect(results.find((r) => r.category === 'Cat1').name).toBe('A2'); // Value 20 > 10
91
- expect(results.find((r) => r.category === 'Cat2').name).toBe('B1');
73
+ await repository.insertMany([
74
+ Object.assign(new ComprehensiveEntity(), { name: 'A1', category: 'Cat1', value: 10 }),
75
+ Object.assign(new ComprehensiveEntity(), { name: 'A2', category: 'Cat1', value: 20 }),
76
+ Object.assign(new ComprehensiveEntity(), { name: 'B1', category: 'Cat2', value: 30 }),
77
+ ]);
78
+ // Load distinct by category, order by category and value desc to get the one with highest value per category
79
+ const results = await repository.loadManyByQuery({}, {
80
+ distinct: ['category'],
81
+ order: [['category', 'asc'], ['value', 'desc']],
92
82
  });
83
+ expect(results).toHaveLength(2);
84
+ expect(results.find((r) => r.category === 'Cat1').name).toBe('A2'); // Value 20 > 10
85
+ expect(results.find((r) => r.category === 'Cat2').name).toBe('B1');
93
86
  });
94
87
  test('should support search with filter and distinct', async () => {
95
88
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('comprehensive_entities')} CASCADE`);
96
- await runInInjectionContext(injector, async () => {
97
- const repository = injectRepository(ComprehensiveEntity);
98
- await repository.insertMany([
99
- Object.assign(new ComprehensiveEntity(), { name: 'Apple One', category: 'Fruit', value: 5 }),
100
- Object.assign(new ComprehensiveEntity(), { name: 'Apple Two', category: 'Fruit', value: 15 }),
101
- Object.assign(new ComprehensiveEntity(), { name: 'Apple Three', category: 'Tech', value: 25 }),
102
- ]);
103
- // Search "Apple", filter value > 10, distinct category
104
- const results = await repository.search({
105
- query: { $tsvector: { fields: ['searchVector'], query: 'Apple' } },
106
- filter: { value: { $gt: 10 } },
107
- distinct: ['category'],
108
- order: [['category', 'asc'], ['value', 'desc']], // Distinct on requires matching order prefix
109
- });
110
- expect(results).toHaveLength(2);
111
- const categories = results.map((r) => r.entity.category).sort();
112
- expect(categories).toEqual(['Fruit', 'Tech']);
113
- const fruit = results.find((r) => r.entity.category === 'Fruit');
114
- expect(fruit.entity.name).toBe('Apple Two'); // Value 15 > 10, Apple One filtered out or lower
89
+ await repository.insertMany([
90
+ Object.assign(new ComprehensiveEntity(), { name: 'Apple One', category: 'Fruit', value: 5 }),
91
+ Object.assign(new ComprehensiveEntity(), { name: 'Apple Two', category: 'Fruit', value: 15 }),
92
+ Object.assign(new ComprehensiveEntity(), { name: 'Apple Three', category: 'Tech', value: 25 }),
93
+ ]);
94
+ // Search "Apple", filter value > 10, distinct category
95
+ const results = await repository.search({
96
+ query: { $tsvector: { fields: ['searchVector'], query: 'Apple' } },
97
+ filter: { value: { $gt: 10 } },
98
+ distinct: ['category'],
99
+ order: [['category', 'asc'], ['value', 'desc']], // Distinct on requires matching order prefix
115
100
  });
101
+ expect(results).toHaveLength(2);
102
+ const categories = results.map((r) => r.entity.category).sort();
103
+ expect(categories).toEqual(['Fruit', 'Tech']);
104
+ const fruit = results.find((r) => r.entity.category === 'Fruit');
105
+ expect(fruit.entity.name).toBe('Apple Two'); // Value 15 > 10, Apple One filtered out or lower
116
106
  });
117
107
  test('should handle soft deletes correctly in updates', async () => {
118
108
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('comprehensive_entities')} CASCADE`);
119
- await runInInjectionContext(injector, async () => {
120
- const repository = injectRepository(ComprehensiveEntity);
121
- const entity = await repository.insert(Object.assign(new ComprehensiveEntity(), { name: 'To Delete', category: 'Test', value: 1 }));
122
- await repository.delete(entity.id);
123
- // Try update without withDeleted (should fail/return undefined)
124
- const tryUpdateResult = await repository.tryUpdate(entity.id, { name: 'Resurrected?' });
125
- expect(tryUpdateResult).toBeUndefined();
126
- // Ensure DB is unchanged
127
- const row = await repository.tryLoad(entity.id, { withDeleted: true });
128
- expect(row.name).toBe('To Delete');
129
- // Update WITH withDeleted (should work)
130
- const updateResult = await repository.update(entity.id, { name: 'Zombie' }, { withDeleted: true });
131
- expect(updateResult.name).toBe('Zombie');
132
- expect(updateResult.metadata.deleteTimestamp).not.toBeNull(); // Still deleted
133
- });
109
+ const entity = await repository.insert(Object.assign(new ComprehensiveEntity(), { name: 'To Delete', category: 'Test', value: 1 }));
110
+ await repository.delete(entity.id);
111
+ // Try update without withDeleted (should fail/return undefined)
112
+ const tryUpdateResult = await repository.tryUpdate(entity.id, { name: 'Resurrected?' });
113
+ expect(tryUpdateResult).toBeUndefined();
114
+ // Ensure DB is unchanged
115
+ const row = await repository.tryLoad(entity.id, { withDeleted: true });
116
+ expect(row.name).toBe('To Delete');
117
+ // Update WITH withDeleted (should work)
118
+ const updateResult = await repository.update(entity.id, { name: 'Zombie' }, { withDeleted: true });
119
+ expect(updateResult.name).toBe('Zombie');
120
+ expect(updateResult.metadata.deleteTimestamp).not.toBeNull(); // Still deleted
134
121
  });
135
122
  test('should handle count with and without deleted', async () => {
136
123
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('comprehensive_entities')} CASCADE`);
137
- await runInInjectionContext(injector, async () => {
138
- const repository = injectRepository(ComprehensiveEntity);
139
- await repository.insert(Object.assign(new ComprehensiveEntity(), { name: 'Alive', category: 'A', value: 1 }));
140
- const dead = await repository.insert(Object.assign(new ComprehensiveEntity(), { name: 'Dead', category: 'A', value: 2 }));
141
- await repository.delete(dead.id);
142
- expect(await repository.count()).toBe(1);
143
- expect(await repository.count({ withDeleted: true })).toBe(2);
144
- expect(await repository.countByQuery({ category: 'A' })).toBe(1);
145
- expect(await repository.countByQuery({ category: 'A' }, { withDeleted: true })).toBe(2);
146
- });
124
+ await repository.insert(Object.assign(new ComprehensiveEntity(), { name: 'Alive', category: 'A', value: 1 }));
125
+ const dead = await repository.insert(Object.assign(new ComprehensiveEntity(), { name: 'Dead', category: 'A', value: 2 }));
126
+ await repository.delete(dead.id);
127
+ expect(await repository.count()).toBe(1);
128
+ expect(await repository.count({ withDeleted: true })).toBe(2);
129
+ expect(await repository.countByQuery({ category: 'A' })).toBe(1);
130
+ expect(await repository.countByQuery({ category: 'A' }, { withDeleted: true })).toBe(2);
147
131
  });
148
132
  test('should fail gracefully (return undefined) for missing entities in tryLoad operations', async () => {
149
- await runInInjectionContext(injector, async () => {
150
- const repository = injectRepository(ComprehensiveEntity);
151
- expect(await repository.tryLoad('00000000-0000-0000-0000-000000000000')).toBeUndefined();
152
- expect(await repository.tryLoadByQuery({ name: 'NonExistent' })).toBeUndefined();
153
- });
133
+ expect(await repository.tryLoad('00000000-0000-0000-0000-000000000000')).toBeUndefined();
134
+ expect(await repository.tryLoadByQuery({ name: 'NonExistent' })).toBeUndefined();
154
135
  });
155
136
  test('should return empty array for empty search results', async () => {
156
- await runInInjectionContext(injector, async () => {
157
- const repository = injectRepository(ComprehensiveEntity);
158
- const results = await repository.search({ query: { $tsvector: { fields: ['name'], query: 'NonExistentTerm' } } });
159
- expect(results).toEqual([]);
160
- });
137
+ const results = await repository.search({ query: { $tsvector: { fields: ['name'], query: 'NonExistentTerm' } } });
138
+ expect(results).toEqual([]);
161
139
  });
162
140
  test('should support complex ordering with multiple fields', async () => {
163
141
  await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('comprehensive_entities')} CASCADE`);
164
- await runInInjectionContext(injector, async () => {
165
- const repository = injectRepository(ComprehensiveEntity);
166
- await repository.insertMany([
167
- Object.assign(new ComprehensiveEntity(), { name: 'A', category: 'X', value: 1 }),
168
- Object.assign(new ComprehensiveEntity(), { name: 'B', category: 'X', value: 2 }),
169
- Object.assign(new ComprehensiveEntity(), { name: 'A', category: 'Y', value: 3 }),
170
- ]);
171
- // Order by Name ASC, then Value DESC
172
- const results = await repository.loadAll({
173
- order: [['name', 'asc'], ['value', 'desc']],
174
- });
175
- expect(results).toHaveLength(3);
176
- expect(results[0].name).toBe('A');
177
- expect(results[0].value).toBe(3); // Y (3) comes before X (1) because both are A? No, wait.
178
- // Name A (Y, 3) and Name A (X, 1).
179
- // Order: Name ASC. A, A, B.
180
- // Tie-break Name: Value DESC. 3, 1.
181
- // So A(3) then A(1).
182
- expect(results[1].name).toBe('A');
183
- expect(results[1].value).toBe(1);
184
- expect(results[2].name).toBe('B');
142
+ await repository.insertMany([
143
+ Object.assign(new ComprehensiveEntity(), { name: 'A', category: 'X', value: 1 }),
144
+ Object.assign(new ComprehensiveEntity(), { name: 'B', category: 'X', value: 2 }),
145
+ Object.assign(new ComprehensiveEntity(), { name: 'A', category: 'Y', value: 3 }),
146
+ ]);
147
+ // Order by Name ASC, then Value DESC
148
+ const results = await repository.loadAll({
149
+ order: [['name', 'asc'], ['value', 'desc']],
185
150
  });
151
+ expect(results).toHaveLength(3);
152
+ expect(results[0].name).toBe('A');
153
+ expect(results[0].value).toBe(3); // Y (3) comes before X (1) because both are A? No, wait.
154
+ // Name A (Y, 3) and Name A (X, 1).
155
+ // Order: Name ASC. A, A, B.
156
+ // Tie-break Name: Value DESC. 3, 1.
157
+ // So A(3) then A(1).
158
+ expect(results[1].name).toBe('A');
159
+ expect(results[1].value).toBe(1);
160
+ expect(results[2].name).toBe('B');
186
161
  });
187
162
  });
@@ -1 +1,2 @@
1
+ /** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
1
2
  export {};