@tstdl/base 0.93.140 → 0.93.142

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 (123) hide show
  1. package/application/application.d.ts +1 -1
  2. package/application/application.js +1 -1
  3. package/application/providers.d.ts +20 -2
  4. package/application/providers.js +34 -7
  5. package/audit/module.d.ts +5 -0
  6. package/audit/module.js +9 -1
  7. package/authentication/client/authentication.service.d.ts +1 -0
  8. package/authentication/client/authentication.service.js +3 -2
  9. package/authentication/server/module.d.ts +5 -0
  10. package/authentication/server/module.js +9 -1
  11. package/authentication/tests/authentication.api-controller.test.js +1 -1
  12. package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
  13. package/authentication/tests/authentication.client-service.test.js +1 -1
  14. package/circuit-breaker/circuit-breaker.d.ts +6 -4
  15. package/circuit-breaker/postgres/circuit-breaker.d.ts +1 -0
  16. package/circuit-breaker/postgres/circuit-breaker.js +8 -5
  17. package/circuit-breaker/postgres/module.d.ts +1 -0
  18. package/circuit-breaker/postgres/module.js +5 -1
  19. package/circuit-breaker/tests/circuit-breaker.test.js +20 -0
  20. package/document-management/server/configure.js +5 -1
  21. package/document-management/server/module.d.ts +1 -1
  22. package/document-management/server/module.js +1 -1
  23. package/document-management/server/services/document-management-ancillary.service.js +1 -1
  24. package/document-management/tests/ai-config-hierarchy.test.js +0 -5
  25. package/document-management/tests/document-management-ai-overrides.test.js +0 -1
  26. package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
  27. package/examples/document-management/main.d.ts +1 -0
  28. package/examples/document-management/main.js +14 -11
  29. package/key-value-store/postgres/module.d.ts +1 -0
  30. package/key-value-store/postgres/module.js +5 -1
  31. package/lock/postgres/module.d.ts +1 -0
  32. package/lock/postgres/module.js +5 -1
  33. package/mail/module.d.ts +5 -1
  34. package/mail/module.js +11 -6
  35. package/module/modules/web-server.module.js +2 -3
  36. package/notification/server/module.d.ts +1 -0
  37. package/notification/server/module.js +5 -1
  38. package/notification/tests/notification-api.test.js +5 -1
  39. package/notification/tests/notification-flow.test.js +8 -5
  40. package/orm/decorators.d.ts +22 -5
  41. package/orm/decorators.js +10 -1
  42. package/orm/server/bootstrap.d.ts +11 -0
  43. package/orm/server/bootstrap.js +31 -0
  44. package/orm/server/drizzle/schema-converter.d.ts +3 -1
  45. package/orm/server/drizzle/schema-converter.js +85 -56
  46. package/orm/server/encryption.d.ts +0 -1
  47. package/orm/server/encryption.js +1 -4
  48. package/orm/server/extension.d.ts +14 -0
  49. package/orm/server/extension.js +27 -0
  50. package/orm/server/index.d.ts +3 -6
  51. package/orm/server/index.js +3 -6
  52. package/orm/server/migration.d.ts +18 -0
  53. package/orm/server/migration.js +58 -0
  54. package/orm/server/repository.d.ts +2 -1
  55. package/orm/server/repository.js +19 -9
  56. package/orm/server/transaction.d.ts +6 -10
  57. package/orm/server/transaction.js +25 -26
  58. package/orm/server/transactional.js +3 -3
  59. package/orm/tests/database-extension.test.js +63 -0
  60. package/orm/tests/database-migration.test.js +83 -0
  61. package/orm/tests/encryption.test.js +3 -4
  62. package/orm/tests/repository-compound-primary-key.test.d.ts +2 -0
  63. package/orm/tests/repository-compound-primary-key.test.js +234 -0
  64. package/orm/tests/schema-generation.test.d.ts +1 -0
  65. package/orm/tests/schema-generation.test.js +52 -5
  66. package/orm/utils.d.ts +17 -2
  67. package/orm/utils.js +49 -1
  68. package/package.json +5 -4
  69. package/rate-limit/postgres/module.d.ts +1 -0
  70. package/rate-limit/postgres/module.js +5 -1
  71. package/reflection/decorator-data.js +11 -12
  72. package/task-queue/README.md +2 -10
  73. package/task-queue/postgres/drizzle/0000_great_gwen_stacy.sql +84 -0
  74. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +250 -89
  75. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  76. package/task-queue/postgres/module.d.ts +1 -0
  77. package/task-queue/postgres/module.js +6 -1
  78. package/task-queue/postgres/schemas.d.ts +15 -6
  79. package/task-queue/postgres/schemas.js +4 -3
  80. package/task-queue/postgres/task-queue.d.ts +18 -15
  81. package/task-queue/postgres/task-queue.js +797 -499
  82. package/task-queue/postgres/task.model.d.ts +20 -9
  83. package/task-queue/postgres/task.model.js +65 -39
  84. package/task-queue/task-context.d.ts +12 -7
  85. package/task-queue/task-context.js +8 -6
  86. package/task-queue/task-queue.d.ts +364 -43
  87. package/task-queue/task-queue.js +153 -41
  88. package/task-queue/tests/coverage-branch.test.d.ts +1 -0
  89. package/task-queue/tests/coverage-branch.test.js +395 -0
  90. package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
  91. package/task-queue/tests/coverage-enhancement.test.js +150 -0
  92. package/task-queue/tests/dag.test.d.ts +1 -0
  93. package/task-queue/tests/dag.test.js +188 -0
  94. package/task-queue/tests/dependencies.test.js +165 -47
  95. package/task-queue/tests/enqueue-batch.test.d.ts +1 -0
  96. package/task-queue/tests/enqueue-batch.test.js +125 -0
  97. package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
  98. package/task-queue/tests/fan-out-spawning.test.js +94 -0
  99. package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
  100. package/task-queue/tests/idempotent-replacement.test.js +114 -0
  101. package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
  102. package/task-queue/tests/missing-idempotent-tasks.test.js +39 -0
  103. package/task-queue/tests/queue.test.js +294 -49
  104. package/task-queue/tests/shutdown.test.d.ts +1 -0
  105. package/task-queue/tests/shutdown.test.js +41 -0
  106. package/task-queue/tests/transactions.test.d.ts +1 -0
  107. package/task-queue/tests/transactions.test.js +47 -0
  108. package/task-queue/tests/worker.test.js +63 -15
  109. package/task-queue/tests/zombie-parent.test.d.ts +1 -0
  110. package/task-queue/tests/zombie-parent.test.js +45 -0
  111. package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
  112. package/task-queue/tests/zombie-recovery.test.js +51 -0
  113. package/test5.js +5 -5
  114. package/testing/integration-setup.d.ts +4 -4
  115. package/testing/integration-setup.js +56 -29
  116. package/text/localization.service.js +2 -2
  117. package/utils/file-reader.js +1 -2
  118. package/utils/timing.d.ts +2 -2
  119. package/task-queue/postgres/drizzle/0000_simple_invisible_woman.sql +0 -74
  120. package/task-queue/tests/complex.test.js +0 -306
  121. package/task-queue/tests/extensive-dependencies.test.js +0 -234
  122. /package/{task-queue/tests/complex.test.d.ts → orm/tests/database-extension.test.d.ts} +0 -0
  123. /package/{task-queue/tests/extensive-dependencies.test.d.ts → orm/tests/database-migration.test.d.ts} +0 -0
@@ -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,11 +1,8 @@
1
- /**
2
- * @module
3
- * Barrel file exporting core server-side ORM functionalities.
4
- * Includes database connection, schema management, repositories, transactions,
5
- * and query conversion utilities.
6
- */
7
1
  export * from './database-schema.js';
8
2
  export * from './database.js';
3
+ export * from './bootstrap.js';
4
+ export * from './extension.js';
5
+ export * from './migration.js';
9
6
  export * from './module.js';
10
7
  export * from './query-converter.js';
11
8
  export * from './repository-config.js';
@@ -1,11 +1,8 @@
1
- /**
2
- * @module
3
- * Barrel file exporting core server-side ORM functionalities.
4
- * Includes database connection, schema management, repositories, transactions,
5
- * and query conversion utilities.
6
- */
7
1
  export * from './database-schema.js';
8
2
  export * from './database.js';
3
+ export * from './bootstrap.js';
4
+ export * from './extension.js';
5
+ export * from './migration.js';
9
6
  export * from './module.js';
10
7
  export * from './query-converter.js';
11
8
  export * from './repository-config.js';
@@ -0,0 +1,18 @@
1
+ import { Injector } from '../../injector/index.js';
2
+ export type DatabaseMigration = {
3
+ name: string;
4
+ migrate: () => void | Promise<void>;
5
+ dependencies?: string[];
6
+ };
7
+ export declare const DATABASE_MIGRATION: import("../../injector/index.js").InjectionToken<DatabaseMigration, never>;
8
+ /**
9
+ * Registers a database migration in the provided injector.
10
+ * @param name - The unique name of the migration.
11
+ * @param migrate - The migration function.
12
+ * @param options - Optional injector and dependencies.
13
+ */
14
+ export declare function registerDatabaseMigration(name: string, migrate: () => void | Promise<void>, { injector, dependencies }?: {
15
+ injector?: Injector;
16
+ dependencies?: string[];
17
+ }): void;
18
+ export declare function runDatabaseMigrationsCore(migrations?: DatabaseMigration[]): Promise<void>;
@@ -0,0 +1,58 @@
1
+ import { injectionToken, Injector } from '../../injector/index.js';
2
+ import { inject, injectAll, runInInjectionContext } from '../../injector/inject.js';
3
+ import { Logger } from '../../logger/index.js';
4
+ import { isDefined } from '../../utils/type-guards.js';
5
+ export const DATABASE_MIGRATION = injectionToken('DatabaseMigration');
6
+ /**
7
+ * Registers a database migration in the provided injector.
8
+ * @param name - The unique name of the migration.
9
+ * @param migrate - The migration function.
10
+ * @param options - Optional injector and dependencies.
11
+ */
12
+ export function registerDatabaseMigration(name, migrate, { injector, dependencies } = {}) {
13
+ const targetInjector = injector ?? Injector;
14
+ targetInjector.register(DATABASE_MIGRATION, { useValue: { name, migrate, dependencies } }, { multi: true });
15
+ }
16
+ export async function runDatabaseMigrationsCore(migrations) {
17
+ const injector = inject(Injector);
18
+ const logger = inject(Logger, 'DatabaseMigrationOrchestrator');
19
+ const actualMigrations = migrations ?? injectAll(DATABASE_MIGRATION, undefined, { optional: true });
20
+ if (actualMigrations.length == 0) {
21
+ return;
22
+ }
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());
27
+ }
28
+ }
29
+ function sortMigrations(migrations) {
30
+ const result = [];
31
+ const visited = new Set();
32
+ const visiting = new Set();
33
+ const migrationMap = new Map(migrations.map((m) => [m.name, m]));
34
+ function visit(name) {
35
+ if (visited.has(name)) {
36
+ return;
37
+ }
38
+ if (visiting.has(name)) {
39
+ throw new Error(`Circular dependency detected in database migrations: ${name}`);
40
+ }
41
+ visiting.add(name);
42
+ const migration = migrationMap.get(name);
43
+ if (isDefined(migration?.dependencies)) {
44
+ for (const dependency of migration.dependencies) {
45
+ visit(dependency);
46
+ }
47
+ }
48
+ visiting.delete(name);
49
+ visited.add(name);
50
+ if (isDefined(migration)) {
51
+ result.push(migration);
52
+ }
53
+ }
54
+ for (const migration of migrations) {
55
+ visit(migration.name);
56
+ }
57
+ return result;
58
+ }
@@ -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';
@@ -18,7 +19,7 @@ type EntityRepositoryContext = {
18
19
  encryptionSecret: Uint8Array<ArrayBuffer> | undefined;
19
20
  transformContext: TransformContext | Promise<TransformContext> | undefined;
20
21
  };
21
- type InferSelect<T extends BaseEntity = BaseEntity> = PgTableFromType<EntityType<T>>['$inferSelect'];
22
+ export type InferSelect<T extends BaseEntity = BaseEntity> = PgTableFromType<EntityType<T>>['$inferSelect'];
22
23
  export declare class EntityRepository<T extends BaseEntity = BaseEntity> extends Transactional<EntityRepositoryContext> implements Resolvable<EntityType<T>> {
23
24
  #private;
24
25
  readonly type: EntityType<T>;
@@ -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);
@@ -26,7 +27,7 @@ import { millisecondsPerSecond } from '../../utils/units.js';
26
27
  import { Entity } from '../entity.js';
27
28
  import { distance, isSimilar, isStrictWordSimilar, isWordSimilar, TRANSACTION_TIMESTAMP, tsHeadline, tsRankCd } from '../sqls/index.js';
28
29
  import { getInheritanceMetadata, isChildEntity } from '../utils.js';
29
- import { getColumnDefinitions, getColumnDefinitionsMap, getDrizzleTableFromType, getTableColumnDefinitions, isTableOwning } from './drizzle/schema-converter.js';
30
+ import { getColumnDefinitions, getColumnDefinitionsMap, getDrizzleTableFromType, getPrimaryKeyColumnDefinitions, getPrimaryKeyColumns, getTableColumnDefinitions, isTableOwning } from './drizzle/schema-converter.js';
30
31
  import { convertQuery, getTsQuery, getTsVector, resolveTargetColumn } from './query-converter.js';
31
32
  import { EntityRepositoryConfig } from './repository-config.js';
32
33
  import { ENCRYPTION_SECRET } from './tokens.js';
@@ -54,6 +55,8 @@ let EntityRepository = class EntityRepository extends Transactional {
54
55
  #baseTableWithMetadata = this.#baseTable;
55
56
  #columnDefinitions = this.#context.columnDefinitions ?? getColumnDefinitions(this.#table);
56
57
  #columnDefinitionsMap = this.#context.columnDefinitionsMap ?? getColumnDefinitionsMap(this.#table);
58
+ #primaryKeyColumns = getPrimaryKeyColumns(this.type, this.#table);
59
+ #primaryKeyColumnNames = this.#primaryKeyColumns.map((column) => column.name);
57
60
  #joinedTables = this.#tablesChain.filter((info) => info.table != this.#table).map((info) => info.table);
58
61
  #inheritanceMetadata = getInheritanceMetadata(this.type);
59
62
  #subclasses = this.#inheritanceMetadata?.subclasses ?? [];
@@ -64,6 +67,7 @@ let EntityRepository = class EntityRepository extends Transactional {
64
67
  type: subclass,
65
68
  table,
66
69
  tablesChain,
70
+ primaryKeyColumns: getPrimaryKeyColumns(subclass, table),
67
71
  columnDefinitions: getColumnDefinitions(table),
68
72
  discriminatorValue: reflectionRegistry.getMetadata(subclass)?.data.tryGet('orm')?.childEntity?.discriminatorValue, // eslint-disable-line @typescript-eslint/no-non-null-asserted-optional-chain
69
73
  };
@@ -84,7 +88,7 @@ let EntityRepository = class EntityRepository extends Transactional {
84
88
  })();
85
89
  #defaultSelection = fromEntries(this.#columnDefinitions.map((column) => [column.name, this.resolveTargetColumn(column)]));
86
90
  #upsertManyExcludedMapping = fromEntries(this.#tableColumnDefinitions
87
- .filter((column) => column.name != this.#table.id.name)
91
+ .filter((column) => !this.#primaryKeyColumnNames.includes(column.name))
88
92
  .map((column) => [column.name, sql `excluded.${sql.identifier(this.getColumn(column).name)}`]));
89
93
  #expirationColumns = this.#columnDefinitions.filter((column) => isDefined(column.reflectionData?.expirationField));
90
94
  #softExpirationColumns = this.#expirationColumns.filter((column) => column.reflectionData.expirationField.mode == 'soft');
@@ -679,7 +683,7 @@ let EntityRepository = class EntityRepository extends Transactional {
679
683
  const transformContext = await this.getTransformContext();
680
684
  const results = {};
681
685
  let generatedId = entity.id;
682
- for (const { table, columnDefinitions } of this.#tablesChain) {
686
+ for (const { table, columnDefinitions, primaryKeyColumns } of this.#tablesChain) {
683
687
  const tableColumnNames = columnDefinitions.map((def) => def.name);
684
688
  const tableColumnNamesSet = new Set(tableColumnNames);
685
689
  const filteredTarget = isDefined(wheres?.target) ? this.filterQuery(wheres.target, tableColumnNamesSet) : undefined;
@@ -695,7 +699,7 @@ let EntityRepository = class EntityRepository extends Transactional {
695
699
  });
696
700
  const tableTarget = isTargetInTable
697
701
  ? targetPaths.map((path) => resolveTargetColumn(path, table, this.#columnDefinitionsMap))
698
- : [table.id];
702
+ : primaryKeyColumns;
699
703
  const [row] = await transaction.pgTransaction
700
704
  .insert(table)
701
705
  .values({ ...columns, ...(isDefined(generatedId) ? { id: generatedId } : {}) })
@@ -758,7 +762,7 @@ let EntityRepository = class EntityRepository extends Transactional {
758
762
  if (generatedIds.length != entities.length) {
759
763
  generatedIds = undefined;
760
764
  }
761
- for (const { table, columnDefinitions } of this.#tablesChain) {
765
+ for (const { table, columnDefinitions, primaryKeyColumns } of this.#tablesChain) {
762
766
  const tableColumnNames = columnDefinitions.map((def) => def.name);
763
767
  const tableColumnNamesSet = new Set(tableColumnNames);
764
768
  const filteredTarget = isDefined(wheres?.target) ? this.filterQuery(wheres.target, tableColumnNamesSet) : undefined;
@@ -772,10 +776,11 @@ let EntityRepository = class EntityRepository extends Transactional {
772
776
  }
773
777
  return columns;
774
778
  }));
779
+ const primaryKeyColumnNames = primaryKeyColumns.map((pk) => pk.name);
775
780
  const mappedUpdate = isDefined(update)
776
781
  ? await this._mapToTableUpdate(update, transformContext, table, columnDefinitions)
777
782
  : {
778
- ...fromEntries(columnDefinitions.filter((column) => column.name != table.id.name).map((column) => [column.name, sql `excluded.${sql.identifier(resolveTargetColumn(column, table, this.#columnDefinitionsMap).name)}`])),
783
+ ...fromEntries(columnDefinitions.filter((column) => !primaryKeyColumnNames.includes(column.name)).map((column) => [column.name, sql `excluded.${sql.identifier(resolveTargetColumn(column, table, this.#columnDefinitionsMap).name)}`])),
779
784
  ...((table == this.#baseTable) ? this._getMetadataUpdate(update) : undefined),
780
785
  };
781
786
  const targetPaths = toArray(target);
@@ -785,7 +790,7 @@ let EntityRepository = class EntityRepository extends Transactional {
785
790
  });
786
791
  const tableTarget = isTargetInTable
787
792
  ? targetPaths.map((path) => resolveTargetColumn(path, table, this.#columnDefinitionsMap))
788
- : [table.id];
793
+ : primaryKeyColumns;
789
794
  const rows = await transaction.pgTransaction
790
795
  .insert(table)
791
796
  .values(values)
@@ -1455,7 +1460,10 @@ let EntityRepository = class EntityRepository extends Transactional {
1455
1460
  return sql `${this.#baseTableWithMetadata.attributes} || ${JSON.stringify(attributes)}::jsonb`;
1456
1461
  }
1457
1462
  applyJoins(dbQuery) {
1458
- return this.#joinedTables.reduce((query, joinedTable) => query.innerJoin(joinedTable, eq(this.#table.id, joinedTable.id)), dbQuery);
1463
+ return this.#tablesChain.filter((info) => info.table != this.#table).reduce((query, info) => {
1464
+ const conditions = info.primaryKeyColumnDefinitions.map((def) => eq(this.#table[def.name], info.table[def.name]));
1465
+ return query.innerJoin(info.table, and(...conditions));
1466
+ }, dbQuery);
1459
1467
  }
1460
1468
  getTablesChain(type) {
1461
1469
  const chain = [];
@@ -1463,7 +1471,9 @@ let EntityRepository = class EntityRepository extends Transactional {
1463
1471
  while (true) {
1464
1472
  const table = getDrizzleTableFromType(currentType, this.#schema);
1465
1473
  const columnDefinitions = getTableColumnDefinitions(table);
1466
- chain.unshift({ table, type: currentType, columnDefinitions });
1474
+ const primaryKeyColumnDefinitions = getPrimaryKeyColumnDefinitions(currentType, table);
1475
+ const primaryKeyColumns = getPrimaryKeyColumns(currentType, table, primaryKeyColumnDefinitions);
1476
+ chain.unshift({ table, type: currentType, columnDefinitions, primaryKeyColumnDefinitions, primaryKeyColumns });
1467
1477
  const parentType = reflectionRegistry.getMetadata(currentType)?.parent;
1468
1478
  if (isNullOrUndefined(parentType) || !isTableOwning(parentType)) {
1469
1479
  break;
@@ -1,15 +1,19 @@
1
+ /** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
1
2
  import { PgTransaction as DrizzlePgTransaction, type PgQueryResultHKT, type PgTransactionConfig } from 'drizzle-orm/pg-core';
2
3
  import type { Record } from '../../types/index.js';
3
4
  import type { Database } from './database.js';
4
5
  export type PgTransaction = DrizzlePgTransaction<PgQueryResultHKT, Record, Record>;
5
6
  export { DrizzlePgTransaction };
6
7
  export type TransactionConfig = PgTransactionConfig;
7
- export declare abstract class Transaction implements AsyncDisposable {
8
+ export declare class Transaction implements AsyncDisposable {
8
9
  #private;
10
+ readonly pgTransaction: PgTransaction;
9
11
  readonly afterCommit: import("../../utils/async-hook/index.js").AsyncHook<never, never, unknown>;
10
12
  readonly parent?: Transaction;
11
13
  manualCommit: boolean;
12
- constructor(parent?: Transaction);
14
+ get isDone(): boolean;
15
+ constructor(pgTransaction: PgTransaction, parent?: Transaction);
16
+ static create(session: Database | PgTransaction, config?: TransactionConfig, parent?: Transaction): Promise<Transaction>;
13
17
  [Symbol.asyncDispose](): Promise<void>;
14
18
  withManualCommit(): void;
15
19
  /**
@@ -19,14 +23,6 @@ export declare abstract class Transaction implements AsyncDisposable {
19
23
  use<T>(handler: () => Promise<T>): Promise<T>;
20
24
  commit(): Promise<void>;
21
25
  rollback(): Promise<void>;
22
- protected abstract _commit(): void | Promise<void>;
23
- protected abstract _rollback(): void | Promise<void>;
24
- }
25
- export declare class DrizzleTransaction extends Transaction {
26
- #private;
27
- readonly pgTransaction: PgTransaction;
28
- constructor(pgTransaction: PgTransaction, parent?: Transaction);
29
- static create(session: Database | PgTransaction, config?: TransactionConfig, parent?: Transaction): Promise<DrizzleTransaction>;
30
26
  protected _commit(): Promise<void>;
31
27
  protected _rollback(): Promise<void>;
32
28
  private setTransactionResultPromise;
@@ -1,16 +1,40 @@
1
+ /** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
1
2
  import { PgTransaction as DrizzlePgTransaction } from 'drizzle-orm/pg-core';
2
3
  import { DeferredPromise } from '../../promise/deferred-promise.js';
3
4
  import { asyncHook } from '../../utils/async-hook/index.js';
4
5
  export { DrizzlePgTransaction };
5
6
  export class Transaction {
7
+ #deferPromise = new DeferredPromise();
8
+ #pgTransactionResultPromise;
6
9
  #useCounter = 0;
7
10
  #done = false;
11
+ pgTransaction;
8
12
  afterCommit = asyncHook();
9
13
  parent;
10
14
  manualCommit = false;
11
- constructor(parent) {
15
+ get isDone() {
16
+ return this.#done;
17
+ }
18
+ constructor(pgTransaction, parent) {
19
+ this.pgTransaction = pgTransaction;
12
20
  this.parent = parent;
13
21
  }
22
+ static async create(session, config, parent) {
23
+ const instancePromise = new DeferredPromise();
24
+ const pgTransactionResultPromise = session.transaction(async (tx) => {
25
+ const transaction = new Transaction(tx, parent);
26
+ instancePromise.resolve(transaction);
27
+ await transaction.#deferPromise;
28
+ }, config);
29
+ pgTransactionResultPromise.catch((error) => {
30
+ if (instancePromise.pending) {
31
+ instancePromise.reject(error);
32
+ }
33
+ });
34
+ const transaction = await instancePromise;
35
+ transaction.setTransactionResultPromise(pgTransactionResultPromise);
36
+ return transaction;
37
+ }
14
38
  async [Symbol.asyncDispose]() {
15
39
  if (!this.#done) {
16
40
  await this.rollback();
@@ -67,31 +91,6 @@ export class Transaction {
67
91
  this.#done = true;
68
92
  await this._rollback();
69
93
  }
70
- }
71
- export class DrizzleTransaction extends Transaction {
72
- #deferPromise = new DeferredPromise();
73
- pgTransaction;
74
- #pgTransactionResultPromise;
75
- constructor(pgTransaction, parent) {
76
- super(parent);
77
- this.pgTransaction = pgTransaction;
78
- }
79
- static async create(session, config, parent) {
80
- const instancePromise = new DeferredPromise();
81
- const pgTransactionResultPromise = session.transaction(async (tx) => {
82
- const transaction = new DrizzleTransaction(tx, parent);
83
- instancePromise.resolve(transaction);
84
- await transaction.#deferPromise;
85
- }, config);
86
- pgTransactionResultPromise.catch((error) => {
87
- if (instancePromise.pending) {
88
- instancePromise.reject(error);
89
- }
90
- });
91
- const transaction = await instancePromise;
92
- transaction.setTransactionResultPromise(pgTransactionResultPromise);
93
- return transaction;
94
- }
95
94
  async _commit() {
96
95
  this.#deferPromise.resolve();
97
96
  await this.#pgTransactionResultPromise;
@@ -4,7 +4,7 @@ import { Injector } from '../../injector/index.js';
4
4
  import { inject, injectAsync, runInInjectionContext } from '../../injector/inject.js';
5
5
  import { isDefined, isNull, isUndefined } from '../../utils/type-guards.js';
6
6
  import { Database } from './database.js';
7
- import { DrizzleTransaction, Transaction } from './transaction.js';
7
+ import { Transaction } from './transaction.js';
8
8
  const transactionCache = new WeakMap();
9
9
  const { getCurrentTransactionalContext, isInTransactionalContext, runInTransactionalContext, tryGetCurrentTransactionalContext } = createContextProvider('Transactional');
10
10
  export { getCurrentTransactionalContext, isInTransactionalContext, runInTransactionalContext, tryGetCurrentTransactionalContext };
@@ -58,7 +58,7 @@ export class Transactional {
58
58
  }
59
59
  }
60
60
  const parentTransaction = tryGetTstdlTransaction(this.session);
61
- const transaction = await DrizzleTransaction.create(this.session, config, parentTransaction);
61
+ const transaction = await Transaction.create(this.session, config, parentTransaction);
62
62
  transactionCache.set(transaction.pgTransaction, transaction);
63
63
  return transaction;
64
64
  }
@@ -77,7 +77,7 @@ export class Transactional {
77
77
  return this;
78
78
  }
79
79
  const context = {
80
- session: session,
80
+ session,
81
81
  instances: this.#instances,
82
82
  data: this.getTransactionalContextData(),
83
83
  };
@@ -0,0 +1,63 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { APPLICATION_INITIALIZER } from '../../application/application.js';
4
+ import { runInInjectionContext } from '../../injector/inject.js';
5
+ import { bootstrapOrm, provideOrm } from '../../orm/server/bootstrap.js';
6
+ import { Database } from '../../orm/server/database.js';
7
+ import { DATABASE_EXTENSION, registerDatabaseExtension, runDatabaseExtensions } from '../../orm/server/extension.js';
8
+ import { DATABASE_MIGRATION } from '../../orm/server/migration.js';
9
+ import { setupIntegrationTest } from '../../testing/index.js';
10
+ describe('Database Extension Registration', () => {
11
+ it('should have DATABASE_EXTENSION token defined', () => {
12
+ expect(DATABASE_EXTENSION).toBeDefined();
13
+ });
14
+ it('should provide ORM bootstrap as application initializer', () => {
15
+ const provider = provideOrm();
16
+ expect(provider.provide).toBe(APPLICATION_INITIALIZER);
17
+ expect(provider.useValue).toBe(bootstrapOrm);
18
+ expect(provider.multi).toBe(true);
19
+ });
20
+ it('should register an extension', async () => {
21
+ const { injector } = await setupIntegrationTest();
22
+ registerDatabaseExtension('btree_gin', { injector });
23
+ const extensions = injector.resolveAll(DATABASE_EXTENSION);
24
+ expect(extensions).toHaveLength(1);
25
+ expect(extensions[0].name).toBe('btree_gin');
26
+ });
27
+ it('should run extensions', async () => {
28
+ const { injector } = await setupIntegrationTest();
29
+ const database = injector.resolve(Database);
30
+ const executeSpy = vi.spyOn(database, 'execute').mockResolvedValue({});
31
+ registerDatabaseExtension('btree_gin', { injector });
32
+ registerDatabaseExtension('uuid-ossp', { injector });
33
+ await runInInjectionContext(injector, async () => {
34
+ await runDatabaseExtensions();
35
+ });
36
+ expect(executeSpy).toHaveBeenCalledWith(sql.raw('CREATE EXTENSION IF NOT EXISTS "btree_gin"'));
37
+ expect(executeSpy).toHaveBeenCalledWith(sql.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'));
38
+ });
39
+ it('should bootstrap ORM (extensions and migrations)', async () => {
40
+ const { injector } = await setupIntegrationTest();
41
+ const database = injector.resolve(Database);
42
+ const executeSpy = vi.spyOn(database, 'execute').mockResolvedValue({});
43
+ registerDatabaseExtension('btree_gin', { injector });
44
+ let migrated = false;
45
+ const migration = {
46
+ name: 'test-migration',
47
+ migrate: async () => {
48
+ migrated = true;
49
+ },
50
+ };
51
+ injector.register(DATABASE_MIGRATION, { useValue: migration }, { multi: true });
52
+ await runInInjectionContext(injector, async () => {
53
+ await bootstrapOrm();
54
+ });
55
+ // Check extension call
56
+ expect(executeSpy).toHaveBeenCalledWith(sql.raw('CREATE EXTENSION IF NOT EXISTS "btree_gin"'));
57
+ // Check migration ran
58
+ expect(migrated).toBe(true);
59
+ // Check locking
60
+ expect(executeSpy).toHaveBeenCalledWith(sql `SELECT pg_advisory_lock(${123456789})`);
61
+ expect(executeSpy).toHaveBeenCalledWith(sql `SELECT pg_advisory_unlock(${123456789})`);
62
+ });
63
+ });
@@ -0,0 +1,83 @@
1
+ import { APPLICATION_INITIALIZER } from '../../application/application.js';
2
+ import { runInInjectionContext } from '../../injector/inject.js';
3
+ import { bootstrapOrm, provideOrm } from '../../orm/server/bootstrap.js';
4
+ import { DATABASE_MIGRATION } from '../../orm/server/migration.js';
5
+ import { setupIntegrationTest } from '../../testing/index.js';
6
+ import { describe, expect, it } from 'vitest';
7
+ describe('Database Migration Orchestration', () => {
8
+ it('should have DATABASE_MIGRATION token defined', () => {
9
+ expect(DATABASE_MIGRATION).toBeDefined();
10
+ });
11
+ it('should provide ORM bootstrap as application initializer', () => {
12
+ const provider = provideOrm();
13
+ expect(provider.provide).toBe(APPLICATION_INITIALIZER);
14
+ expect(provider.useValue).toBe(bootstrapOrm);
15
+ expect(provider.multi).toBe(true);
16
+ });
17
+ it('should register and resolve a migration', async () => {
18
+ const { injector } = await setupIntegrationTest();
19
+ let migrated = false;
20
+ const migration = {
21
+ name: 'test-migration',
22
+ migrate: async () => {
23
+ migrated = true;
24
+ },
25
+ };
26
+ injector.register(DATABASE_MIGRATION, { useValue: migration }, { multi: true });
27
+ const migrations = injector.resolveAll(DATABASE_MIGRATION);
28
+ expect(migrations).toHaveLength(1);
29
+ const firstMigration = migrations[0];
30
+ expect(firstMigration.name).toBe('test-migration');
31
+ await firstMigration.migrate();
32
+ expect(migrated).toBe(true);
33
+ });
34
+ it('should run migrations in the correct order based on dependencies', async () => {
35
+ const { injector } = await setupIntegrationTest();
36
+ const executionOrder = [];
37
+ const migrationA = {
38
+ name: 'migration-a',
39
+ migrate: async () => {
40
+ executionOrder.push('a');
41
+ },
42
+ dependencies: ['migration-b'],
43
+ };
44
+ const migrationB = {
45
+ name: 'migration-b',
46
+ migrate: async () => {
47
+ executionOrder.push('b');
48
+ },
49
+ };
50
+ const migrationC = {
51
+ name: 'migration-c',
52
+ migrate: async () => {
53
+ executionOrder.push('c');
54
+ },
55
+ dependencies: ['migration-a'],
56
+ };
57
+ injector.register(DATABASE_MIGRATION, { useValue: migrationA }, { multi: true });
58
+ injector.register(DATABASE_MIGRATION, { useValue: migrationB }, { multi: true });
59
+ injector.register(DATABASE_MIGRATION, { useValue: migrationC }, { multi: true });
60
+ await runInInjectionContext(injector, async () => {
61
+ await bootstrapOrm();
62
+ });
63
+ expect(executionOrder).toEqual(['b', 'a', 'c']);
64
+ });
65
+ it('should throw on circular dependency', async () => {
66
+ const { injector } = await setupIntegrationTest();
67
+ const migrationA = {
68
+ name: 'migration-a',
69
+ migrate: async () => { },
70
+ dependencies: ['migration-b'],
71
+ };
72
+ const migrationB = {
73
+ name: 'migration-b',
74
+ migrate: async () => { },
75
+ dependencies: ['migration-a'],
76
+ };
77
+ injector.register(DATABASE_MIGRATION, { useValue: migrationA }, { multi: true });
78
+ injector.register(DATABASE_MIGRATION, { useValue: migrationB }, { multi: true });
79
+ await runInInjectionContext(injector, async () => {
80
+ await expect(bootstrapOrm()).rejects.toThrow('Circular dependency detected');
81
+ });
82
+ });
83
+ });
@@ -1,4 +1,3 @@
1
- import { DetailsError } from '../../errors/index.js';
2
1
  import { describe, expect, test } from 'vitest';
3
2
  import { decryptBytes, encryptBytes } from '../server/encryption.js';
4
3
  describe('ORM Encryption', () => {
@@ -12,15 +11,15 @@ describe('ORM Encryption', () => {
12
11
  const decrypted = await decryptBytes(encrypted, key);
13
12
  expect(new TextDecoder().decode(decrypted)).toBe('Hello, ORM Encryption!');
14
13
  });
15
- test('should throw DetailsError on corrupted data', async () => {
14
+ test('should throw Error on corrupted data', async () => {
16
15
  const key = await generateKey();
17
16
  const data = new TextEncoder().encode('Corrupt me');
18
17
  const encrypted = await encryptBytes(data, key);
19
18
  // Corrupt the ciphertext (last byte)
20
19
  const lastIndex = encrypted.length - 1;
21
20
  encrypted[lastIndex] = encrypted[lastIndex] ^ 1;
22
- await expect(decryptBytes(encrypted, key)).rejects.toThrow(DetailsError);
23
- await expect(decryptBytes(encrypted, key)).rejects.toThrow('Decrypt error');
21
+ await expect(decryptBytes(encrypted, key)).rejects.toThrow(Error);
22
+ await expect(decryptBytes(encrypted, key)).rejects.toThrow('Decryption failed.');
24
23
  });
25
24
  test('should throw error on invalid version', async () => {
26
25
  const key = await generateKey();
@@ -0,0 +1,2 @@
1
+ /** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
2
+ export {};