@tstdl/base 0.93.162 → 0.93.164
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/api/server/gateway.d.ts +1 -0
- package/api/server/gateway.js +11 -4
- package/authentication/tests/authentication.api-controller.test.js +24 -0
- package/circuit-breaker/postgres/circuit-breaker.d.ts +0 -3
- package/circuit-breaker/postgres/circuit-breaker.js +9 -12
- package/circuit-breaker/tests/circuit-breaker.test.js +7 -2
- package/object-storage/s3/tests/s3.object-storage.integration.test.js +2 -2
- package/orm/tests/build-jsonb.test.js +5 -5
- package/orm/tests/query-converter-complex.test.js +5 -5
- package/orm/tests/repository-cti-complex.test.js +54 -73
- package/orm/tests/repository-cti-embedded.test.js +9 -19
- package/orm/tests/repository-cti-search.test.js +12 -23
- package/orm/tests/repository-edge-cases.test.js +81 -119
- package/orm/tests/repository-mapping.test.js +3 -9
- package/orm/tests/repository-search-coverage.test.js +52 -74
- package/orm/tests/repository-transactions-nested.test.js +96 -120
- package/orm/tests/transactional.test.js +5 -14
- package/package.json +11 -11
- package/task-queue/tests/optimization-edge-cases.test.js +11 -14
- package/task-queue/tests/queue.test.js +29 -2
- package/testing/README.md +23 -35
- package/testing/integration-setup.d.ts +54 -7
- package/testing/integration-setup.js +147 -21
package/api/server/gateway.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
|
|
1
2
|
import 'urlpattern-polyfill';
|
|
2
3
|
import type { HttpServerRequestContext } from '../../http/server/http-server.js';
|
|
3
4
|
import { HttpServerResponse, type HttpServerRequest } from '../../http/server/index.js';
|
package/api/server/gateway.js
CHANGED
|
@@ -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);
|
|
@@ -14,9 +15,7 @@ import 'urlpattern-polyfill';
|
|
|
14
15
|
import { Auditor } from '../../audit/auditor.js';
|
|
15
16
|
import { ActorType } from '../../audit/types.js';
|
|
16
17
|
import { NIL_UUID } from '../../constants.js';
|
|
17
|
-
import { BadRequestError } from '../../errors/
|
|
18
|
-
import { NotFoundError } from '../../errors/not-found.error.js';
|
|
19
|
-
import { NotImplementedError } from '../../errors/not-implemented.error.js';
|
|
18
|
+
import { BadRequestError, InvalidTokenError, NotFoundError, NotImplementedError } from '../../errors/index.js';
|
|
20
19
|
import { HttpServerResponse } from '../../http/server/index.js';
|
|
21
20
|
import { inject, injectArgument, resolveArgumentType, Singleton } from '../../injector/index.js';
|
|
22
21
|
import { Logger } from '../../logger/index.js';
|
|
@@ -213,7 +212,15 @@ let ApiGateway = ApiGateway_1 = class ApiGateway {
|
|
|
213
212
|
return await requestTokenProvider.getToken(requestContext);
|
|
214
213
|
},
|
|
215
214
|
getAuditor: async () => {
|
|
216
|
-
|
|
215
|
+
let token = null;
|
|
216
|
+
try {
|
|
217
|
+
token = await requestContext.tryGetToken();
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (!(error instanceof InvalidTokenError)) {
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
217
224
|
return auditor.fork(context.api.resource)
|
|
218
225
|
.withCorrelation()
|
|
219
226
|
.with({
|
|
@@ -55,6 +55,30 @@ describe('AuthenticationApiController Integration', () => {
|
|
|
55
55
|
expect(service.subjectId()).toBe(user.id);
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
|
+
test('login should work even if an expired token is present in the Authorization header', async () => {
|
|
59
|
+
await runInInjectionContext(injector, async () => {
|
|
60
|
+
const user = await subjectService.createUser({ tenantId, email: 'expired-token-login@example.com', firstName: 'E', lastName: 'L' });
|
|
61
|
+
await serverService.setCredentials(user, 'Strong-Password-2026!');
|
|
62
|
+
// Create an expired token
|
|
63
|
+
const now = Math.floor(Date.now() / 1000);
|
|
64
|
+
const expiredTokenResult = await serverService.createToken({
|
|
65
|
+
subject: user,
|
|
66
|
+
sessionId: crypto.randomUUID(),
|
|
67
|
+
impersonator: undefined,
|
|
68
|
+
additionalTokenPayload: {},
|
|
69
|
+
refreshTokenExpiration: now - 3600,
|
|
70
|
+
expiration: now - 3600, // Expired 1 hour ago
|
|
71
|
+
issuedAt: now - 7200,
|
|
72
|
+
timestamp: (now - 7200) * 1000,
|
|
73
|
+
});
|
|
74
|
+
// Inject the expired token into the client service
|
|
75
|
+
service.updateRawTokens(expiredTokenResult.token);
|
|
76
|
+
// Now try to login
|
|
77
|
+
await service.login({ tenantId, subject: user.id }, 'Strong-Password-2026!');
|
|
78
|
+
expect(service.isLoggedIn()).toBe(true);
|
|
79
|
+
expect(service.subjectId()).toBe(user.id);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
58
82
|
test('checkSecret should work via API client', async () => {
|
|
59
83
|
const result = await service.checkSecret('abc');
|
|
60
84
|
expect(result.strength).toBeLessThan(2);
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { afterResolve } from '../../injector/index.js';
|
|
2
1
|
import { CircuitBreaker, type CircuitBreakerCheckResult } from '../circuit-breaker.js';
|
|
3
2
|
export declare class PostgresCircuitBreakerService extends CircuitBreaker {
|
|
4
3
|
#private;
|
|
5
|
-
private static checkStatement;
|
|
6
|
-
[afterResolve](): void;
|
|
7
4
|
check(): Promise<CircuitBreakerCheckResult>;
|
|
8
5
|
recordSuccess(): Promise<void>;
|
|
9
6
|
recordFailure(): Promise<void>;
|
|
@@ -4,9 +4,8 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
var PostgresCircuitBreakerService_1;
|
|
8
7
|
import { and, eq, lte, sql, isNotNull as sqlIsNotNull } from 'drizzle-orm';
|
|
9
|
-
import {
|
|
8
|
+
import { injectArgument, provide, Singleton } from '../../injector/index.js';
|
|
10
9
|
import { coalesce, interval, TRANSACTION_TIMESTAMP } from '../../orm/index.js';
|
|
11
10
|
import { DatabaseConfig, injectRepository } from '../../orm/server/index.js';
|
|
12
11
|
import { isString, isUndefined } from '../../utils/type-guards.js';
|
|
@@ -16,18 +15,14 @@ import { PostgresCircuitBreaker } from './model.js';
|
|
|
16
15
|
import { PostgresCircuitBreakerModuleConfig } from './module.js';
|
|
17
16
|
import { circuitBreaker } from './schemas.js';
|
|
18
17
|
let PostgresCircuitBreakerService = class PostgresCircuitBreakerService extends CircuitBreaker {
|
|
19
|
-
static { PostgresCircuitBreakerService_1 = this; }
|
|
20
|
-
static checkStatement;
|
|
21
18
|
#repository = injectRepository(PostgresCircuitBreaker);
|
|
22
19
|
#arg = injectArgument(this);
|
|
23
20
|
#key = isString(this.#arg) ? this.#arg : this.#arg.key;
|
|
24
21
|
#threshold = (isString(this.#arg) ? undefined : this.#arg.threshold) ?? 5;
|
|
25
22
|
#resetTimeout = (isString(this.#arg) ? undefined : this.#arg.resetTimeout) ?? 30 * millisecondsPerSecond;
|
|
26
|
-
|
|
27
|
-
PostgresCircuitBreakerService_1.checkStatement ??= this.getPreparedCheckStatement();
|
|
28
|
-
}
|
|
23
|
+
#checkStatement = this.getPreparedCheckStatement();
|
|
29
24
|
async check() {
|
|
30
|
-
const [result] = await
|
|
25
|
+
const [result] = await this.#checkStatement.execute({ key: this.#key });
|
|
31
26
|
// 1. Breaker doesn't exist or is Closed
|
|
32
27
|
if (isUndefined(result) || (result.state === CircuitBreakerState.Closed)) {
|
|
33
28
|
return { allowed: true, state: CircuitBreakerState.Closed, isProbe: false };
|
|
@@ -36,9 +31,11 @@ let PostgresCircuitBreakerService = class PostgresCircuitBreakerService extends
|
|
|
36
31
|
if (result.isProbe) {
|
|
37
32
|
return { allowed: true, state: CircuitBreakerState.HalfOpen, isProbe: true };
|
|
38
33
|
}
|
|
39
|
-
// 3.
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
// 3. If it's already HalfOpen, allow it but it's not the primary probe (others must handle concurrency)
|
|
35
|
+
if (result.state === CircuitBreakerState.HalfOpen) {
|
|
36
|
+
return { allowed: true, state: CircuitBreakerState.HalfOpen, isProbe: false };
|
|
37
|
+
}
|
|
38
|
+
// 4. Fallback: Catch-all for Open state where timeout hasn't expired. Reject
|
|
42
39
|
return { allowed: false, state: result.state, isProbe: false };
|
|
43
40
|
}
|
|
44
41
|
async recordSuccess() {
|
|
@@ -92,7 +89,7 @@ let PostgresCircuitBreakerService = class PostgresCircuitBreakerService extends
|
|
|
92
89
|
.prepare('circuit_breaker_check');
|
|
93
90
|
}
|
|
94
91
|
};
|
|
95
|
-
PostgresCircuitBreakerService =
|
|
92
|
+
PostgresCircuitBreakerService = __decorate([
|
|
96
93
|
Singleton({
|
|
97
94
|
argumentIdentityProvider: (arg) => isString(arg) ? arg : arg.key,
|
|
98
95
|
providers: [
|
|
@@ -56,11 +56,16 @@ describe('Circuit Breaker (Standalone) Tests', () => {
|
|
|
56
56
|
expect(probe.allowed).toBe(true);
|
|
57
57
|
expect(probe.state).toBe(CircuitBreakerState.HalfOpen);
|
|
58
58
|
expect(probe.isProbe).toBe(true);
|
|
59
|
-
// Subsequent check should be
|
|
59
|
+
// Subsequent check should be allowed but not the probe
|
|
60
60
|
const subsequent = await breaker.check();
|
|
61
|
-
expect(subsequent.allowed).toBe(
|
|
61
|
+
expect(subsequent.allowed).toBe(true);
|
|
62
62
|
expect(subsequent.state).toBe(CircuitBreakerState.HalfOpen);
|
|
63
63
|
expect(subsequent.isProbe).toBe(false);
|
|
64
|
+
// Another check should also be allowed
|
|
65
|
+
const third = await breaker.check();
|
|
66
|
+
expect(third.allowed).toBe(true);
|
|
67
|
+
expect(third.state).toBe(CircuitBreakerState.HalfOpen);
|
|
68
|
+
expect(third.isProbe).toBe(false);
|
|
64
69
|
});
|
|
65
70
|
it('should close if Probe succeeds', async () => {
|
|
66
71
|
const timeoutMs = 100;
|
|
@@ -73,7 +73,7 @@ describe('S3ObjectStorage Integration', () => {
|
|
|
73
73
|
const key = 'signed-download.txt';
|
|
74
74
|
await storage.uploadObject(key, new TextEncoder().encode('signed download'));
|
|
75
75
|
const url = await storage.getDownloadUrl(key, Date.now() + 60000);
|
|
76
|
-
expect(url).toMatch(/http:\/\/(127\.0\.0\.1|localhost):
|
|
76
|
+
expect(url).toMatch(/http:\/\/(127\.0\.0\.1|localhost):19552/);
|
|
77
77
|
const response = await fetch(url);
|
|
78
78
|
expect(response.status).toBe(200);
|
|
79
79
|
expect(await response.text()).toBe('signed download');
|
|
@@ -230,7 +230,7 @@ describe('S3ObjectStorage Integration', () => {
|
|
|
230
230
|
const url = await storage.getDownloadUrl(key, Date.now() + 60000, {
|
|
231
231
|
Expires: new Date(Date.now() + 60000).toUTCString(),
|
|
232
232
|
});
|
|
233
|
-
expect(url).toMatch(/http:\/\/(127\.0\.0\.1|localhost):
|
|
233
|
+
expect(url).toMatch(/http:\/\/(127\.0\.0\.1|localhost):19552/);
|
|
234
234
|
const response = await fetch(url);
|
|
235
235
|
expect(response.status).toBe(200);
|
|
236
236
|
});
|
|
@@ -6,29 +6,29 @@ describe('buildJsonb', () => {
|
|
|
6
6
|
test('should build jsonb from simple object', () => {
|
|
7
7
|
const query = buildJsonb({ a: 1, b: 'foo' });
|
|
8
8
|
const { sql, params } = dialect.sqlToQuery(query);
|
|
9
|
-
expect(sql).toBe('jsonb_build_object($1::text, to_jsonb($2), $3::text, to_jsonb($4))');
|
|
9
|
+
expect(sql).toBe('jsonb_build_object($1::text, to_jsonb($2::numeric), $3::text, to_jsonb($4::text))');
|
|
10
10
|
expect(params).toEqual(['a', 1, 'b', 'foo']);
|
|
11
11
|
});
|
|
12
12
|
test('should build jsonb from object with non-simple keys', () => {
|
|
13
13
|
const query = buildJsonb({ 'Betriebs-Nr.': '18182952' });
|
|
14
14
|
const { sql, params } = dialect.sqlToQuery(query);
|
|
15
15
|
// This is what failed before: it lacked the ::text cast
|
|
16
|
-
expect(sql).toBe('jsonb_build_object($1::text, to_jsonb($2))');
|
|
16
|
+
expect(sql).toBe('jsonb_build_object($1::text, to_jsonb($2::text))');
|
|
17
17
|
expect(params).toEqual(['Betriebs-Nr.', '18182952']);
|
|
18
18
|
});
|
|
19
19
|
test('should build jsonb from nested structures', () => {
|
|
20
20
|
const query = buildJsonb({
|
|
21
21
|
additionalData: { 'Betriebs-Nr.': '18182952' },
|
|
22
|
-
tags: ['a', 'b']
|
|
22
|
+
tags: ['a', 'b'],
|
|
23
23
|
});
|
|
24
24
|
const { sql, params } = dialect.sqlToQuery(query);
|
|
25
|
-
expect(sql).toBe('jsonb_build_object($1::text, jsonb_build_object($2::text, to_jsonb($3)), $4::text, jsonb_build_array(to_jsonb($5), to_jsonb($6)))');
|
|
25
|
+
expect(sql).toBe('jsonb_build_object($1::text, jsonb_build_object($2::text, to_jsonb($3::text)), $4::text, jsonb_build_array(to_jsonb($5::text), to_jsonb($6::text)))');
|
|
26
26
|
expect(params).toEqual(['additionalData', 'Betriebs-Nr.', '18182952', 'tags', 'a', 'b']);
|
|
27
27
|
});
|
|
28
28
|
test('should handle numbers correctly', () => {
|
|
29
29
|
const query = buildJsonb({ score: 0.5 });
|
|
30
30
|
const { sql, params } = dialect.sqlToQuery(query);
|
|
31
|
-
expect(sql).toBe('jsonb_build_object($1::text, to_jsonb($2))');
|
|
31
|
+
expect(sql).toBe('jsonb_build_object($1::text, to_jsonb($2::numeric))');
|
|
32
32
|
expect(params).toEqual(['score', 0.5]);
|
|
33
33
|
});
|
|
34
34
|
test('should handle null and empty structures', () => {
|
|
@@ -68,7 +68,7 @@ describe('ORM Query Converter Complex', () => {
|
|
|
68
68
|
const condition = convertQuery(q, table, colMap);
|
|
69
69
|
const { sql, params } = dialect.sqlToQuery(condition);
|
|
70
70
|
// Tokenizer is supported via JSON object syntax
|
|
71
|
-
expect(sql).toContain('"test"."complex_items"."name" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, to_jsonb($3), $4::text, jsonb_build_object($5::text, to_jsonb($6), $7::text, to_jsonb($8), $9::text, to_jsonb($10))))::pdb.query');
|
|
71
|
+
expect(sql).toContain('"test"."complex_items"."name" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, to_jsonb($3::text), $4::text, jsonb_build_object($5::text, to_jsonb($6::text), $7::text, to_jsonb($8::numeric), $9::text, to_jsonb($10::numeric))))::pdb.query');
|
|
72
72
|
expect(params).toEqual(['match', 'value', 'test', 'tokenizer', 'type', 'ngram', 'min_gram', 3, 'max_gram', 3]);
|
|
73
73
|
});
|
|
74
74
|
test('should handle ParadeDB $parade range', () => {
|
|
@@ -85,7 +85,7 @@ describe('ORM Query Converter Complex', () => {
|
|
|
85
85
|
const condition = convertQuery(q, table, colMap);
|
|
86
86
|
const { sql, params } = dialect.sqlToQuery(condition);
|
|
87
87
|
// This should fall back to convertParadeComparisonQuery with recursive jsonb build
|
|
88
|
-
expect(sql).toContain('"test"."complex_items"."value" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, jsonb_build_object($3::text, to_jsonb($4)), $5::text, jsonb_build_object($6::text, to_jsonb($7))))::pdb.query');
|
|
88
|
+
expect(sql).toContain('"test"."complex_items"."value" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, jsonb_build_object($3::text, to_jsonb($4::numeric)), $5::text, jsonb_build_object($6::text, to_jsonb($7::numeric))))::pdb.query');
|
|
89
89
|
expect(params).toEqual(['range', 'lower_bound', 'included', 10, 'upper_bound', 'excluded', 20]);
|
|
90
90
|
});
|
|
91
91
|
test('should handle ParadeDB $parade phrasePrefix', () => {
|
|
@@ -98,7 +98,7 @@ describe('ORM Query Converter Complex', () => {
|
|
|
98
98
|
};
|
|
99
99
|
const condition = convertQuery(q, table, colMap);
|
|
100
100
|
const { sql, params } = dialect.sqlToQuery(condition);
|
|
101
|
-
expect(sql).toContain('"test"."complex_items"."name" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, jsonb_build_array(to_jsonb($3), to_jsonb($4)), $5::text, to_jsonb($6)))::pdb.query');
|
|
101
|
+
expect(sql).toContain('"test"."complex_items"."name" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, jsonb_build_array(to_jsonb($3::text), to_jsonb($4::text)), $5::text, to_jsonb($6::numeric)))::pdb.query');
|
|
102
102
|
expect(params).toEqual(['phrase_prefix', 'phrases', 'hello', 'wor', 'max_expansions', 10]);
|
|
103
103
|
});
|
|
104
104
|
test('should handle ParadeDB $parade regexPhrase', () => {
|
|
@@ -111,7 +111,7 @@ describe('ORM Query Converter Complex', () => {
|
|
|
111
111
|
};
|
|
112
112
|
const condition = convertQuery(q, table, colMap);
|
|
113
113
|
const { sql, params } = dialect.sqlToQuery(condition);
|
|
114
|
-
expect(sql).toContain('"test"."complex_items"."name" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, jsonb_build_array(to_jsonb($3), to_jsonb($4)), $5::text, to_jsonb($6)))::pdb.query');
|
|
114
|
+
expect(sql).toContain('"test"."complex_items"."name" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, jsonb_build_array(to_jsonb($3::text), to_jsonb($4::text)), $5::text, to_jsonb($6::numeric)))::pdb.query');
|
|
115
115
|
expect(params).toEqual(['regex_phrase', 'regexes', 'he.*', 'wo.*', 'slop', 1]);
|
|
116
116
|
});
|
|
117
117
|
test('should handle ParadeDB top-level moreLikeThis', () => {
|
|
@@ -125,7 +125,7 @@ describe('ORM Query Converter Complex', () => {
|
|
|
125
125
|
};
|
|
126
126
|
const condition = convertQuery(q, table, colMap);
|
|
127
127
|
const { sql, params } = dialect.sqlToQuery(condition);
|
|
128
|
-
expect(sql).toContain('"test"."complex_items"."id" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, to_jsonb($3), $4::text, to_jsonb(ARRAY[$5, $6])))::pdb.query');
|
|
128
|
+
expect(sql).toContain('"test"."complex_items"."id" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, to_jsonb($3::text), $4::text, to_jsonb(ARRAY[$5, $6])))::pdb.query');
|
|
129
129
|
expect(params).toEqual(['more_like_this', 'key_value', '123', 'fields', 'name', 'description']);
|
|
130
130
|
});
|
|
131
131
|
});
|
|
@@ -8,17 +8,15 @@ 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
10
|
import { sql } from 'drizzle-orm';
|
|
11
|
-
import {
|
|
12
|
-
import { Injector, runInInjectionContext } from '../../injector/index.js';
|
|
11
|
+
import { describe, expect } from 'vitest';
|
|
13
12
|
import { StringProperty } from '../../schema/index.js';
|
|
13
|
+
import { getIntegrationTest } from '../../testing/index.js';
|
|
14
14
|
import { ChildEntity, Inheritance, Table } from '../decorators.js';
|
|
15
15
|
import { Entity } from '../entity.js';
|
|
16
|
-
import { configureOrm, Database } from '../server/index.js';
|
|
17
16
|
import { injectRepository } from '../server/repository.js';
|
|
17
|
+
const schema = 'test_orm_cti_complex';
|
|
18
|
+
const test = getIntegrationTest({ orm: { schema } });
|
|
18
19
|
describe('ORM Repository CTI Complex', () => {
|
|
19
|
-
let injector;
|
|
20
|
-
let db;
|
|
21
|
-
const schema = 'test_orm_cti_complex';
|
|
22
20
|
let BaseEntity = class BaseEntity extends Entity {
|
|
23
21
|
type;
|
|
24
22
|
baseName;
|
|
@@ -57,15 +55,7 @@ describe('ORM Repository CTI Complex', () => {
|
|
|
57
55
|
Table('leaf_entities', { schema }),
|
|
58
56
|
ChildEntity('leaf')
|
|
59
57
|
], LeafEntity);
|
|
60
|
-
|
|
61
|
-
injector = new Injector('Test');
|
|
62
|
-
configureOrm({
|
|
63
|
-
repositoryConfig: { schema },
|
|
64
|
-
connection: {
|
|
65
|
-
host: '127.0.0.1', port: 5432, user: 'tstdl', password: 'wf7rq6glrk5jykne', database: 'tstdl',
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
db = injector.resolve(Database);
|
|
58
|
+
test.beforeEach(async ({ database: db }) => {
|
|
69
59
|
await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
70
60
|
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('leaf_entities')} CASCADE`);
|
|
71
61
|
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('middle_entities')} CASCADE`);
|
|
@@ -102,69 +92,60 @@ describe('ORM Repository CTI Complex', () => {
|
|
|
102
92
|
`);
|
|
103
93
|
});
|
|
104
94
|
test('should support deep inheritance (3 levels)', async () => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
leafName: 'Leaf',
|
|
111
|
-
});
|
|
112
|
-
const inserted = await repository.insert(leaf);
|
|
113
|
-
expect(inserted.id).toBeDefined();
|
|
114
|
-
expect(inserted.baseName).toBe('Base');
|
|
115
|
-
expect(inserted.middleName).toBe('Middle');
|
|
116
|
-
expect(inserted.leafName).toBe('Leaf');
|
|
117
|
-
const loaded = await repository.load(inserted.id);
|
|
118
|
-
expect(loaded).toBeInstanceOf(LeafEntity);
|
|
119
|
-
expect(loaded.baseName).toBe('Base');
|
|
120
|
-
expect(loaded.middleName).toBe('Middle');
|
|
121
|
-
expect(loaded.leafName).toBe('Leaf');
|
|
95
|
+
const repository = injectRepository(LeafEntity);
|
|
96
|
+
const leaf = Object.assign(new LeafEntity(), {
|
|
97
|
+
baseName: 'Base',
|
|
98
|
+
middleName: 'Middle',
|
|
99
|
+
leafName: 'Leaf',
|
|
122
100
|
});
|
|
101
|
+
const inserted = await repository.insert(leaf);
|
|
102
|
+
expect(inserted.id).toBeDefined();
|
|
103
|
+
expect(inserted.baseName).toBe('Base');
|
|
104
|
+
expect(inserted.middleName).toBe('Middle');
|
|
105
|
+
expect(inserted.leafName).toBe('Leaf');
|
|
106
|
+
const loaded = await repository.load(inserted.id);
|
|
107
|
+
expect(loaded).toBeInstanceOf(LeafEntity);
|
|
108
|
+
expect(loaded.baseName).toBe('Base');
|
|
109
|
+
expect(loaded.middleName).toBe('Middle');
|
|
110
|
+
expect(loaded.leafName).toBe('Leaf');
|
|
123
111
|
});
|
|
124
112
|
test('should throw error on discriminator mismatch', async () => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await expect(leafRepository.load(middle.id)).rejects.toThrow();
|
|
134
|
-
});
|
|
113
|
+
const middleRepository = injectRepository(MiddleEntity);
|
|
114
|
+
const leafRepository = injectRepository(LeafEntity);
|
|
115
|
+
const middle = await middleRepository.insert(Object.assign(new MiddleEntity(), {
|
|
116
|
+
baseName: 'B',
|
|
117
|
+
middleName: 'M',
|
|
118
|
+
}));
|
|
119
|
+
// Try to load middle as leaf
|
|
120
|
+
await expect(leafRepository.load(middle.id)).rejects.toThrow();
|
|
135
121
|
});
|
|
136
122
|
test('should upsert correctly on child entities', async () => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
expect(upserted.middleName).toBe('M1_Updated');
|
|
146
|
-
});
|
|
123
|
+
const repository = injectRepository(MiddleEntity);
|
|
124
|
+
const e1 = Object.assign(new MiddleEntity(), { baseName: 'B1', middleName: 'M1' });
|
|
125
|
+
const inserted = await repository.insert(e1);
|
|
126
|
+
const update = Object.assign(new MiddleEntity(), { id: inserted.id, baseName: 'B1_Updated', middleName: 'M1_Updated' });
|
|
127
|
+
const upserted = await repository.upsert('id', update);
|
|
128
|
+
expect(upserted.id).toBe(inserted.id);
|
|
129
|
+
expect(upserted.baseName).toBe('B1_Updated');
|
|
130
|
+
expect(upserted.middleName).toBe('M1_Updated');
|
|
147
131
|
});
|
|
148
|
-
test('should handle upsert when parent exists but child does not', async () => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const loaded = await middleRepository.load(id);
|
|
167
|
-
expect(loaded.middleName).toBe('NewMiddle');
|
|
168
|
-
});
|
|
132
|
+
test('should handle upsert when parent exists but child does not', async ({ database: db }) => {
|
|
133
|
+
const middleRepository = injectRepository(MiddleEntity);
|
|
134
|
+
// Manually insert into base table only
|
|
135
|
+
const id = '00000000-0000-0000-0000-000000000001';
|
|
136
|
+
await db.execute(sql `
|
|
137
|
+
INSERT INTO ${sql.identifier(schema)}.${sql.identifier('base_entities')} (id, type, base_name, revision, revision_timestamp, create_timestamp, attributes)
|
|
138
|
+
VALUES (${id}, 'middle', 'OnlyBase', 1, now(), now(), '{}')
|
|
139
|
+
`);
|
|
140
|
+
// Now upsert via middle repository
|
|
141
|
+
const update = Object.assign(new MiddleEntity(), { id, baseName: 'Updated', middleName: 'NewMiddle' });
|
|
142
|
+
// This is expected to fail or behave unexpectedly if not handled,
|
|
143
|
+
// because upsert (onConflictDoUpdate) usually targets one table.
|
|
144
|
+
// In CTI, we might need special handling.
|
|
145
|
+
const upserted = await middleRepository.upsert('id', update);
|
|
146
|
+
expect(upserted.id).toBe(id);
|
|
147
|
+
expect(upserted.middleName).toBe('NewMiddle');
|
|
148
|
+
const loaded = await middleRepository.load(id);
|
|
149
|
+
expect(loaded.middleName).toBe('NewMiddle');
|
|
169
150
|
});
|
|
170
151
|
});
|
|
@@ -9,15 +9,16 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
};
|
|
10
10
|
import { Injector, runInInjectionContext } from '../../injector/index.js';
|
|
11
11
|
import { StringProperty } from '../../schema/index.js';
|
|
12
|
+
import { setupIntegrationTest } from '../../testing/index.js';
|
|
12
13
|
import { sql } from 'drizzle-orm';
|
|
13
14
|
import { beforeAll, describe, expect, test } from 'vitest';
|
|
14
15
|
import { ChildEntity, Column, EmbeddedProperty, Inheritance, Table } from '../decorators.js';
|
|
15
16
|
import { Entity } from '../entity.js';
|
|
16
|
-
import {
|
|
17
|
+
import { Database } from '../server/index.js';
|
|
17
18
|
import { injectRepository } from '../server/repository.js';
|
|
18
19
|
describe('ORM Repository CTI Embedded (Integration)', () => {
|
|
19
20
|
let injector;
|
|
20
|
-
let
|
|
21
|
+
let database;
|
|
21
22
|
const schema = 'test_orm_cti_embedded';
|
|
22
23
|
class Address {
|
|
23
24
|
street;
|
|
@@ -77,22 +78,11 @@ describe('ORM Repository CTI Embedded (Integration)', () => {
|
|
|
77
78
|
ChildEntity('user')
|
|
78
79
|
], UserWithContact);
|
|
79
80
|
beforeAll(async () => {
|
|
80
|
-
injector =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
port: 5432,
|
|
86
|
-
user: 'tstdl',
|
|
87
|
-
password: 'wf7rq6glrk5jykne',
|
|
88
|
-
database: 'tstdl',
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
db = injector.resolve(Database);
|
|
92
|
-
await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
93
|
-
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('users_with_contact')} CASCADE`);
|
|
94
|
-
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('entities_with_address')} CASCADE`);
|
|
95
|
-
await db.execute(sql `
|
|
81
|
+
({ injector, database } = await setupIntegrationTest({ orm: { schema } }));
|
|
82
|
+
await database.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
83
|
+
await database.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('users_with_contact')} CASCADE`);
|
|
84
|
+
await database.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('entities_with_address')} CASCADE`);
|
|
85
|
+
await database.execute(sql `
|
|
96
86
|
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('entities_with_address')} (
|
|
97
87
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
98
88
|
type TEXT NOT NULL,
|
|
@@ -106,7 +96,7 @@ describe('ORM Repository CTI Embedded (Integration)', () => {
|
|
|
106
96
|
UNIQUE (id, type)
|
|
107
97
|
)
|
|
108
98
|
`);
|
|
109
|
-
await
|
|
99
|
+
await database.execute(sql `
|
|
110
100
|
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('users_with_contact')} (
|
|
111
101
|
id UUID PRIMARY KEY,
|
|
112
102
|
type TEXT NOT NULL CHECK (type = 'user'),
|
|
@@ -11,13 +11,13 @@ import { sql } from 'drizzle-orm';
|
|
|
11
11
|
import { beforeAll, describe, expect, test } from 'vitest';
|
|
12
12
|
import { Injector, runInInjectionContext } from '../../injector/index.js';
|
|
13
13
|
import { StringProperty } from '../../schema/index.js';
|
|
14
|
+
import { setupIntegrationTest } from '../../testing/index.js';
|
|
14
15
|
import { ChildEntity, Column, GeneratedTsVector, Inheritance, Table } from '../decorators.js';
|
|
15
16
|
import { Entity } from '../entity.js';
|
|
16
|
-
import {
|
|
17
|
-
import { injectRepository } from '../server/repository.js';
|
|
17
|
+
import { injectRepository } from '../server/index.js';
|
|
18
18
|
describe('ORM Repository CTI Search (Integration)', () => {
|
|
19
19
|
let injector;
|
|
20
|
-
let
|
|
20
|
+
let database;
|
|
21
21
|
const schema = 'test_orm_cti_search';
|
|
22
22
|
let Item = class Item extends Entity {
|
|
23
23
|
type;
|
|
@@ -70,23 +70,12 @@ describe('ORM Repository CTI Search (Integration)', () => {
|
|
|
70
70
|
ChildEntity('electronic')
|
|
71
71
|
], Electronic);
|
|
72
72
|
beforeAll(async () => {
|
|
73
|
-
injector =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
user: 'tstdl',
|
|
80
|
-
password: 'wf7rq6glrk5jykne',
|
|
81
|
-
database: 'tstdl',
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
db = injector.resolve(Database);
|
|
85
|
-
await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
86
|
-
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('books')} CASCADE`);
|
|
87
|
-
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('electronics')} CASCADE`);
|
|
88
|
-
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('items')} CASCADE`);
|
|
89
|
-
await db.execute(sql `
|
|
73
|
+
({ injector, database } = await setupIntegrationTest({ orm: { schema } }));
|
|
74
|
+
await database.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
75
|
+
await database.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('books')} CASCADE`);
|
|
76
|
+
await database.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('electronics')} CASCADE`);
|
|
77
|
+
await database.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('items')} CASCADE`);
|
|
78
|
+
await database.execute(sql `
|
|
90
79
|
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('items')} (
|
|
91
80
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
92
81
|
type TEXT NOT NULL,
|
|
@@ -101,8 +90,8 @@ describe('ORM Repository CTI Search (Integration)', () => {
|
|
|
101
90
|
UNIQUE (id, type)
|
|
102
91
|
)
|
|
103
92
|
`);
|
|
104
|
-
await
|
|
105
|
-
await
|
|
93
|
+
await database.execute(sql `CREATE INDEX items_search_idx ON ${sql.identifier(schema)}.${sql.identifier('items')} USING GIN (search_vector)`);
|
|
94
|
+
await database.execute(sql `
|
|
106
95
|
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('books')} (
|
|
107
96
|
id UUID PRIMARY KEY,
|
|
108
97
|
type TEXT NOT NULL CHECK (type = 'book'),
|
|
@@ -110,7 +99,7 @@ describe('ORM Repository CTI Search (Integration)', () => {
|
|
|
110
99
|
FOREIGN KEY (id, type) REFERENCES ${sql.identifier(schema)}.${sql.identifier('items')} (id, type) ON DELETE CASCADE
|
|
111
100
|
)
|
|
112
101
|
`);
|
|
113
|
-
await
|
|
102
|
+
await database.execute(sql `
|
|
114
103
|
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('electronics')} (
|
|
115
104
|
id UUID PRIMARY KEY,
|
|
116
105
|
type TEXT NOT NULL CHECK (type = 'electronic'),
|