@tstdl/base 0.93.140 → 0.93.141

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 (98) 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/server/module.d.ts +5 -0
  8. package/authentication/server/module.js +9 -1
  9. package/authentication/tests/authentication.api-controller.test.js +1 -1
  10. package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
  11. package/authentication/tests/authentication.client-service.test.js +1 -1
  12. package/circuit-breaker/postgres/module.d.ts +1 -0
  13. package/circuit-breaker/postgres/module.js +5 -1
  14. package/document-management/server/configure.js +5 -1
  15. package/document-management/server/module.d.ts +1 -1
  16. package/document-management/server/module.js +1 -1
  17. package/document-management/server/services/document-management-ancillary.service.js +1 -1
  18. package/document-management/tests/ai-config-hierarchy.test.js +0 -5
  19. package/document-management/tests/document-management-ai-overrides.test.js +0 -1
  20. package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
  21. package/examples/document-management/main.d.ts +1 -0
  22. package/examples/document-management/main.js +14 -11
  23. package/key-value-store/postgres/module.d.ts +1 -0
  24. package/key-value-store/postgres/module.js +5 -1
  25. package/lock/postgres/module.d.ts +1 -0
  26. package/lock/postgres/module.js +5 -1
  27. package/mail/module.d.ts +5 -1
  28. package/mail/module.js +11 -6
  29. package/module/modules/web-server.module.js +2 -3
  30. package/notification/server/module.d.ts +1 -0
  31. package/notification/server/module.js +5 -1
  32. package/notification/tests/notification-flow.test.js +2 -2
  33. package/orm/decorators.d.ts +5 -1
  34. package/orm/decorators.js +1 -1
  35. package/orm/server/drizzle/schema-converter.js +17 -30
  36. package/orm/server/encryption.d.ts +0 -1
  37. package/orm/server/encryption.js +1 -4
  38. package/orm/server/index.d.ts +1 -6
  39. package/orm/server/index.js +1 -6
  40. package/orm/server/migration.d.ts +19 -0
  41. package/orm/server/migration.js +72 -0
  42. package/orm/server/repository.d.ts +1 -1
  43. package/orm/server/transaction.d.ts +5 -10
  44. package/orm/server/transaction.js +22 -26
  45. package/orm/server/transactional.js +3 -3
  46. package/orm/tests/database-migration.test.d.ts +1 -0
  47. package/orm/tests/database-migration.test.js +82 -0
  48. package/orm/tests/encryption.test.js +3 -4
  49. package/orm/utils.d.ts +17 -2
  50. package/orm/utils.js +49 -1
  51. package/package.json +4 -3
  52. package/rate-limit/postgres/module.d.ts +1 -0
  53. package/rate-limit/postgres/module.js +5 -1
  54. package/reflection/decorator-data.js +11 -12
  55. package/task-queue/README.md +2 -9
  56. package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
  57. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
  58. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  59. package/task-queue/postgres/module.d.ts +1 -0
  60. package/task-queue/postgres/module.js +5 -1
  61. package/task-queue/postgres/schemas.d.ts +9 -6
  62. package/task-queue/postgres/schemas.js +4 -3
  63. package/task-queue/postgres/task-queue.d.ts +2 -12
  64. package/task-queue/postgres/task-queue.js +431 -354
  65. package/task-queue/postgres/task.model.d.ts +12 -5
  66. package/task-queue/postgres/task.model.js +51 -25
  67. package/task-queue/task-context.d.ts +2 -2
  68. package/task-queue/task-context.js +7 -7
  69. package/task-queue/task-queue.d.ts +36 -19
  70. package/task-queue/task-queue.js +18 -10
  71. package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
  72. package/task-queue/tests/cascading-cancellations.test.js +38 -0
  73. package/task-queue/tests/complex.test.js +44 -228
  74. package/task-queue/tests/coverage-branch.test.d.ts +1 -0
  75. package/task-queue/tests/coverage-branch.test.js +407 -0
  76. package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
  77. package/task-queue/tests/coverage-enhancement.test.js +144 -0
  78. package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
  79. package/task-queue/tests/dag-dependencies.test.js +41 -0
  80. package/task-queue/tests/dependencies.test.js +26 -26
  81. package/task-queue/tests/extensive-dependencies.test.js +64 -139
  82. package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
  83. package/task-queue/tests/fan-out-spawning.test.js +53 -0
  84. package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
  85. package/task-queue/tests/idempotent-replacement.test.js +61 -0
  86. package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
  87. package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
  88. package/task-queue/tests/queue.test.js +33 -24
  89. package/task-queue/tests/worker.test.js +20 -5
  90. package/task-queue/tests/zombie-parent.test.d.ts +1 -0
  91. package/task-queue/tests/zombie-parent.test.js +45 -0
  92. package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
  93. package/task-queue/tests/zombie-recovery.test.js +51 -0
  94. package/test5.js +5 -5
  95. package/testing/integration-setup.d.ts +4 -4
  96. package/testing/integration-setup.js +54 -29
  97. package/text/localization.service.js +2 -2
  98. package/utils/file-reader.js +1 -2
@@ -10,7 +10,7 @@ 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
12
  import { clearTenantData, setupIntegrationTest, truncateTables } from '../../testing/index.js';
13
- import { InAppNotification, NotificationChannel, NotificationLogEntity, NotificationStatus, WebPushSubscription } from '../models/index.js';
13
+ import { InAppNotification, InAppNotificationArchive, NotificationChannel, NotificationLogEntity, NotificationPreference, NotificationStatus, NotificationType, 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';
@@ -63,7 +63,7 @@ describe('Notification Flow (Integration)', () => {
63
63
  worker.registerProvider(NotificationChannel.InApp, injector.resolve(InAppChannelProvider));
64
64
  });
65
65
  beforeEach(async () => {
66
- await truncateTables(database, schema, ['log', 'in_app', 'in_app_archive', 'type', 'preference', 'web_push_subscription']);
66
+ await truncateTables(database, schema, [NotificationLogEntity, InAppNotification, InAppNotificationArchive, NotificationType, NotificationPreference, WebPushSubscription]);
67
67
  await clearTenantData(database, 'authentication', ['user', 'subject'], tenantId);
68
68
  vi.clearAllMocks();
69
69
  });
@@ -3,7 +3,7 @@
3
3
  * Defines decorators for ORM entities and columns, used to configure database schema mapping.
4
4
  */
5
5
  import type { SQL } from 'drizzle-orm';
6
- import type { ExtraConfigColumn } from 'drizzle-orm/pg-core';
6
+ import type { ExtraConfigColumn, UpdateDeleteAction } from 'drizzle-orm/pg-core';
7
7
  import type { LiteralUnion, SetRequired } from 'type-fest';
8
8
  import { type SpecificCreateDecoratorOptions } from '../reflection/index.js';
9
9
  import type { AbstractConstructor, Record, TypedOmit } from '../types/index.js';
@@ -87,6 +87,8 @@ type ReferenceReflectionData<T extends AnyEntity = AnyEntity> = {
87
87
  target: () => EntityType<T>;
88
88
  targetColumn?: TargetColumnPath<T>;
89
89
  excludeTenant?: boolean;
90
+ onDelete?: UpdateDeleteAction;
91
+ onUpdate?: UpdateDeleteAction;
90
92
  };
91
93
  /**
92
94
  * Reflection data for unique constraints.
@@ -122,6 +124,8 @@ export type ForeignKeyReflectionData = {
122
124
  options?: {
123
125
  name?: string;
124
126
  naming?: NamingStrategy;
127
+ onDelete?: UpdateDeleteAction;
128
+ onUpdate?: UpdateDeleteAction;
125
129
  };
126
130
  };
127
131
  export type IndexOptions<T extends BaseEntity> = {
package/orm/decorators.js CHANGED
@@ -71,7 +71,7 @@ export function PrimaryKeyProperty() {
71
71
  export function Reference(target, targetColumnOrOptions) {
72
72
  const targetColumn = (isString(targetColumnOrOptions) ? targetColumnOrOptions : targetColumnOrOptions?.targetColumn);
73
73
  const options = isString(targetColumnOrOptions) ? undefined : targetColumnOrOptions;
74
- return createColumnDecorator({ references: [{ target, targetColumn, excludeTenant: options?.excludeTenant }] });
74
+ return createColumnDecorator({ references: [{ target, targetColumn, excludeTenant: options?.excludeTenant, onDelete: options?.onDelete, onUpdate: options?.onUpdate }] });
75
75
  }
76
76
  export {
77
77
  /** @deprecated use {@link Reference} instead. */
@@ -15,13 +15,13 @@ 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, assertDefinedPass, isArray, isDefined, isNotNull, isNotNullOrUndefined, isNull, isString, isUndefined } from '../../../utils/type-guards.js';
18
+ import { assertDefined, 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';
22
22
  import { getEnumName } from '../../enums.js';
23
23
  import { JsonSchema, NumericDateSchema, NumericSchema, TimestampSchema, TsVectorSchema, UuidSchema } from '../../schemas/index.js';
24
- import { getInheritanceMetadata, isChildEntity } from '../../utils.js';
24
+ import { getEntitySchema, getEntityTableName, getInheritanceMetadata, getTableReflectionDatas, isChildEntity } from '../../utils.js';
25
25
  import { decryptBytes, encryptBytes } from '../encryption.js';
26
26
  import { convertQuery, resolveTargetColumn, resolveTargetColumns } from '../query-converter.js';
27
27
  const getDbSchema = memoizeSingle(pgSchema);
@@ -42,8 +42,8 @@ export function getColumnDefinitionsMap(table) {
42
42
  export function _getDrizzleTableFromType(type, fallbackSchemaName) {
43
43
  const tableReflectionDatas = getTableReflectionDatas(type);
44
44
  const mergedTableReflectionData = tableReflectionDatas.reduceRight((merged, data) => ({ ...merged, ...data }), {});
45
- const schema = assertDefinedPass(mergedTableReflectionData.schema ?? fallbackSchemaName, 'Table schema not provided');
46
- const tableName = getTableName(type);
45
+ const schema = getEntitySchema(type, fallbackSchemaName);
46
+ const tableName = getEntityTableName(type);
47
47
  const dbSchema = getDbSchema(schema);
48
48
  const inheritanceMetadata = getInheritanceMetadata(type);
49
49
  const allColumnDefinitions = getPostgresColumnEntries(type, dbSchema, tableName, undefined, '', { filterInherited: false }, type);
@@ -130,7 +130,7 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
130
130
  if (isDefined(inheritanceMetadata) && isDefined(childEntityMetadata)) {
131
131
  const hasTenantId = isDefined(table['tenantId']);
132
132
  constraints.push(check(getIdentifier(tableName, discriminatorColumn, 'check'), eq(table[discriminatorColumn], sql.raw(`'${childEntityMetadata.discriminatorValue}'`))), foreignKey({
133
- name: getForeignKeyName(tableName, getTableName(reflectionRegistry.getMetadata(type).parent), [...(hasTenantId ? ['tenantId'] : []), discriminatorColumn, 'id']),
133
+ name: getForeignKeyName(tableName, getEntityTableName(reflectionRegistry.getMetadata(type).parent), [...(hasTenantId ? ['tenantId'] : []), discriminatorColumn, 'id']),
134
134
  columns: [
135
135
  ...(hasTenantId ? [table['tenantId']] : []),
136
136
  table[discriminatorColumn],
@@ -247,7 +247,7 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
247
247
  const foreignTable = getDrizzleTableFromType(tenantReferenceData.target(), dbSchema.schemaName);
248
248
  const nonTenantColumn = tenantReferenceData.targetColumn ?? 'id';
249
249
  return foreignKey({
250
- name: getForeignKeyName(tableName, getTableName(tenantReferenceData.target()), [nonTenantColumn]),
250
+ name: getForeignKeyName(tableName, getEntityTableName(tenantReferenceData.target()), [nonTenantColumn]),
251
251
  columns: [getColumn(table, 'tenantId'), getColumn(table, columnDefinition.name)],
252
252
  foreignColumns: [getColumn(foreignTable, 'tenantId'), getColumn(foreignTable, nonTenantColumn)],
253
253
  });
@@ -257,11 +257,18 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
257
257
  return tableReflectionData.foreignKeys?.map((foreignKeyData) => {
258
258
  const foreignKeyTarget = foreignKeyData.target();
259
259
  const foreignTable = getDrizzleTableFromType(foreignKeyTarget, dbSchema.schemaName);
260
- return foreignKey({
261
- name: foreignKeyData.options?.name ?? getForeignKeyName(tableName, getTableName(foreignKeyData.target()), foreignKeyData.columns, { naming: foreignKeyData.options?.naming }),
260
+ let builder = foreignKey({
261
+ name: foreignKeyData.options?.name ?? getForeignKeyName(tableName, getEntityTableName(foreignKeyData.target()), foreignKeyData.columns, { naming: foreignKeyData.options?.naming }),
262
262
  columns: foreignKeyData.columns.map((column) => getColumn(table, column)),
263
263
  foreignColumns: foreignKeyData.foreignColumns.map((column) => getColumn(foreignTable, column)),
264
264
  });
265
+ if (isDefined(foreignKeyData.options?.onDelete)) {
266
+ builder = builder.onDelete(foreignKeyData.options.onDelete);
267
+ }
268
+ if (isDefined(foreignKeyData.options?.onUpdate)) {
269
+ builder = builder.onUpdate(foreignKeyData.options.onUpdate);
270
+ }
271
+ return builder;
265
272
  }) ?? [];
266
273
  }),
267
274
  ...tableReflectionDatas.flatMap((tableReflectionData) => tableReflectionData.unique).filter(isDefined).map((data) => {
@@ -398,7 +405,7 @@ function getPostgresColumn(tableName, columnName, dbSchema, propertySchema, refl
398
405
  if (((reflectionData.primaryKey == true) || (context.property == 'id' && reflectionData.primaryKey != false)) && (options.skipPrimaryKey != true)) {
399
406
  column = column.primaryKey();
400
407
  }
401
- for (const { target, targetColumn, excludeTenant } of reflectionData.references ?? []) {
408
+ for (const { target, targetColumn, excludeTenant, onDelete, onUpdate } of reflectionData.references ?? []) {
402
409
  column = column.references(() => {
403
410
  const targetType = target();
404
411
  if ((excludeTenant != true) && (typeExtends(context.type, TenantBaseEntity) || typeExtends(context.type, TenantEntity)) && (typeExtends(targetType, TenantBaseEntity) || typeExtends(targetType, TenantEntity))) {
@@ -406,7 +413,7 @@ function getPostgresColumn(tableName, columnName, dbSchema, propertySchema, refl
406
413
  }
407
414
  const targetTable = getDrizzleTableFromType(targetType, dbSchema.schemaName);
408
415
  return targetTable[(targetColumn ?? 'id')];
409
- });
416
+ }, { onDelete, onUpdate });
410
417
  }
411
418
  return column;
412
419
  }
@@ -462,9 +469,6 @@ export function getPgEnum(schema, enumeration, context) {
462
469
  }
463
470
  return dbEnum;
464
471
  }
465
- function getDefaultTableName(type) {
466
- return toSnakeCase(isString(type.entityName) ? type.entityName : type.name.replace(/\d+$/u, ''));
467
- }
468
472
  function getPrimaryKeyName(tableName, columnsOrBaseName, options) {
469
473
  return getIdentifier(tableName, columnsOrBaseName, 'pk', options);
470
474
  }
@@ -486,23 +490,6 @@ function getForeignKeyName(tableName, foreignTableName, columnsOrBaseName, optio
486
490
  }
487
491
  return identifier;
488
492
  }
489
- function getTableName(type) {
490
- const tableReflectionDatas = getTableReflectionDatas(type);
491
- const tableReflectionData = tableReflectionDatas[0];
492
- return tableReflectionData?.name ?? getDefaultTableName(type);
493
- }
494
- function getTableReflectionDatas(type) {
495
- const metadata = reflectionRegistry.getMetadata(type);
496
- assertDefined(metadata, `Type ${type.name} does not have reflection metadata.`);
497
- const tableReflectionDatas = [];
498
- for (let currentMetadata = metadata; isNotNullOrUndefined(currentMetadata?.parent); currentMetadata = reflectionRegistry.getMetadata(currentMetadata.parent)) {
499
- const tableReflectionData = currentMetadata.data.tryGet('orm');
500
- if (isDefined(tableReflectionData)) {
501
- tableReflectionDatas.push(tableReflectionData);
502
- }
503
- }
504
- return tableReflectionDatas;
505
- }
506
493
  function getIdentifier(tableName, columnsOrBaseName, suffix, options) {
507
494
  const middle = isString(columnsOrBaseName) ? columnsOrBaseName : getColumnNames(columnsOrBaseName).join('_');
508
495
  const identifier = `${getTablePrefix(tableName, options?.naming)}_${middle}_${suffix}`;
@@ -12,7 +12,6 @@ export declare function encryptBytes(bytes: Uint8Array<ArrayBuffer>, key: Crypto
12
12
  * @param bytes The byte array to decrypt (must include version and IV).
13
13
  * @param key The CryptoKey to use for decryption.
14
14
  * @returns A promise that resolves to the original decrypted byte array.
15
- * @throws {DetailsError} If decryption fails (e.g., wrong key, corrupted data).
16
15
  * @throws {Error} If the encryption version is invalid.
17
16
  */
18
17
  export declare function decryptBytes(bytes: Uint8Array, key: CryptoKey): Promise<Uint8Array<ArrayBuffer>>;
@@ -3,7 +3,6 @@
3
3
  * Provides utility functions for encrypting and decrypting byte arrays using AES-GCM.
4
4
  * It includes versioning to handle potential future changes in the encryption format.
5
5
  */
6
- import { DetailsError } from '../../errors/index.js';
7
6
  import { decrypt, encrypt } from '../../utils/cryptography.js';
8
7
  import { getRandomBytes } from '../../utils/random.js';
9
8
  import { assert } from '../../utils/type-guards.js';
@@ -36,7 +35,6 @@ export async function encryptBytes(bytes, key) {
36
35
  * @param bytes The byte array to decrypt (must include version and IV).
37
36
  * @param key The CryptoKey to use for decryption.
38
37
  * @returns A promise that resolves to the original decrypted byte array.
39
- * @throws {DetailsError} If decryption fails (e.g., wrong key, corrupted data).
40
38
  * @throws {Error} If the encryption version is invalid.
41
39
  */
42
40
  export async function decryptBytes(bytes, key) {
@@ -49,7 +47,6 @@ export async function decryptBytes(bytes, key) {
49
47
  return new Uint8Array(decrypted);
50
48
  }
51
49
  catch (error) {
52
- // Wrap decryption errors for better context
53
- throw new DetailsError('Decrypt error', error);
50
+ throw new Error('Decryption failed.', { cause: error });
54
51
  }
55
52
  }
@@ -1,11 +1,6 @@
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 './migration.js';
9
4
  export * from './module.js';
10
5
  export * from './query-converter.js';
11
6
  export * from './repository-config.js';
@@ -1,11 +1,6 @@
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 './migration.js';
9
4
  export * from './module.js';
10
5
  export * from './query-converter.js';
11
6
  export * from './repository-config.js';
@@ -0,0 +1,19 @@
1
+ import { Injector, type ProvidersItem } 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 provideDatabaseMigrator(): ProvidersItem;
19
+ export declare function runDatabaseMigrations(): Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { provideInitializer } from '../../application/providers.js';
3
+ import { injectionToken, Injector } from '../../injector/index.js';
4
+ import { inject, injectAll, runInInjectionContext } from '../../injector/inject.js';
5
+ import { Logger } from '../../logger/index.js';
6
+ import { isDefined } from '../../utils/type-guards.js';
7
+ import { Database } from './database.js';
8
+ export const DATABASE_MIGRATION = injectionToken('DatabaseMigration');
9
+ /**
10
+ * Registers a database migration in the provided injector.
11
+ * @param name - The unique name of the migration.
12
+ * @param migrate - The migration function.
13
+ * @param options - Optional injector and dependencies.
14
+ */
15
+ export function registerDatabaseMigration(name, migrate, { injector, dependencies } = {}) {
16
+ const targetInjector = injector ?? Injector;
17
+ targetInjector.register(DATABASE_MIGRATION, { useValue: { name, migrate, dependencies } }, { multi: true });
18
+ }
19
+ export function provideDatabaseMigrator() {
20
+ return provideInitializer(runDatabaseMigrations);
21
+ }
22
+ export async function runDatabaseMigrations() {
23
+ const injector = inject(Injector);
24
+ const database = inject(Database);
25
+ const logger = inject(Logger, 'DatabaseMigrationOrchestrator');
26
+ const migrations = injectAll(DATABASE_MIGRATION, undefined, { optional: true });
27
+ if (migrations.length == 0) {
28
+ return;
29
+ }
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})`);
41
+ }
42
+ }
43
+ function sortMigrations(migrations) {
44
+ const result = [];
45
+ const visited = new Set();
46
+ const visiting = new Set();
47
+ const migrationMap = new Map(migrations.map((m) => [m.name, m]));
48
+ function visit(name) {
49
+ if (visited.has(name)) {
50
+ return;
51
+ }
52
+ if (visiting.has(name)) {
53
+ throw new Error(`Circular dependency detected in database migrations: ${name}`);
54
+ }
55
+ visiting.add(name);
56
+ const migration = migrationMap.get(name);
57
+ if (isDefined(migration?.dependencies)) {
58
+ for (const dependency of migration.dependencies) {
59
+ visit(dependency);
60
+ }
61
+ }
62
+ visiting.delete(name);
63
+ visited.add(name);
64
+ if (isDefined(migration)) {
65
+ result.push(migration);
66
+ }
67
+ }
68
+ for (const migration of migrations) {
69
+ visit(migration.name);
70
+ }
71
+ return result;
72
+ }
@@ -18,7 +18,7 @@ type EntityRepositoryContext = {
18
18
  encryptionSecret: Uint8Array<ArrayBuffer> | undefined;
19
19
  transformContext: TransformContext | Promise<TransformContext> | undefined;
20
20
  };
21
- type InferSelect<T extends BaseEntity = BaseEntity> = PgTableFromType<EntityType<T>>['$inferSelect'];
21
+ export type InferSelect<T extends BaseEntity = BaseEntity> = PgTableFromType<EntityType<T>>['$inferSelect'];
22
22
  export declare class EntityRepository<T extends BaseEntity = BaseEntity> extends Transactional<EntityRepositoryContext> implements Resolvable<EntityType<T>> {
23
23
  #private;
24
24
  readonly type: EntityType<T>;
@@ -1,15 +1,18 @@
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
+ constructor(pgTransaction: PgTransaction, parent?: Transaction);
15
+ static create(session: Database | PgTransaction, config?: TransactionConfig, parent?: Transaction): Promise<Transaction>;
13
16
  [Symbol.asyncDispose](): Promise<void>;
14
17
  withManualCommit(): void;
15
18
  /**
@@ -19,14 +22,6 @@ export declare abstract class Transaction implements AsyncDisposable {
19
22
  use<T>(handler: () => Promise<T>): Promise<T>;
20
23
  commit(): Promise<void>;
21
24
  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
25
  protected _commit(): Promise<void>;
31
26
  protected _rollback(): Promise<void>;
32
27
  private setTransactionResultPromise;
@@ -1,16 +1,37 @@
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
+ constructor(pgTransaction, parent) {
16
+ this.pgTransaction = pgTransaction;
12
17
  this.parent = parent;
13
18
  }
19
+ static async create(session, config, parent) {
20
+ const instancePromise = new DeferredPromise();
21
+ const pgTransactionResultPromise = session.transaction(async (tx) => {
22
+ const transaction = new Transaction(tx, parent);
23
+ instancePromise.resolve(transaction);
24
+ await transaction.#deferPromise;
25
+ }, config);
26
+ pgTransactionResultPromise.catch((error) => {
27
+ if (instancePromise.pending) {
28
+ instancePromise.reject(error);
29
+ }
30
+ });
31
+ const transaction = await instancePromise;
32
+ transaction.setTransactionResultPromise(pgTransactionResultPromise);
33
+ return transaction;
34
+ }
14
35
  async [Symbol.asyncDispose]() {
15
36
  if (!this.#done) {
16
37
  await this.rollback();
@@ -67,31 +88,6 @@ export class Transaction {
67
88
  this.#done = true;
68
89
  await this._rollback();
69
90
  }
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
91
  async _commit() {
96
92
  this.#deferPromise.resolve();
97
93
  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 @@
1
+ export {};
@@ -0,0 +1,82 @@
1
+ import { APPLICATION_INITIALIZER } from '../../application/application.js';
2
+ import { runInInjectionContext } from '../../injector/inject.js';
3
+ import { DATABASE_MIGRATION, provideDatabaseMigrator, runDatabaseMigrations } from '../../orm/server/migration.js';
4
+ import { setupIntegrationTest } from '../../testing/index.js';
5
+ import { describe, expect, it } from 'vitest';
6
+ describe('Database Migration Orchestration', () => {
7
+ it('should have DATABASE_MIGRATION token defined', () => {
8
+ expect(DATABASE_MIGRATION).toBeDefined();
9
+ });
10
+ it('should provide database migrator as application initializer', () => {
11
+ const provider = provideDatabaseMigrator();
12
+ expect(provider.provide).toBe(APPLICATION_INITIALIZER);
13
+ expect(provider.useValue).toBe(runDatabaseMigrations);
14
+ expect(provider.multi).toBe(true);
15
+ });
16
+ it('should register and resolve a migration', async () => {
17
+ const { injector } = await setupIntegrationTest();
18
+ let migrated = false;
19
+ const migration = {
20
+ name: 'test-migration',
21
+ migrate: async () => {
22
+ migrated = true;
23
+ },
24
+ };
25
+ injector.register(DATABASE_MIGRATION, { useValue: migration }, { multi: true });
26
+ const migrations = injector.resolveAll(DATABASE_MIGRATION);
27
+ expect(migrations).toHaveLength(1);
28
+ const firstMigration = migrations[0];
29
+ expect(firstMigration.name).toBe('test-migration');
30
+ await firstMigration.migrate();
31
+ expect(migrated).toBe(true);
32
+ });
33
+ it('should run migrations in the correct order based on dependencies', async () => {
34
+ const { injector } = await setupIntegrationTest();
35
+ const executionOrder = [];
36
+ const migrationA = {
37
+ name: 'migration-a',
38
+ migrate: async () => {
39
+ executionOrder.push('a');
40
+ },
41
+ dependencies: ['migration-b'],
42
+ };
43
+ const migrationB = {
44
+ name: 'migration-b',
45
+ migrate: async () => {
46
+ executionOrder.push('b');
47
+ },
48
+ };
49
+ const migrationC = {
50
+ name: 'migration-c',
51
+ migrate: async () => {
52
+ executionOrder.push('c');
53
+ },
54
+ dependencies: ['migration-a'],
55
+ };
56
+ injector.register(DATABASE_MIGRATION, { useValue: migrationA }, { multi: true });
57
+ injector.register(DATABASE_MIGRATION, { useValue: migrationB }, { multi: true });
58
+ injector.register(DATABASE_MIGRATION, { useValue: migrationC }, { multi: true });
59
+ await runInInjectionContext(injector, async () => {
60
+ await runDatabaseMigrations();
61
+ });
62
+ expect(executionOrder).toEqual(['b', 'a', 'c']);
63
+ });
64
+ it('should throw on circular dependency', async () => {
65
+ const { injector } = await setupIntegrationTest();
66
+ const migrationA = {
67
+ name: 'migration-a',
68
+ migrate: async () => { },
69
+ dependencies: ['migration-b'],
70
+ };
71
+ const migrationB = {
72
+ name: 'migration-b',
73
+ migrate: async () => { },
74
+ dependencies: ['migration-a'],
75
+ };
76
+ injector.register(DATABASE_MIGRATION, { useValue: migrationA }, { multi: true });
77
+ injector.register(DATABASE_MIGRATION, { useValue: migrationB }, { multi: true });
78
+ await runInInjectionContext(injector, async () => {
79
+ await expect(runDatabaseMigrations()).rejects.toThrow('Circular dependency detected');
80
+ });
81
+ });
82
+ });
@@ -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();
package/orm/utils.d.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  */
5
5
  import { type PropertyMetadata } from '../reflection/registry.js';
6
6
  import type { AbstractConstructor } from '../types/index.js';
7
- import type { InheritanceMetadata } from './decorators.js';
8
- import type { BaseEntity, Entity } from './entity.js';
7
+ import type { InheritanceMetadata, OrmTableReflectionData } from './decorators.js';
8
+ import type { BaseEntity, Entity, EntityType } from './entity.js';
9
9
  /**
10
10
  * Converts an array of entities into a Map keyed by entity ID.
11
11
  * @template T - The entity type, must extend `Entity` (i.e., have an `id` property).
@@ -24,3 +24,18 @@ export declare function getEntityIds(entities: (Entity | BaseEntity)[]): string[
24
24
  export declare function isChildEntity(type: AbstractConstructor): boolean;
25
25
  export declare function getInheritanceMetadata(type: AbstractConstructor): InheritanceMetadata | undefined;
26
26
  export declare function getOwnProperties(type: AbstractConstructor): PropertyMetadata[];
27
+ /**
28
+ * Gets the database schema name for a given entity type, traversing the inheritance hierarchy.
29
+ * @param type The entity class.
30
+ * @param fallback Optional fallback schema name if none is defined in metadata.
31
+ * @returns The resolved schema name.
32
+ */
33
+ export declare function getEntitySchema(type: AbstractConstructor, fallback?: string): string;
34
+ /**
35
+ * Gets the database table name for a given entity type, traversing the inheritance hierarchy.
36
+ * @param type The entity class.
37
+ * @returns The resolved table name.
38
+ */
39
+ export declare function getEntityTableName(type: AbstractConstructor): string;
40
+ export declare function getDefaultTableName(type: EntityType): string;
41
+ export declare function getTableReflectionDatas(type: EntityType): OrmTableReflectionData[];