@tstdl/base 0.93.144 → 0.93.146
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/authentication/authentication.api.d.ts +9 -0
- package/authentication/authentication.api.js +3 -0
- package/authentication/client/authentication.service.js +5 -5
- package/authentication/client/http-client.middleware.js +6 -2
- package/authentication/tests/authentication.client-middleware.test.js +35 -0
- package/authentication/tests/authentication.client-service-refresh.test.js +7 -0
- package/authentication/tests/authentication.client-service.test.js +15 -19
- package/authentication/tests/authentication.service.test.js +92 -119
- package/notification/tests/notification-client.test.js +39 -50
- package/notification/tests/notification-flow.test.js +204 -238
- package/notification/tests/notification-sse.service.test.js +20 -27
- package/notification/tests/notification-type.service.test.js +17 -20
- package/orm/tests/query-complex.test.js +80 -111
- package/orm/tests/repository-advanced.test.js +100 -143
- package/orm/tests/repository-attributes.test.js +30 -39
- package/orm/tests/repository-compound-primary-key.test.js +67 -75
- package/orm/tests/repository-comprehensive.test.js +76 -101
- package/orm/tests/repository-coverage.test.d.ts +1 -0
- package/orm/tests/repository-coverage.test.js +88 -149
- package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
- package/orm/tests/repository-cti-extensive.test.js +118 -147
- package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
- package/orm/tests/repository-cti-mapping.test.js +29 -42
- package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
- package/orm/tests/repository-cti-soft-delete.test.js +25 -37
- package/orm/tests/repository-cti-transactions.test.js +19 -33
- package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
- package/orm/tests/repository-cti-upsert-many.test.js +38 -50
- package/orm/tests/repository-cti.test.d.ts +1 -0
- package/orm/tests/repository-cti.test.js +195 -247
- package/orm/tests/repository-expiration.test.d.ts +1 -0
- package/orm/tests/repository-expiration.test.js +46 -59
- package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
- package/orm/tests/repository-extra-coverage.test.js +195 -337
- package/orm/tests/repository-mapping.test.d.ts +1 -0
- package/orm/tests/repository-mapping.test.js +20 -20
- package/orm/tests/repository-regression.test.js +124 -163
- package/orm/tests/repository-search.test.js +30 -44
- package/orm/tests/repository-soft-delete.test.js +54 -79
- package/orm/tests/repository-types.test.js +77 -111
- package/package.json +1 -1
- package/task-queue/tests/worker.test.js +5 -5
- package/testing/README.md +38 -16
- package/testing/integration-setup.d.ts +11 -0
- package/testing/integration-setup.js +57 -30
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
|
|
1
2
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -8,12 +9,12 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
8
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
10
|
};
|
|
10
11
|
import { describe, expect, test } from 'vitest';
|
|
12
|
+
import { Injector } from '../../injector/index.js';
|
|
11
13
|
import { StringProperty } from '../../schema/index.js';
|
|
14
|
+
import { ChildEntity, Column, Inheritance, Table } from '../decorators.js';
|
|
12
15
|
import { Entity } from '../entity.js';
|
|
13
|
-
import { Column, Inheritance, ChildEntity, Table } from '../decorators.js';
|
|
14
|
-
import { getRepository } from '../server/repository.js';
|
|
15
16
|
import { configureOrm } from '../server/index.js';
|
|
16
|
-
import {
|
|
17
|
+
import { getRepository } from '../server/repository.js';
|
|
17
18
|
describe('ORM Repository Mapping (CTI)', () => {
|
|
18
19
|
test('should flatten joined result when mapping child entity', async () => {
|
|
19
20
|
let BaseUser = class BaseUser extends Entity {
|
|
@@ -49,23 +50,22 @@ describe('ORM Repository Mapping (CTI)', () => {
|
|
|
49
50
|
const injector = new Injector('Test');
|
|
50
51
|
configureOrm({
|
|
51
52
|
repositoryConfig: { schema: 'test' },
|
|
52
|
-
connection: { database: 'test' } // Mock config, we won't query DB
|
|
53
|
-
|
|
54
|
-
await runInInjectionContext(injector, async () => {
|
|
55
|
-
const repository = new (getRepository(Admin))();
|
|
56
|
-
// Simulate Drizzle joined result structure
|
|
57
|
-
const mockResult = {
|
|
58
|
-
users: { id: '123', type: 'admin', name: 'John Doe', revision: 1 },
|
|
59
|
-
admins: { id: '123', type: 'admin', role: 'super-admin' }
|
|
60
|
-
};
|
|
61
|
-
// Access private method for testing (using any cast)
|
|
62
|
-
const mappedEntity = await repository._mapToEntity(mockResult, {});
|
|
63
|
-
expect(mappedEntity).toBeInstanceOf(Admin);
|
|
64
|
-
expect(mappedEntity.id).toBe('123');
|
|
65
|
-
expect(mappedEntity.type).toBe('admin');
|
|
66
|
-
expect(mappedEntity.name).toBe('John Doe');
|
|
67
|
-
expect(mappedEntity.role).toBe('super-admin');
|
|
68
|
-
expect(mappedEntity.metadata.revision).toBe(1);
|
|
53
|
+
connection: { database: 'test' }, // Mock config, we won't query DB
|
|
54
|
+
injector,
|
|
69
55
|
});
|
|
56
|
+
const repository = injector.resolve(getRepository(Admin));
|
|
57
|
+
// Simulate Drizzle joined result structure
|
|
58
|
+
const mockResult = {
|
|
59
|
+
users: { id: '123', type: 'admin', name: 'John Doe', revision: 1 },
|
|
60
|
+
admins: { id: '123', type: 'admin', role: 'super-admin' }
|
|
61
|
+
};
|
|
62
|
+
// Access private method for testing (using any cast)
|
|
63
|
+
const mappedEntity = await repository._mapToEntity(mockResult, {});
|
|
64
|
+
expect(mappedEntity).toBeInstanceOf(Admin);
|
|
65
|
+
expect(mappedEntity.id).toBe('123');
|
|
66
|
+
expect(mappedEntity.type).toBe('admin');
|
|
67
|
+
expect(mappedEntity.name).toBe('John Doe');
|
|
68
|
+
expect(mappedEntity.role).toBe('super-admin');
|
|
69
|
+
expect(mappedEntity.metadata.revision).toBe(1);
|
|
70
70
|
});
|
|
71
71
|
});
|
|
@@ -7,20 +7,22 @@ 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 { sql } from 'drizzle-orm';
|
|
11
|
+
import { beforeAll, describe, expect, test } from 'vitest';
|
|
10
12
|
import { defineEnum } from '../../enumeration/index.js';
|
|
11
13
|
import { NotFoundError } from '../../errors/not-found.error.js';
|
|
12
|
-
import { Injector, runInInjectionContext } from '../../injector/index.js';
|
|
13
14
|
import { Array, Enumeration, Integer, string, StringProperty } from '../../schema/index.js';
|
|
14
|
-
import {
|
|
15
|
-
import { beforeAll, describe, expect, test } from 'vitest';
|
|
15
|
+
import { setupIntegrationTest } from '../../testing/index.js';
|
|
16
16
|
import { Column, EmbeddedProperty, EncryptedProperty, Reference, Table } from '../decorators.js';
|
|
17
17
|
import { Entity } from '../entity.js';
|
|
18
18
|
import { JsonProperty, NumericDateProperty } from '../schemas/index.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { getRepository } from '../server/index.js';
|
|
20
|
+
import { ENCRYPTION_SECRET } from '../server/tokens.js';
|
|
21
21
|
describe('ORM Repository Regression (Integration)', () => {
|
|
22
22
|
let injector;
|
|
23
23
|
let db;
|
|
24
|
+
let standardRepository;
|
|
25
|
+
let postRepository;
|
|
24
26
|
const schema = 'test_orm_regression';
|
|
25
27
|
const UserRole = defineEnum('UserRole', {
|
|
26
28
|
Admin: 'admin',
|
|
@@ -99,20 +101,13 @@ describe('ORM Repository Regression (Integration)', () => {
|
|
|
99
101
|
Table('posts', { schema })
|
|
100
102
|
], Post);
|
|
101
103
|
beforeAll(async () => {
|
|
102
|
-
injector = new Injector('Test');
|
|
103
104
|
const encryptionSecret = new Uint8Array(32).fill(1);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
password: 'wf7rq6glrk5jykne',
|
|
111
|
-
database: 'tstdl',
|
|
112
|
-
},
|
|
113
|
-
encryptionSecret
|
|
114
|
-
});
|
|
115
|
-
db = injector.resolve(Database);
|
|
105
|
+
({ injector, database: db } = await setupIntegrationTest({
|
|
106
|
+
orm: { schema },
|
|
107
|
+
}));
|
|
108
|
+
injector.register(ENCRYPTION_SECRET, { useValue: encryptionSecret });
|
|
109
|
+
standardRepository = injector.resolve(getRepository(StandardEntity));
|
|
110
|
+
postRepository = injector.resolve(getRepository(Post));
|
|
116
111
|
await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
117
112
|
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('posts')} CASCADE`);
|
|
118
113
|
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('standard_entities')} CASCADE`);
|
|
@@ -151,180 +146,146 @@ describe('ORM Repository Regression (Integration)', () => {
|
|
|
151
146
|
});
|
|
152
147
|
test('should perform basic CRUD on standard entity', async () => {
|
|
153
148
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('standard_entities')} CASCADE`);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const { rows: [deletedRow] } = await db.execute(sql `SELECT delete_timestamp FROM ${sql.identifier(schema)}.${sql.identifier('standard_entities')} WHERE id = ${inserted.id}`);
|
|
188
|
-
expect(deletedRow['delete_timestamp']).not.toBeNull();
|
|
189
|
-
});
|
|
149
|
+
const entity = new StandardEntity();
|
|
150
|
+
entity.name = 'Test';
|
|
151
|
+
entity.address = { street: 'Main St', number: 123 };
|
|
152
|
+
entity.secret = 'my-secret-password';
|
|
153
|
+
entity.role = UserRole.User;
|
|
154
|
+
entity.data = { foo: 'bar' };
|
|
155
|
+
entity.tags = ['a', 'b'];
|
|
156
|
+
entity.birthday = 19716; // 2023-12-25
|
|
157
|
+
const inserted = await standardRepository.insert(entity);
|
|
158
|
+
expect(inserted.id).toBeDefined();
|
|
159
|
+
expect(inserted.name).toBe('Test');
|
|
160
|
+
expect(inserted.address.street).toBe('Main St');
|
|
161
|
+
expect(inserted.secret).toBe('my-secret-password');
|
|
162
|
+
expect(inserted.role).toBe(UserRole.User);
|
|
163
|
+
expect(inserted.data).toEqual({ foo: 'bar' });
|
|
164
|
+
expect(inserted.tags).toEqual(['a', 'b']);
|
|
165
|
+
expect(inserted.birthday).toBe(19716);
|
|
166
|
+
// Verify DB encrypted state
|
|
167
|
+
const { rows: [row] } = await db.execute(sql `SELECT secret, dob FROM ${sql.identifier(schema)}.${sql.identifier('standard_entities')} WHERE id = ${inserted.id}`);
|
|
168
|
+
expect(row['secret']).toBeInstanceOf(Uint8Array);
|
|
169
|
+
expect(row['secret']).not.toEqual(new TextEncoder().encode('my-secret-password'));
|
|
170
|
+
expect(row['dob']).toBe('2023-12-25');
|
|
171
|
+
// Update
|
|
172
|
+
await standardRepository.update(inserted.id, { name: 'Updated' });
|
|
173
|
+
const updated = await standardRepository.load(inserted.id);
|
|
174
|
+
expect(updated.name).toBe('Updated');
|
|
175
|
+
expect(updated.secret).toBe('my-secret-password'); // Still readable
|
|
176
|
+
// Soft Delete
|
|
177
|
+
await standardRepository.delete(inserted.id);
|
|
178
|
+
const deleted = await standardRepository.tryLoad(inserted.id);
|
|
179
|
+
expect(deleted).toBeUndefined();
|
|
180
|
+
const { rows: [deletedRow] } = await db.execute(sql `SELECT delete_timestamp FROM ${sql.identifier(schema)}.${sql.identifier('standard_entities')} WHERE id = ${inserted.id}`);
|
|
181
|
+
expect(deletedRow['delete_timestamp']).not.toBeNull();
|
|
190
182
|
});
|
|
191
183
|
test('should support Reference decorator', async () => {
|
|
192
184
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('standard_entities')} CASCADE`);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
expect(insertedPost.authorId).toBe(insertedAuthor.id);
|
|
201
|
-
const loadedPost = await postRepository.load(insertedPost.id);
|
|
202
|
-
expect(loadedPost.authorId).toBe(insertedAuthor.id);
|
|
203
|
-
});
|
|
185
|
+
const author = Object.assign(new StandardEntity(), { name: 'Author', address: { street: 'A', number: 1 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 });
|
|
186
|
+
const insertedAuthor = await standardRepository.insert(author);
|
|
187
|
+
const post = Object.assign(new Post(), { title: 'Hello World', authorId: insertedAuthor.id });
|
|
188
|
+
const insertedPost = await postRepository.insert(post);
|
|
189
|
+
expect(insertedPost.authorId).toBe(insertedAuthor.id);
|
|
190
|
+
const loadedPost = await postRepository.load(insertedPost.id);
|
|
191
|
+
expect(loadedPost.authorId).toBe(insertedAuthor.id);
|
|
204
192
|
});
|
|
205
193
|
test('should perform insertMany and loadMany', async () => {
|
|
206
194
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('standard_entities')} CASCADE`);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
expect(loaded).toHaveLength(2);
|
|
217
|
-
expect(loaded.map((e) => e.name).sort()).toEqual(['E1', 'E2']);
|
|
218
|
-
});
|
|
195
|
+
const entities = [
|
|
196
|
+
Object.assign(new StandardEntity(), { name: 'E1', address: { street: 'S1', number: 1 }, secret: 's1', role: UserRole.User, data: {}, tags: [], birthday: 0 }),
|
|
197
|
+
Object.assign(new StandardEntity(), { name: 'E2', address: { street: 'S2', number: 2 }, secret: 's2', role: UserRole.Admin, data: {}, tags: [], birthday: 0 }),
|
|
198
|
+
];
|
|
199
|
+
const inserted = await standardRepository.insertMany(entities);
|
|
200
|
+
expect(inserted).toHaveLength(2);
|
|
201
|
+
const loaded = await standardRepository.loadMany(inserted.map((e) => e.id));
|
|
202
|
+
expect(loaded).toHaveLength(2);
|
|
203
|
+
expect(loaded.map((e) => e.name).sort()).toEqual(['E1', 'E2']);
|
|
219
204
|
});
|
|
220
205
|
test('should support upsert', async () => {
|
|
221
206
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('standard_entities')} CASCADE`);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const loaded = await repository.load(inserted.id);
|
|
233
|
-
expect(loaded.name).toBe('Upserted');
|
|
234
|
-
});
|
|
207
|
+
const entity = Object.assign(new StandardEntity(), { name: 'Original', address: { street: 'S', number: 1 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 });
|
|
208
|
+
const inserted = await standardRepository.insert(entity);
|
|
209
|
+
// Upsert: update existing
|
|
210
|
+
const updateEntity = Object.assign(new StandardEntity(), { id: inserted.id, name: 'Upserted', address: { street: 'S', number: 1 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 });
|
|
211
|
+
const upserted = await standardRepository.upsert('id', updateEntity);
|
|
212
|
+
expect(upserted.id).toBe(inserted.id);
|
|
213
|
+
expect(upserted.name).toBe('Upserted');
|
|
214
|
+
// Verify DB
|
|
215
|
+
const loaded = await standardRepository.load(inserted.id);
|
|
216
|
+
expect(loaded.name).toBe('Upserted');
|
|
235
217
|
});
|
|
236
218
|
test('should support complex queries with $or and $and', async () => {
|
|
237
219
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('standard_entities')} CASCADE`);
|
|
238
|
-
await
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
$
|
|
247
|
-
|
|
248
|
-
{ $and: [{ role: UserRole.Admin }, { 'address.number': { $gt: 25 } }] }
|
|
249
|
-
]
|
|
250
|
-
});
|
|
251
|
-
expect(results).toHaveLength(2);
|
|
252
|
-
expect(results.map((r) => r.name).sort()).toEqual(['B', 'C']);
|
|
220
|
+
await standardRepository.insertMany([
|
|
221
|
+
Object.assign(new StandardEntity(), { name: 'A', address: { street: 'S1', number: 10 }, secret: 's', role: UserRole.Admin, data: {}, tags: [], birthday: 0 }),
|
|
222
|
+
Object.assign(new StandardEntity(), { name: 'B', address: { street: 'S2', number: 20 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }),
|
|
223
|
+
Object.assign(new StandardEntity(), { name: 'C', address: { street: 'S3', number: 30 }, secret: 's', role: UserRole.Admin, data: {}, tags: [], birthday: 0 }),
|
|
224
|
+
]);
|
|
225
|
+
const results = await standardRepository.loadManyByQuery({
|
|
226
|
+
$or: [
|
|
227
|
+
{ name: 'B' },
|
|
228
|
+
{ $and: [{ role: UserRole.Admin }, { 'address.number': { $gt: 25 } }] }
|
|
229
|
+
]
|
|
253
230
|
});
|
|
231
|
+
expect(results).toHaveLength(2);
|
|
232
|
+
expect(results.map((r) => r.name).sort()).toEqual(['B', 'C']);
|
|
254
233
|
});
|
|
255
234
|
test('should support hasAll', async () => {
|
|
256
235
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('standard_entities')} CASCADE`);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
expect(await repository.hasAll(inserted.map((e) => e.id))).toBe(true);
|
|
264
|
-
expect(await repository.hasAll([...inserted.map((e) => e.id), '00000000-0000-0000-0000-000000000000'])).toBe(false);
|
|
265
|
-
});
|
|
236
|
+
const inserted = await standardRepository.insertMany([
|
|
237
|
+
Object.assign(new StandardEntity(), { name: 'E1', address: { street: 'S1', number: 1 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }),
|
|
238
|
+
Object.assign(new StandardEntity(), { name: 'E2', address: { street: 'S2', number: 2 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }),
|
|
239
|
+
]);
|
|
240
|
+
expect(await standardRepository.hasAll(inserted.map((e) => e.id))).toBe(true);
|
|
241
|
+
expect(await standardRepository.hasAll([...inserted.map((e) => e.id), '00000000-0000-0000-0000-000000000000'])).toBe(false);
|
|
266
242
|
});
|
|
267
243
|
test('should throw NotFoundError on missing load', async () => {
|
|
268
|
-
await
|
|
269
|
-
const repository = injectRepository(StandardEntity);
|
|
270
|
-
await expect(repository.load('00000000-0000-0000-0000-000000000000')).rejects.toThrow(NotFoundError);
|
|
271
|
-
});
|
|
244
|
+
await expect(standardRepository.load('00000000-0000-0000-0000-000000000000')).rejects.toThrow(NotFoundError);
|
|
272
245
|
});
|
|
273
246
|
test('should support upsertMany', async () => {
|
|
274
247
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('standard_entities')} CASCADE`);
|
|
275
|
-
await
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
expect(all.find((e) => e.id === e1.id).name).toBe('E1-Updated');
|
|
286
|
-
expect(all.find((e) => e.name === 'E2')).toBeDefined();
|
|
287
|
-
});
|
|
248
|
+
const e1 = await standardRepository.insert(Object.assign(new StandardEntity(), { name: 'E1', address: { street: 'S1', number: 1 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }));
|
|
249
|
+
const upsertData = [
|
|
250
|
+
Object.assign(new StandardEntity(), { id: e1.id, name: 'E1-Updated', address: { street: 'S1', number: 1 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }),
|
|
251
|
+
Object.assign(new StandardEntity(), { name: 'E2', address: { street: 'S2', number: 2 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }),
|
|
252
|
+
];
|
|
253
|
+
await standardRepository.upsertMany('id', upsertData);
|
|
254
|
+
const all = await standardRepository.loadAll();
|
|
255
|
+
expect(all).toHaveLength(2);
|
|
256
|
+
expect(all.find((e) => e.id === e1.id).name).toBe('E1-Updated');
|
|
257
|
+
expect(all.find((e) => e.name === 'E2')).toBeDefined();
|
|
288
258
|
});
|
|
289
259
|
test('should support deleteManyByQuery', async () => {
|
|
290
260
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('standard_entities')} CASCADE`);
|
|
291
|
-
await
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
expect(remaining).toHaveLength(1);
|
|
301
|
-
expect(remaining[0].name).toBe('B');
|
|
302
|
-
});
|
|
261
|
+
await standardRepository.insertMany([
|
|
262
|
+
Object.assign(new StandardEntity(), { name: 'A', address: { street: 'S', number: 1 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }),
|
|
263
|
+
Object.assign(new StandardEntity(), { name: 'A', address: { street: 'S', number: 2 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }),
|
|
264
|
+
Object.assign(new StandardEntity(), { name: 'B', address: { street: 'S', number: 3 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }),
|
|
265
|
+
]);
|
|
266
|
+
await standardRepository.deleteManyByQuery({ name: 'A' });
|
|
267
|
+
const remaining = await standardRepository.loadAll();
|
|
268
|
+
expect(remaining).toHaveLength(1);
|
|
269
|
+
expect(remaining[0].name).toBe('B');
|
|
303
270
|
});
|
|
304
271
|
test('should support tryInsert', async () => {
|
|
305
272
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('standard_entities')} CASCADE`);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const conflict = await repository.tryInsert(Object.assign(new StandardEntity(), { id: inserted.id, name: 'Conflict', address: { street: 'S', number: 1 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }));
|
|
313
|
-
expect(conflict).toBeUndefined();
|
|
314
|
-
});
|
|
273
|
+
const entity = Object.assign(new StandardEntity(), { name: 'E1', address: { street: 'S', number: 1 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 });
|
|
274
|
+
const inserted = await standardRepository.tryInsert(entity);
|
|
275
|
+
expect(inserted).toBeDefined();
|
|
276
|
+
// Conflict
|
|
277
|
+
const conflict = await standardRepository.tryInsert(Object.assign(new StandardEntity(), { id: inserted.id, name: 'Conflict', address: { street: 'S', number: 1 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }));
|
|
278
|
+
expect(conflict).toBeUndefined();
|
|
315
279
|
});
|
|
316
280
|
test('should support insertManyIfNotExists', async () => {
|
|
317
281
|
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('standard_entities')} CASCADE`);
|
|
318
|
-
await
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
expect(results).toHaveLength(1);
|
|
327
|
-
expect(results[0].name).toBe('E2');
|
|
328
|
-
});
|
|
282
|
+
const e1 = await standardRepository.insert(Object.assign(new StandardEntity(), { name: 'E1', address: { street: 'S1', number: 1 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }));
|
|
283
|
+
const data = [
|
|
284
|
+
Object.assign(new StandardEntity(), { id: e1.id, name: 'E1-Conflict', address: { street: 'S1', number: 1 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }),
|
|
285
|
+
Object.assign(new StandardEntity(), { name: 'E2', address: { street: 'S2', number: 2 }, secret: 's', role: UserRole.User, data: {}, tags: [], birthday: 0 }),
|
|
286
|
+
];
|
|
287
|
+
const results = await standardRepository.insertManyIfNotExists('id', data);
|
|
288
|
+
expect(results).toHaveLength(1);
|
|
289
|
+
expect(results[0].name).toBe('E2');
|
|
329
290
|
});
|
|
330
291
|
});
|
|
@@ -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, beforeEach, describe, expect, test } from 'vitest';
|
|
12
|
-
import { Injector, runInInjectionContext } from '../../injector/index.js';
|
|
13
12
|
import { StringProperty } from '../../schema/index.js';
|
|
14
13
|
import { dropTables, setupIntegrationTest, truncateTables } from '../../testing/index.js';
|
|
14
|
+
import { isArray } from '../../utils/type-guards.js';
|
|
15
15
|
import { Table } from '../decorators.js';
|
|
16
16
|
import { Entity } from '../entity.js';
|
|
17
|
-
import {
|
|
18
|
-
import { injectRepository } from '../server/repository.js';
|
|
17
|
+
import { getRepository } from '../server/index.js';
|
|
19
18
|
describe('ORM Repository Search', () => {
|
|
20
19
|
let injector;
|
|
21
20
|
let database;
|
|
21
|
+
let repository;
|
|
22
22
|
const schema = 'test_orm_search';
|
|
23
23
|
let SearchEntity = class SearchEntity extends Entity {
|
|
24
24
|
title;
|
|
@@ -37,6 +37,7 @@ describe('ORM Repository Search', () => {
|
|
|
37
37
|
], SearchEntity);
|
|
38
38
|
beforeAll(async () => {
|
|
39
39
|
({ injector, database } = await setupIntegrationTest({ orm: { schema } }));
|
|
40
|
+
repository = injector.resolve(getRepository(SearchEntity));
|
|
40
41
|
await dropTables(database, schema, ['search_entities']);
|
|
41
42
|
await database.execute(sql `
|
|
42
43
|
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('search_entities')} (
|
|
@@ -55,40 +56,31 @@ describe('ORM Repository Search', () => {
|
|
|
55
56
|
await truncateTables(database, schema, ['search_entities']);
|
|
56
57
|
});
|
|
57
58
|
test('should support search with score transformer', async () => {
|
|
58
|
-
await
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
query: { $tsvector: { fields: ['title'], query: 'hello' } },
|
|
63
|
-
score: (score) => sql `(${score}) * 100`,
|
|
64
|
-
});
|
|
65
|
-
expect(results).toHaveLength(1);
|
|
66
|
-
expect(results[0].score).toBeGreaterThan(1); // normalized score is small, * 100 should be > 1
|
|
59
|
+
await repository.insert(Object.assign(new SearchEntity(), { title: 'Hello World', content: 'Some content here' }));
|
|
60
|
+
const results = await repository.search({
|
|
61
|
+
query: { $tsvector: { fields: ['title'], query: 'hello' } },
|
|
62
|
+
score: (score) => sql `(${score}) * 100`,
|
|
67
63
|
});
|
|
64
|
+
expect(results).toHaveLength(1);
|
|
65
|
+
expect(results[0].score).toBeGreaterThan(1); // normalized score is small, * 100 should be > 1
|
|
68
66
|
});
|
|
69
67
|
test('should support search with highlight (column names)', async () => {
|
|
70
|
-
await
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
query: { $tsvector: { fields: ['title', 'content'], query: 'Highlander' } },
|
|
75
|
-
highlight: { source: 'title' },
|
|
76
|
-
});
|
|
77
|
-
expect(results).toHaveLength(1);
|
|
78
|
-
expect(results[0].highlight).toContain('<b>Highlander</b>');
|
|
68
|
+
await repository.insert(Object.assign(new SearchEntity(), { title: 'Highlander', content: 'There can be only one' }));
|
|
69
|
+
const results = await repository.search({
|
|
70
|
+
query: { $tsvector: { fields: ['title', 'content'], query: 'Highlander' } },
|
|
71
|
+
highlight: { source: 'title' },
|
|
79
72
|
});
|
|
73
|
+
expect(results).toHaveLength(1);
|
|
74
|
+
expect(results[0].highlight).toContain('<b>Highlander</b>');
|
|
80
75
|
});
|
|
81
76
|
test('should support search with highlight (raw SQL)', async () => {
|
|
82
|
-
await
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
query: { $tsvector: { fields: ['title', 'content'], query: 'Master' } },
|
|
87
|
-
highlight: { source: sql `title || ' ' || content` },
|
|
88
|
-
});
|
|
89
|
-
expect(results).toHaveLength(1);
|
|
90
|
-
expect(results[0].highlight).toContain('<b>Master</b>');
|
|
77
|
+
await repository.insert(Object.assign(new SearchEntity(), { title: 'SQL Master', content: 'Database wizard' }));
|
|
78
|
+
const results = await repository.search({
|
|
79
|
+
query: { $tsvector: { fields: ['title', 'content'], query: 'Master' } },
|
|
80
|
+
highlight: { source: sql `title || ' ' || content` },
|
|
91
81
|
});
|
|
82
|
+
expect(results).toHaveLength(1);
|
|
83
|
+
expect(results[0].highlight).toContain('<b>Master</b>');
|
|
92
84
|
});
|
|
93
85
|
test('should support ParadeDB search with highlightPositions', async () => {
|
|
94
86
|
// This test requires pg_search extension to be installed.
|
|
@@ -100,20 +92,14 @@ describe('ORM Repository Search', () => {
|
|
|
100
92
|
console.warn('ParadeDB (pg_search) not available or index creation failed, skipping ParadeDB test.', e);
|
|
101
93
|
return;
|
|
102
94
|
}
|
|
103
|
-
await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
query: { $parade: { fields: ['title'], query: 'Parade' } },
|
|
108
|
-
highlight: { source: 'title', includePositions: true },
|
|
109
|
-
});
|
|
110
|
-
expect(results).toHaveLength(1);
|
|
111
|
-
expect(results[0].highlight).toBeDefined();
|
|
112
|
-
expect(results[0].highlightPositions).toBeDefined();
|
|
113
|
-
expect(isArray(results[0].highlightPositions)).toBe(true);
|
|
95
|
+
await repository.insert(Object.assign(new SearchEntity(), { title: 'Parade', content: 'ParadeDB is fast' }));
|
|
96
|
+
const results = await repository.search({
|
|
97
|
+
query: { $parade: { fields: ['title'], query: 'Parade' } },
|
|
98
|
+
highlight: { source: 'title', includePositions: true },
|
|
114
99
|
});
|
|
100
|
+
expect(results).toHaveLength(1);
|
|
101
|
+
expect(results[0].highlight).toBeDefined();
|
|
102
|
+
expect(results[0].highlightPositions).toBeDefined();
|
|
103
|
+
expect(isArray(results[0].highlightPositions)).toBe(true);
|
|
115
104
|
});
|
|
116
105
|
});
|
|
117
|
-
function isArray(val) {
|
|
118
|
-
return Array.isArray(val);
|
|
119
|
-
}
|