@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.
@@ -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';
@@ -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/bad-request.error.js';
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
- const token = await requestContext.tryGetToken();
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 { afterResolve, injectArgument, provide, Singleton } from '../../injector/index.js';
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
- [afterResolve]() {
27
- PostgresCircuitBreakerService_1.checkStatement ??= this.getPreparedCheckStatement();
28
- }
23
+ #checkStatement = this.getPreparedCheckStatement();
29
24
  async check() {
30
- const [result] = await PostgresCircuitBreakerService_1.checkStatement.execute({ key: this.#key });
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. Fallback: Catch-all for failed transitions.
40
- // - If state is HalfOpen, someone else is probing. Reject.
41
- // - If state is Open, timeout hasn't expired. Reject
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 = PostgresCircuitBreakerService_1 = __decorate([
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 denied (Half-Open wait)
59
+ // Subsequent check should be allowed but not the probe
60
60
  const subsequent = await breaker.check();
61
- expect(subsequent.allowed).toBe(false);
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):9000/);
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):9000/);
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 { beforeAll, describe, expect, test } from 'vitest';
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
- beforeAll(async () => {
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
- await runInInjectionContext(injector, async () => {
106
- const repository = injectRepository(LeafEntity);
107
- const leaf = Object.assign(new LeafEntity(), {
108
- baseName: 'Base',
109
- middleName: 'Middle',
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
- await runInInjectionContext(injector, async () => {
126
- const middleRepository = injectRepository(MiddleEntity);
127
- const leafRepository = injectRepository(LeafEntity);
128
- const middle = await middleRepository.insert(Object.assign(new MiddleEntity(), {
129
- baseName: 'B',
130
- middleName: 'M',
131
- }));
132
- // Try to load middle as leaf
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
- await runInInjectionContext(injector, async () => {
138
- const repository = injectRepository(MiddleEntity);
139
- const e1 = Object.assign(new MiddleEntity(), { baseName: 'B1', middleName: 'M1' });
140
- const inserted = await repository.insert(e1);
141
- const update = Object.assign(new MiddleEntity(), { id: inserted.id, baseName: 'B1_Updated', middleName: 'M1_Updated' });
142
- const upserted = await repository.upsert('id', update);
143
- expect(upserted.id).toBe(inserted.id);
144
- expect(upserted.baseName).toBe('B1_Updated');
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
- await runInInjectionContext(injector, async () => {
150
- const baseRepository = injectRepository(BaseEntity);
151
- const middleRepository = injectRepository(MiddleEntity);
152
- // Manually insert into base table only
153
- const id = '00000000-0000-0000-0000-000000000001';
154
- await db.execute(sql `
155
- INSERT INTO ${sql.identifier(schema)}.${sql.identifier('base_entities')} (id, type, base_name, revision, revision_timestamp, create_timestamp, attributes)
156
- VALUES (${id}, 'middle', 'OnlyBase', 1, now(), now(), '{}')
157
- `);
158
- // Now upsert via middle repository
159
- const update = Object.assign(new MiddleEntity(), { id, baseName: 'Updated', middleName: 'NewMiddle' });
160
- // This is expected to fail or behave unexpectedly if not handled,
161
- // because upsert (onConflictDoUpdate) usually targets one table.
162
- // In CTI, we might need special handling.
163
- const upserted = await middleRepository.upsert('id', update);
164
- expect(upserted.id).toBe(id);
165
- expect(upserted.middleName).toBe('NewMiddle');
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 { configureOrm, Database } from '../server/index.js';
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 db;
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 = new Injector('Test');
81
- configureOrm({
82
- repositoryConfig: { schema },
83
- connection: {
84
- host: '127.0.0.1',
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 db.execute(sql `
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 { configureOrm, Database } from '../server/index.js';
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 db;
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 = new Injector('Test');
74
- configureOrm({
75
- repositoryConfig: { schema },
76
- connection: {
77
- host: '127.0.0.1',
78
- port: 5432,
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 db.execute(sql `CREATE INDEX items_search_idx ON ${sql.identifier(schema)}.${sql.identifier('items')} USING GIN (search_vector)`);
105
- await db.execute(sql `
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 db.execute(sql `
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'),