@tstdl/base 0.93.141 → 0.93.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/authentication/client/authentication.service.d.ts +1 -0
  2. package/authentication/client/authentication.service.js +3 -2
  3. package/circuit-breaker/circuit-breaker.d.ts +6 -4
  4. package/circuit-breaker/postgres/circuit-breaker.d.ts +1 -0
  5. package/circuit-breaker/postgres/circuit-breaker.js +8 -5
  6. package/circuit-breaker/tests/circuit-breaker.test.js +20 -0
  7. package/examples/document-management/main.js +2 -2
  8. package/notification/tests/notification-api.test.js +5 -1
  9. package/notification/tests/notification-flow.test.js +9 -6
  10. package/orm/decorators.d.ts +17 -4
  11. package/orm/decorators.js +9 -0
  12. package/orm/server/bootstrap.d.ts +11 -0
  13. package/orm/server/bootstrap.js +31 -0
  14. package/orm/server/drizzle/schema-converter.d.ts +3 -1
  15. package/orm/server/drizzle/schema-converter.js +71 -29
  16. package/orm/server/extension.d.ts +14 -0
  17. package/orm/server/extension.js +27 -0
  18. package/orm/server/index.d.ts +2 -0
  19. package/orm/server/index.js +2 -0
  20. package/orm/server/migration.d.ts +2 -3
  21. package/orm/server/migration.js +7 -21
  22. package/orm/server/repository.d.ts +1 -0
  23. package/orm/server/repository.js +19 -9
  24. package/orm/server/transaction.d.ts +1 -0
  25. package/orm/server/transaction.js +3 -0
  26. package/orm/tests/database-extension.test.js +63 -0
  27. package/orm/tests/database-migration.test.js +7 -6
  28. package/orm/tests/repository-compound-primary-key.test.d.ts +2 -0
  29. package/orm/tests/repository-compound-primary-key.test.js +234 -0
  30. package/orm/tests/schema-generation.test.d.ts +1 -0
  31. package/orm/tests/schema-generation.test.js +52 -5
  32. package/package.json +4 -4
  33. package/task-queue/README.md +0 -1
  34. package/task-queue/postgres/drizzle/0000_faithful_daimon_hellstrom.sql +84 -0
  35. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +155 -72
  36. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  37. package/task-queue/postgres/module.js +2 -1
  38. package/task-queue/postgres/schemas.d.ts +6 -0
  39. package/task-queue/postgres/task-queue.d.ts +18 -5
  40. package/task-queue/postgres/task-queue.js +577 -357
  41. package/task-queue/postgres/task.model.d.ts +10 -6
  42. package/task-queue/postgres/task.model.js +28 -28
  43. package/task-queue/task-context.d.ts +10 -5
  44. package/task-queue/task-context.js +5 -3
  45. package/task-queue/task-queue.d.ts +342 -38
  46. package/task-queue/task-queue.js +135 -31
  47. package/task-queue/tests/coverage-branch.test.js +46 -58
  48. package/task-queue/tests/coverage-enhancement.test.js +123 -117
  49. package/task-queue/tests/{extensive-dependencies.test.js → dag.test.js} +66 -37
  50. package/task-queue/tests/dependencies.test.js +143 -25
  51. package/task-queue/tests/enqueue-batch.test.js +125 -0
  52. package/task-queue/tests/fan-out-spawning.test.js +46 -5
  53. package/task-queue/tests/idempotent-replacement.test.js +54 -1
  54. package/task-queue/tests/missing-idempotent-tasks.test.js +9 -8
  55. package/task-queue/tests/queue.test.js +261 -25
  56. package/task-queue/tests/shutdown.test.js +41 -0
  57. package/task-queue/tests/transactions.test.d.ts +1 -0
  58. package/task-queue/tests/transactions.test.js +47 -0
  59. package/task-queue/tests/worker.test.js +46 -13
  60. package/task-queue/tests/zombie-parent.test.js +4 -4
  61. package/task-queue/tests/zombie-recovery.test.js +3 -3
  62. package/testing/integration-setup.js +5 -3
  63. package/utils/timing.d.ts +2 -2
  64. package/task-queue/postgres/drizzle/0000_wakeful_sunspot.sql +0 -82
  65. package/task-queue/tests/cascading-cancellations.test.js +0 -38
  66. package/task-queue/tests/complex.test.js +0 -122
  67. package/task-queue/tests/dag-dependencies.test.js +0 -41
  68. /package/{task-queue/tests/cascading-cancellations.test.d.ts → orm/tests/database-extension.test.d.ts} +0 -0
  69. /package/task-queue/tests/{complex.test.d.ts → dag.test.d.ts} +0 -0
  70. /package/task-queue/tests/{dag-dependencies.test.d.ts → enqueue-batch.test.d.ts} +0 -0
  71. /package/task-queue/tests/{extensive-dependencies.test.d.ts → shutdown.test.d.ts} +0 -0
@@ -27,6 +27,7 @@ export declare class AuthenticationClientService<AdditionalTokenPayload extends
27
27
  private readonly logger;
28
28
  private readonly disposeSignal;
29
29
  private readonly forceRefreshRequested;
30
+ private readonly forceRefreshRequested$;
30
31
  private clockOffset;
31
32
  private initialized;
32
33
  private refreshLoopPromise;
@@ -70,6 +70,7 @@ let AuthenticationClientService = class AuthenticationClientService {
70
70
  logger = inject(Logger, 'AuthenticationService');
71
71
  disposeSignal = inject(CancellationSignal).fork();
72
72
  forceRefreshRequested = signal(false);
73
+ forceRefreshRequested$ = toObservable(this.forceRefreshRequested);
73
74
  clockOffset = 0;
74
75
  initialized = false;
75
76
  refreshLoopPromise;
@@ -435,7 +436,7 @@ let AuthenticationClientService = class AuthenticationClientService {
435
436
  this.token$.pipe(filter((t) => t != currentToken)),
436
437
  ];
437
438
  if (!forceRefresh) {
438
- wakeUpSignals.push(toObservable(this.forceRefreshRequested).pipe(filter((v) => v)));
439
+ wakeUpSignals.push(this.forceRefreshRequested$.pipe(filter((requested) => requested)));
439
440
  }
440
441
  if (delay > 0) {
441
442
  await firstValueFrom(race([timer(delay), ...wakeUpSignals]), { defaultValue: undefined });
@@ -451,7 +452,7 @@ let AuthenticationClientService = class AuthenticationClientService {
451
452
  timer(2500),
452
453
  from(this.disposeSignal),
453
454
  this.token$.pipe(filter((t) => t != currentToken)),
454
- toObservable(this.forceRefreshRequested).pipe(filter((requested) => requested), skip(this.forceRefreshRequested() ? 1 : 0)),
455
+ this.forceRefreshRequested$.pipe(filter((requested) => requested), skip(this.forceRefreshRequested() ? 1 : 0)),
455
456
  ]), { defaultValue: undefined });
456
457
  }
457
458
  }
@@ -6,15 +6,15 @@ export declare const CircuitBreakerState: {
6
6
  readonly HalfOpen: "half-open";
7
7
  };
8
8
  export type CircuitBreakerState = EnumType<typeof CircuitBreakerState>;
9
- export interface CircuitBreakerConfig {
9
+ export type CircuitBreakerConfig = {
10
10
  threshold: number;
11
11
  resetTimeout: number;
12
- }
13
- export interface CircuitBreakerCheckResult {
12
+ };
13
+ export type CircuitBreakerCheckResult = {
14
14
  allowed: boolean;
15
15
  state: CircuitBreakerState;
16
16
  isProbe?: boolean;
17
- }
17
+ };
18
18
  export type CircuitBreakerArgument = string | (CircuitBreakerConfig & {
19
19
  key: string;
20
20
  });
@@ -29,4 +29,6 @@ export declare abstract class CircuitBreaker implements Resolvable<CircuitBreake
29
29
  abstract recordSuccess(): Promise<void>;
30
30
  /** Records a failure, potentially tripping the breaker to Open. */
31
31
  abstract recordFailure(): Promise<void>;
32
+ /** Records multiple failures at once. */
33
+ abstract recordFailures(count: number): Promise<void>;
32
34
  }
@@ -4,4 +4,5 @@ export declare class PostgresCircuitBreakerService extends CircuitBreaker {
4
4
  check(): Promise<CircuitBreakerCheckResult>;
5
5
  recordSuccess(): Promise<void>;
6
6
  recordFailure(): Promise<void>;
7
+ recordFailures(count: number): Promise<void>;
7
8
  }
@@ -43,8 +43,11 @@ let PostgresCircuitBreakerService = class PostgresCircuitBreakerService extends
43
43
  await this.#repository.tryDeleteByQuery({ key: this.#key });
44
44
  }
45
45
  async recordFailure() {
46
+ await this.recordFailures(1);
47
+ }
48
+ async recordFailures(count) {
46
49
  const table = this.#repository.table;
47
- const initialTrip = 1 >= this.#threshold;
50
+ const initialTrip = count >= this.#threshold;
48
51
  const initialState = initialTrip ? CircuitBreakerState.Open : CircuitBreakerState.Closed;
49
52
  const initialResetTimestamp = initialTrip
50
53
  ? sql `${TRANSACTION_TIMESTAMP} + ${interval(this.#resetTimeout, 'milliseconds')}`
@@ -52,16 +55,16 @@ let PostgresCircuitBreakerService = class PostgresCircuitBreakerService extends
52
55
  await this.#repository.upsert(['key'], {
53
56
  key: this.#key,
54
57
  state: initialState,
55
- failureCount: 1,
58
+ failureCount: count,
56
59
  resetTimestamp: initialResetTimestamp,
57
60
  }, {
58
- failureCount: sql `${table.failureCount} + 1`,
61
+ failureCount: sql `${table.failureCount} + ${count}`,
59
62
  state: sql `CASE
60
- WHEN ${table.failureCount} + 1 >= ${this.#threshold} THEN ${CircuitBreakerState.Open}
63
+ WHEN ${table.failureCount} + ${count} >= ${this.#threshold} THEN ${CircuitBreakerState.Open}
61
64
  ELSE ${table.state}
62
65
  END`,
63
66
  resetTimestamp: sql `CASE
64
- WHEN ${table.failureCount} + 1 >= ${this.#threshold} THEN ${TRANSACTION_TIMESTAMP} + ${interval(this.#resetTimeout, 'milliseconds')}
67
+ WHEN ${table.failureCount} + ${count} >= ${this.#threshold} THEN ${TRANSACTION_TIMESTAMP} + ${interval(this.#resetTimeout, 'milliseconds')}
65
68
  ELSE ${table.resetTimestamp}
66
69
  END`,
67
70
  });
@@ -90,4 +90,24 @@ describe('Circuit Breaker (Standalone) Tests', () => {
90
90
  expect(result.allowed).toBe(false);
91
91
  expect(result.state).toBe(CircuitBreakerState.Open);
92
92
  });
93
+ it('should trip to Open after threshold failures via recordFailures', async () => {
94
+ const breaker = provider.provide(`cb-bulk-trip-${Date.now()}`, { threshold: 5, resetTimeout: 1000 });
95
+ // 1. Bulk Failure (Threshold Not Reached)
96
+ await breaker.recordFailures(3);
97
+ let result = await breaker.check();
98
+ expect(result.state).toBe(CircuitBreakerState.Closed);
99
+ // 2. Bulk Failure (Threshold Reached)
100
+ await breaker.recordFailures(2);
101
+ result = await breaker.check();
102
+ expect(result.allowed).toBe(false);
103
+ expect(result.state).toBe(CircuitBreakerState.Open);
104
+ });
105
+ it('should trip to Open after threshold failures via single large recordFailures', async () => {
106
+ const breaker = provider.provide(`cb-single-bulk-trip-${Date.now()}`, { threshold: 5, resetTimeout: 1000 });
107
+ // Single large bulk failure exceeding threshold
108
+ await breaker.recordFailures(10);
109
+ const result = await breaker.check();
110
+ expect(result.allowed).toBe(false);
111
+ expect(result.state).toBe(CircuitBreakerState.Open);
112
+ });
93
113
  });
@@ -24,7 +24,7 @@ import { PrettyPrintLogFormatter, provideConsoleLogTransport } from '../../logge
24
24
  import { configureLocalMessageBus } from '../../message-bus/index.js';
25
25
  import { WebServerModule } from '../../module/index.js';
26
26
  import { configureS3ObjectStorage } from '../../object-storage/s3/index.js';
27
- import { configureOrm, provideDatabaseMigrator } from '../../orm/server/index.js';
27
+ import { configureOrm, provideOrm } from '../../orm/server/index.js';
28
28
  import { configurePostgresRateLimiter } from '../../rate-limit/postgres/module.js';
29
29
  import { configureDefaultSignalsImplementation } from '../../signals/implementation/configure.js';
30
30
  import { configurePostgresTaskQueue } from '../../task-queue/postgres/index.js';
@@ -173,5 +173,5 @@ Application.run('DocumentManagementTest', [
173
173
  provideModule(main, WebServerModule),
174
174
  provideSignalHandler(),
175
175
  provideConsoleLogTransport(PrettyPrintLogFormatter),
176
- provideDatabaseMigrator(),
176
+ provideOrm(),
177
177
  ]);
@@ -1,5 +1,5 @@
1
1
  import { Subject } from 'rxjs';
2
- import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
3
3
  import { SubjectService } from '../../authentication/server/subject.service.js';
4
4
  import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
5
5
  import { NotificationChannel } from '../models/index.js';
@@ -44,6 +44,10 @@ describe('Notification API (Integration)', () => {
44
44
  beforeEach(() => {
45
45
  vi.clearAllMocks();
46
46
  });
47
+ afterEach(async () => {
48
+ await clearTenantData(database, schema, ['in_app', 'in_app_archive', 'log', 'preference', 'web_push_subscription'], tenantId);
49
+ await clearTenantData(database, 'authentication', ['user', 'subject'], tenantId);
50
+ });
47
51
  const createMockContext = (params = {}) => ({
48
52
  parameters: params,
49
53
  abortSignal: new AbortController().signal,
@@ -4,13 +4,13 @@ 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
- import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
7
+ import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
8
8
  import { SubjectService } from '../../authentication/server/subject.service.js';
9
9
  import { runInInjectionContext, Singleton } from '../../injector/index.js';
10
10
  import { MailService } from '../../mail/mail.service.js';
11
11
  import { injectRepository } from '../../orm/server/index.js';
12
- import { clearTenantData, setupIntegrationTest, truncateTables } from '../../testing/index.js';
13
- import { InAppNotification, InAppNotificationArchive, NotificationChannel, NotificationLogEntity, NotificationPreference, NotificationStatus, NotificationType, WebPushSubscription } from '../models/index.js';
12
+ import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
13
+ import { InAppNotification, NotificationChannel, NotificationLogEntity, NotificationStatus, WebPushSubscription } from '../models/index.js';
14
14
  import { configureNotification } from '../server/module.js';
15
15
  import { EmailChannelProvider } from '../server/providers/email-channel-provider.js';
16
16
  import { NotificationAncillaryService } from '../server/services/notification-ancillary.service.js';
@@ -34,7 +34,7 @@ describe('Notification Flow (Integration)', () => {
34
34
  let subjectService;
35
35
  let mailServiceMock;
36
36
  const schema = 'notification';
37
- const tenantId = crypto.randomUUID();
37
+ let tenantId;
38
38
  beforeAll(async () => {
39
39
  ({ injector, database } = await setupIntegrationTest({
40
40
  orm: { schema },
@@ -63,10 +63,13 @@ describe('Notification Flow (Integration)', () => {
63
63
  worker.registerProvider(NotificationChannel.InApp, injector.resolve(InAppChannelProvider));
64
64
  });
65
65
  beforeEach(async () => {
66
- await truncateTables(database, schema, [NotificationLogEntity, InAppNotification, InAppNotificationArchive, NotificationType, NotificationPreference, WebPushSubscription]);
67
- await clearTenantData(database, 'authentication', ['user', 'subject'], tenantId);
66
+ tenantId = crypto.randomUUID();
68
67
  vi.clearAllMocks();
69
68
  });
69
+ afterEach(async () => {
70
+ await clearTenantData(database, schema, ['in_app', 'in_app_archive', 'log', 'preference', 'web_push_subscription'], tenantId);
71
+ await clearTenantData(database, 'authentication', ['user', 'subject'], tenantId);
72
+ });
70
73
  test('should execute full notification flow with escalation', async () => {
71
74
  await runInInjectionContext(injector, async () => {
72
75
  const logRepo = injectRepository(NotificationLogEntity);
@@ -40,8 +40,7 @@ export type WhereBuilder<T extends BaseEntity = BaseEntity> = SqlBuilder<T, Quer
40
40
  export type OrmTableReflectionData<T extends BaseEntity = BaseEntity> = {
41
41
  name?: string;
42
42
  schema?: string;
43
- compoundPrimaryKeyName?: string;
44
- compoundPrimaryKeyNaming?: NamingStrategy;
43
+ primaryKey?: PrimaryKeyReflectionData<T>;
45
44
  unique?: UniqueReflectionData[];
46
45
  index?: IndexReflectionData[];
47
46
  paradeIndex?: ParadeIndexReflectionData<T>;
@@ -90,11 +89,18 @@ type ReferenceReflectionData<T extends AnyEntity = AnyEntity> = {
90
89
  onDelete?: UpdateDeleteAction;
91
90
  onUpdate?: UpdateDeleteAction;
92
91
  };
92
+ export type PrimaryKeyReflectionData<T extends BaseEntity = any> = {
93
+ columns?: ValueOrProvider<(TargetColumnPath<T> | SQL | ExtraConfigColumn)[], ExtraConfigColumnsFromType<EntityType<T>>>;
94
+ options?: {
95
+ name?: string;
96
+ naming?: NamingStrategy;
97
+ };
98
+ };
93
99
  /**
94
100
  * Reflection data for unique constraints.
95
101
  */
96
- export type UniqueReflectionData = {
97
- columns?: string[];
102
+ export type UniqueReflectionData<T extends BaseEntity = any> = {
103
+ columns?: ValueOrProvider<(TargetColumnPath<T> | SQL | ExtraConfigColumn)[], ExtraConfigColumnsFromType<EntityType<T>>>;
98
104
  options?: {
99
105
  name?: string;
100
106
  naming?: NamingStrategy;
@@ -294,6 +300,13 @@ export declare function Table(name?: string, options?: TableOptions): ClassDecor
294
300
  * @param options Table options including name and schema.
295
301
  */
296
302
  export declare function Table(options?: TableOptions): ClassDecorator;
303
+ /**
304
+ * Define a composite primary key on multiple columns.
305
+ * @template T The entity type.
306
+ * @param columns An array of property names included in the primary key.
307
+ * @param options Additional primary key options.
308
+ */
309
+ export declare function PrimaryKey<T extends AnyEntity>(columns: Columns<T>, options?: PrimaryKeyReflectionData['options']): ClassDecorator;
297
310
  /**
298
311
  * Define a foreign key relationship.
299
312
  * @param target A function returning the referenced entity type.
package/orm/decorators.js CHANGED
@@ -113,6 +113,15 @@ export function Table(nameOrOptions, optionsOrNothing) {
113
113
  : filterUndefinedObjectProperties({ name, schema });
114
114
  return createTableDecorator(data);
115
115
  }
116
+ /**
117
+ * Define a composite primary key on multiple columns.
118
+ * @template T The entity type.
119
+ * @param columns An array of property names included in the primary key.
120
+ * @param options Additional primary key options.
121
+ */
122
+ export function PrimaryKey(columns, options) {
123
+ return createTableDecorator({ primaryKey: { columns, options } });
124
+ }
116
125
  /**
117
126
  * Define a foreign key relationship.
118
127
  * @param target A function returning the referenced entity type.
@@ -0,0 +1,11 @@
1
+ import { type ProvidersItem } from '../../injector/injector.js';
2
+ /**
3
+ * Provides the ORM bootstrap functionality by returning an initializer provider.
4
+ * This will run database extensions and migrations during application startup.
5
+ * @returns A provider item.
6
+ */
7
+ export declare function provideOrm(): ProvidersItem;
8
+ /**
9
+ * Bootstraps the ORM by running database extensions and migrations within an advisory lock.
10
+ */
11
+ export declare function bootstrapOrm(): Promise<void>;
@@ -0,0 +1,31 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { provideInitializer } from '../../application/providers.js';
3
+ import { inject, runInInjectionContext } from '../../injector/inject.js';
4
+ import { Injector } from '../../injector/injector.js';
5
+ import { Database } from './database.js';
6
+ import { runDatabaseExtensions } from './extension.js';
7
+ import { runDatabaseMigrationsCore } from './migration.js';
8
+ /**
9
+ * Provides the ORM bootstrap functionality by returning an initializer provider.
10
+ * This will run database extensions and migrations during application startup.
11
+ * @returns A provider item.
12
+ */
13
+ export function provideOrm() {
14
+ return provideInitializer(bootstrapOrm);
15
+ }
16
+ /**
17
+ * Bootstraps the ORM by running database extensions and migrations within an advisory lock.
18
+ */
19
+ export async function bootstrapOrm() {
20
+ const injector = inject(Injector);
21
+ const database = inject(Database);
22
+ const lockId = 123456789; // Fixed lock ID for database setup
23
+ await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
24
+ try {
25
+ await runInInjectionContext(injector, async () => await runDatabaseExtensions());
26
+ await runInInjectionContext(injector, async () => await runDatabaseMigrationsCore());
27
+ }
28
+ finally {
29
+ await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
30
+ }
31
+ }
@@ -1,4 +1,4 @@
1
- import { type PgEnum, type PgSchema, type PgTableWithColumns } from 'drizzle-orm/pg-core';
1
+ import { type PgColumn, type PgEnum, type PgSchema, type PgTableWithColumns } from 'drizzle-orm/pg-core';
2
2
  import type { AbstractConstructor, Enumeration } from '../../../types/index.js';
3
3
  import { type EntityType } from '../../entity.js';
4
4
  import type { ColumnDefinition, ColumnDefinitionsMap, PgTableFromType } from '../types.js';
@@ -13,4 +13,6 @@ export declare function getColumnDefinitionsMap(table: PgTableWithColumns<any>):
13
13
  export declare function _getDrizzleTableFromType<T extends EntityType, S extends string>(type: T, fallbackSchemaName?: S): PgTableFromType<T, S>;
14
14
  export declare function isTableOwning(type: AbstractConstructor): boolean;
15
15
  export declare function getPgEnum(schema: string | PgSchema, enumeration: Enumeration, context?: ConverterContext): PgEnum<[string, ...string[]]>;
16
+ export declare function getPrimaryKeyColumnDefinitions(type: EntityType, table: PgTableFromType, columnDefinitions?: ColumnDefinition[], columnDefinitionsMap?: ColumnDefinitionsMap): ColumnDefinition[];
17
+ export declare function getPrimaryKeyColumns(type: EntityType, table: PgTableFromType, columnDefinitions?: ColumnDefinition[], columnDefinitionsMap?: ColumnDefinitionsMap): PgColumn[];
16
18
  export {};
@@ -15,7 +15,7 @@ import { typeExtends } from '../../../utils/index.js';
15
15
  import { merge } from '../../../utils/merge.js';
16
16
  import { compileDereferencer } from '../../../utils/object/dereference.js';
17
17
  import { fromEntries, mapObjectKeysToSnakeCase, objectEntries } from '../../../utils/object/object.js';
18
- import { assertDefined, isArray, isDefined, isNotNull, isNull, isString, isUndefined } from '../../../utils/type-guards.js';
18
+ import { assertDefined, assertDefinedPass, isArray, isDefined, isNotNull, isNull, isString, isUndefined } from '../../../utils/type-guards.js';
19
19
  import { resolveValueOrProvider } from '../../../utils/value-or-provider.js';
20
20
  import { bytea, numericDate, timestamp, tsvector } from '../../data-types/index.js';
21
21
  import { TenantBaseEntity, TenantEntity } from '../../entity.js';
@@ -47,8 +47,13 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
47
47
  const dbSchema = getDbSchema(schema);
48
48
  const inheritanceMetadata = getInheritanceMetadata(type);
49
49
  const allColumnDefinitions = getPostgresColumnEntries(type, dbSchema, tableName, undefined, '', { filterInherited: false }, type);
50
+ const primaryKeyPropertyNames = new Set(isDefined(mergedTableReflectionData.primaryKey?.columns)
51
+ ? resolveValueOrProvider(mergedTableReflectionData.primaryKey.columns, {}).filter((c) => isString(c))
52
+ : []);
50
53
  const columnDefinitions = allColumnDefinitions.filter((cd) => !isChildEntity(type)
51
54
  || !cd.inherited
55
+ || (cd.reflectionData?.primaryKey == true)
56
+ || primaryKeyPropertyNames.has(cd.objectPath.path)
52
57
  || (cd.objectPath.path == 'id')
53
58
  || (cd.objectPath.path == 'tenantId')
54
59
  || (cd.objectPath.path == inheritanceMetadata?.discriminatorColumn));
@@ -100,23 +105,43 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
100
105
  }
101
106
  return builder;
102
107
  }
103
- function buildInheritanceCompositeUniqueConstraint(table) {
104
- const inheritanceMetadata = mergedTableReflectionData.inheritance;
108
+ function getInheritanceColumns(inheritanceTable, inheritanceType) {
109
+ const inheritanceMetadata = getInheritanceMetadata(inheritanceType);
105
110
  const discriminatorColumn = inheritanceMetadata.discriminatorColumn;
106
- const hasTenantId = isDefined(table['tenantId']);
111
+ const pkColumnDefinitions = getPrimaryKeyColumnDefinitions(inheritanceType, inheritanceTable, columnDefinitions, columnDefinitionsMap);
112
+ const discriminatorColumnObject = inheritanceTable[discriminatorColumn];
113
+ const hasTenantId = pkColumnDefinitions.some((d) => d.objectPath.path == 'tenantId');
114
+ const tenantIdColumn = hasTenantId ? inheritanceTable['tenantId'] : undefined;
107
115
  const columns = [
108
- ...(hasTenantId ? [table['tenantId']] : []),
109
- table[discriminatorColumn],
110
- table['id'],
116
+ ...(isDefined(tenantIdColumn) ? [tenantIdColumn] : []),
117
+ discriminatorColumnObject,
118
+ ...pkColumnDefinitions.filter((d) => d.objectPath.path != 'tenantId').map((d) => inheritanceTable[d.name]),
119
+ ];
120
+ const identifierParts = [
121
+ ...(hasTenantId ? ['tenantId'] : []),
122
+ discriminatorColumn,
123
+ ...pkColumnDefinitions.filter((d) => d.objectPath.path != 'tenantId').map((d) => d.objectPath.path),
111
124
  ];
125
+ return { columns, identifierParts };
126
+ }
127
+ function buildInheritanceCompositeUniqueConstraint(table) {
128
+ const { columns } = getInheritanceColumns(table, type);
112
129
  return unique(getUniqueName(tableName, columns)).on(...columns);
113
130
  }
114
131
  function buildPrimaryKey(table) {
115
- const columns = primaryKeyColumnDefinitions
116
- .toSorted(compareByValueSelectionToOrder(['tenantId', 'id', orderRest], (columnDefinition) => columnDefinition.name))
117
- .map((columnDefinition) => table[columnDefinition.name]);
132
+ const primaryKeyData = mergedTableReflectionData.primaryKey;
133
+ const resolvedColumns = isDefined(primaryKeyData?.columns) ? resolveValueOrProvider(primaryKeyData.columns, table) : undefined;
134
+ const columns = match(resolvedColumns)
135
+ .with(P.nonNullable, (primaryColumns) => primaryColumns.map((columnValue) => {
136
+ if (columnValue instanceof SQL) {
137
+ return columnValue;
138
+ }
139
+ return getColumn(table, columnValue);
140
+ }))
141
+ .with(undefined, () => getPrimaryKeyColumns(type, table, columnDefinitions, columnDefinitionsMap))
142
+ .exhaustive();
118
143
  return primaryKey({
119
- name: mergedTableReflectionData.compoundPrimaryKeyName ?? getPrimaryKeyName(tableName, columns, { naming: mergedTableReflectionData.compoundPrimaryKeyNaming }),
144
+ name: primaryKeyData?.options?.name ?? getPrimaryKeyName(tableName, columns, { naming: primaryKeyData?.options?.naming }),
120
145
  columns,
121
146
  });
122
147
  }
@@ -128,22 +153,14 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
128
153
  constraints.push(buildInheritanceCompositeUniqueConstraint(table));
129
154
  }
130
155
  if (isDefined(inheritanceMetadata) && isDefined(childEntityMetadata)) {
131
- const hasTenantId = isDefined(table['tenantId']);
156
+ const parentType = reflectionRegistry.getMetadata(type).parent;
157
+ const parentTable = getDrizzleTableFromType(parentType, schema);
158
+ const { columns: foreignKeyColumns, identifierParts: foreignKeyIdentifierParts } = getInheritanceColumns(table, type);
159
+ const { columns: parentForeignKeyColumns } = getInheritanceColumns(parentTable, parentType);
132
160
  constraints.push(check(getIdentifier(tableName, discriminatorColumn, 'check'), eq(table[discriminatorColumn], sql.raw(`'${childEntityMetadata.discriminatorValue}'`))), foreignKey({
133
- name: getForeignKeyName(tableName, getEntityTableName(reflectionRegistry.getMetadata(type).parent), [...(hasTenantId ? ['tenantId'] : []), discriminatorColumn, 'id']),
134
- columns: [
135
- ...(hasTenantId ? [table['tenantId']] : []),
136
- table[discriminatorColumn],
137
- table['id'],
138
- ],
139
- foreignColumns: (() => {
140
- const parentTable = getDrizzleTableFromType(reflectionRegistry.getMetadata(type).parent, schema);
141
- return [
142
- ...(hasTenantId ? [parentTable['tenantId']] : []),
143
- parentTable[discriminatorColumn],
144
- parentTable.id,
145
- ];
146
- })(),
161
+ name: getForeignKeyName(tableName, getEntityTableName(parentType), foreignKeyIdentifierParts),
162
+ columns: foreignKeyColumns,
163
+ foreignColumns: parentForeignKeyColumns,
147
164
  }).onDelete('cascade'));
148
165
  }
149
166
  return constraints;
@@ -222,7 +239,7 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
222
239
  return builder;
223
240
  }
224
241
  const primaryKeyColumnDefinitions = columnDefinitions.filter((columnDefinition) => (columnDefinition.reflectionData?.primaryKey == true) || (columnDefinition.objectPath.path == 'id' && columnDefinition.reflectionData?.primaryKey != false));
225
- const skipPrimaryKey = primaryKeyColumnDefinitions.length > 1;
242
+ const skipPrimaryKey = (primaryKeyColumnDefinitions.length > 1) || isDefined(mergedTableReflectionData.primaryKey?.columns);
226
243
  const columnEntries = columnDefinitions.map((entry) => [entry.name, entry.buildType({ skipPrimaryKey })]);
227
244
  const drizzleSchema = dbSchema.table(tableName, fromEntries(columnEntries), (drizzleTable) => {
228
245
  const table = drizzleTable;
@@ -231,7 +248,7 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
231
248
  const bm25Index = buildParadeIndex(table);
232
249
  const checks = tableReflectionDatas.flatMap((tableReflectionData) => tableReflectionData.checks).filter(isDefined).map((data) => check(data.name, data.builder(table)));
233
250
  return [
234
- ...((primaryKeyColumnDefinitions.length > 1)
251
+ ...(skipPrimaryKey
235
252
  ? [buildPrimaryKey(table)]
236
253
  : []),
237
254
  ...buildInheritanceConstraints(table),
@@ -272,7 +289,8 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
272
289
  }) ?? [];
273
290
  }),
274
291
  ...tableReflectionDatas.flatMap((tableReflectionData) => tableReflectionData.unique).filter(isDefined).map((data) => {
275
- const columns = data.columns?.map((column) => getColumn(table, column));
292
+ const resolvedColumns = resolveValueOrProvider(data.columns, table);
293
+ const columns = resolvedColumns.map((column) => getColumn(table, column));
276
294
  let constraint = unique(data.options?.name ?? getUniqueName(tableName, columns, { naming: data.options?.naming })).on(...columns);
277
295
  if (data.options?.nulls == 'not distinct') {
278
296
  constraint = constraint.nullsNotDistinct();
@@ -469,6 +487,30 @@ export function getPgEnum(schema, enumeration, context) {
469
487
  }
470
488
  return dbEnum;
471
489
  }
490
+ export function getPrimaryKeyColumnDefinitions(type, table, columnDefinitions, columnDefinitionsMap) {
491
+ const tableReflectionDatas = getTableReflectionDatas(type);
492
+ const mergedTableReflectionData = tableReflectionDatas.reduceRight((merged, data) => ({ ...merged, ...data }), {});
493
+ const resolvedColumnDefinitions = columnDefinitions ?? getColumnDefinitions(table);
494
+ const resolvedColumnDefinitionsMap = columnDefinitionsMap ?? getColumnDefinitionsMap(table);
495
+ const primaryKeyData = mergedTableReflectionData.primaryKey;
496
+ let definitions;
497
+ if (isDefined(primaryKeyData?.columns)) {
498
+ const resolvedColumns = resolveValueOrProvider(primaryKeyData.columns, table);
499
+ definitions = resolvedColumns.map((columnValue) => {
500
+ if (columnValue instanceof SQL) {
501
+ throw new NotSupportedError('SQL in primary key is not supported by Repository.');
502
+ }
503
+ return assertDefinedPass(resolvedColumnDefinitionsMap.get(columnValue), `Could not map primary key property ${columnValue} to column.`);
504
+ });
505
+ }
506
+ else {
507
+ definitions = resolvedColumnDefinitions.filter((columnDefinition) => (columnDefinition.reflectionData?.primaryKey == true) || (columnDefinition.objectPath.path == 'id' && columnDefinition.reflectionData?.primaryKey != false));
508
+ }
509
+ return definitions.toSorted(compareByValueSelectionToOrder(['tenantId', 'id', orderRest], (columnDefinition) => columnDefinition.objectPath.path));
510
+ }
511
+ export function getPrimaryKeyColumns(type, table, columnDefinitions, columnDefinitionsMap) {
512
+ return getPrimaryKeyColumnDefinitions(type, table, columnDefinitions, columnDefinitionsMap).map((def) => table[def.name]);
513
+ }
472
514
  function getPrimaryKeyName(tableName, columnsOrBaseName, options) {
473
515
  return getIdentifier(tableName, columnsOrBaseName, 'pk', options);
474
516
  }
@@ -0,0 +1,14 @@
1
+ import { Injector } from '../../injector/index.js';
2
+ export type DatabaseExtension = {
3
+ name: string;
4
+ };
5
+ export declare const DATABASE_EXTENSION: import("../../injector/index.js").InjectionToken<DatabaseExtension, never>;
6
+ /**
7
+ * Registers a database extension in the provided injector.
8
+ * @param name - The name of the extension.
9
+ * @param options - Optional injector.
10
+ */
11
+ export declare function registerDatabaseExtension(name: string, { injector }?: {
12
+ injector?: Injector;
13
+ }): void;
14
+ export declare function runDatabaseExtensions(): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { injectionToken, Injector } from '../../injector/index.js';
3
+ import { inject, injectAll } from '../../injector/inject.js';
4
+ import { Logger } from '../../logger/index.js';
5
+ import { Database } from './database.js';
6
+ export const DATABASE_EXTENSION = injectionToken('DatabaseExtension');
7
+ /**
8
+ * Registers a database extension in the provided injector.
9
+ * @param name - The name of the extension.
10
+ * @param options - Optional injector.
11
+ */
12
+ export function registerDatabaseExtension(name, { injector } = {}) {
13
+ const targetInjector = injector ?? Injector;
14
+ targetInjector.register(DATABASE_EXTENSION, { useValue: { name } }, { multi: true });
15
+ }
16
+ export async function runDatabaseExtensions() {
17
+ const database = inject(Database);
18
+ const logger = inject(Logger, 'DatabaseExtensionOrchestrator');
19
+ const extensions = injectAll(DATABASE_EXTENSION, undefined, { optional: true });
20
+ if (extensions.length == 0) {
21
+ return;
22
+ }
23
+ for (const extension of extensions) {
24
+ logger.info(`Ensuring database extension: ${extension.name}`);
25
+ await database.execute(sql.raw(`CREATE EXTENSION IF NOT EXISTS "${extension.name}"`));
26
+ }
27
+ }
@@ -1,5 +1,7 @@
1
1
  export * from './database-schema.js';
2
2
  export * from './database.js';
3
+ export * from './bootstrap.js';
4
+ export * from './extension.js';
3
5
  export * from './migration.js';
4
6
  export * from './module.js';
5
7
  export * from './query-converter.js';
@@ -1,5 +1,7 @@
1
1
  export * from './database-schema.js';
2
2
  export * from './database.js';
3
+ export * from './bootstrap.js';
4
+ export * from './extension.js';
3
5
  export * from './migration.js';
4
6
  export * from './module.js';
5
7
  export * from './query-converter.js';
@@ -1,4 +1,4 @@
1
- import { Injector, type ProvidersItem } from '../../injector/index.js';
1
+ import { Injector } from '../../injector/index.js';
2
2
  export type DatabaseMigration = {
3
3
  name: string;
4
4
  migrate: () => void | Promise<void>;
@@ -15,5 +15,4 @@ export declare function registerDatabaseMigration(name: string, migrate: () => v
15
15
  injector?: Injector;
16
16
  dependencies?: string[];
17
17
  }): void;
18
- export declare function provideDatabaseMigrator(): ProvidersItem;
19
- export declare function runDatabaseMigrations(): Promise<void>;
18
+ export declare function runDatabaseMigrationsCore(migrations?: DatabaseMigration[]): Promise<void>;
@@ -1,10 +1,7 @@
1
- import { sql } from 'drizzle-orm';
2
- import { provideInitializer } from '../../application/providers.js';
3
1
  import { injectionToken, Injector } from '../../injector/index.js';
4
2
  import { inject, injectAll, runInInjectionContext } from '../../injector/inject.js';
5
3
  import { Logger } from '../../logger/index.js';
6
4
  import { isDefined } from '../../utils/type-guards.js';
7
- import { Database } from './database.js';
8
5
  export const DATABASE_MIGRATION = injectionToken('DatabaseMigration');
9
6
  /**
10
7
  * Registers a database migration in the provided injector.
@@ -16,28 +13,17 @@ export function registerDatabaseMigration(name, migrate, { injector, dependencie
16
13
  const targetInjector = injector ?? Injector;
17
14
  targetInjector.register(DATABASE_MIGRATION, { useValue: { name, migrate, dependencies } }, { multi: true });
18
15
  }
19
- export function provideDatabaseMigrator() {
20
- return provideInitializer(runDatabaseMigrations);
21
- }
22
- export async function runDatabaseMigrations() {
16
+ export async function runDatabaseMigrationsCore(migrations) {
23
17
  const injector = inject(Injector);
24
- const database = inject(Database);
25
18
  const logger = inject(Logger, 'DatabaseMigrationOrchestrator');
26
- const migrations = injectAll(DATABASE_MIGRATION, undefined, { optional: true });
27
- if (migrations.length == 0) {
19
+ const actualMigrations = migrations ?? injectAll(DATABASE_MIGRATION, undefined, { optional: true });
20
+ if (actualMigrations.length == 0) {
28
21
  return;
29
22
  }
30
- const lockId = 123456789; // Fixed lock ID for migrations
31
- await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
32
- try {
33
- const sortedMigrations = sortMigrations(migrations);
34
- for (const migration of sortedMigrations) {
35
- logger.info(`Running database migration: ${migration.name}`);
36
- await runInInjectionContext(injector, async () => await migration.migrate());
37
- }
38
- }
39
- finally {
40
- await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
23
+ const sortedMigrations = sortMigrations(actualMigrations);
24
+ for (const migration of sortedMigrations) {
25
+ logger.info(`Running database migration: ${migration.name}`);
26
+ await runInInjectionContext(injector, async () => await migration.migrate());
41
27
  }
42
28
  }
43
29
  function sortMigrations(migrations) {
@@ -1,3 +1,4 @@
1
+ /** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
1
2
  import { SQL, type SQLWrapper } from 'drizzle-orm';
2
3
  import type { AnyPgTable, PgColumn, PgInsertValue, PgSelectBuilder, PgUpdateSetSource, SelectedFields } from 'drizzle-orm/pg-core';
3
4
  import { afterResolve, resolveArgumentType, type Resolvable } from '../../injector/interfaces.js';