@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.
- package/application/application.d.ts +1 -1
- package/application/application.js +1 -1
- package/application/providers.d.ts +20 -2
- package/application/providers.js +34 -7
- package/audit/module.d.ts +5 -0
- package/audit/module.js +9 -1
- package/authentication/server/module.d.ts +5 -0
- package/authentication/server/module.js +9 -1
- package/authentication/tests/authentication.api-controller.test.js +1 -1
- package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
- package/authentication/tests/authentication.client-service.test.js +1 -1
- package/circuit-breaker/postgres/module.d.ts +1 -0
- package/circuit-breaker/postgres/module.js +5 -1
- package/document-management/server/configure.js +5 -1
- package/document-management/server/module.d.ts +1 -1
- package/document-management/server/module.js +1 -1
- package/document-management/server/services/document-management-ancillary.service.js +1 -1
- package/document-management/tests/ai-config-hierarchy.test.js +0 -5
- package/document-management/tests/document-management-ai-overrides.test.js +0 -1
- package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
- package/examples/document-management/main.d.ts +1 -0
- package/examples/document-management/main.js +14 -11
- package/key-value-store/postgres/module.d.ts +1 -0
- package/key-value-store/postgres/module.js +5 -1
- package/lock/postgres/module.d.ts +1 -0
- package/lock/postgres/module.js +5 -1
- package/mail/module.d.ts +5 -1
- package/mail/module.js +11 -6
- package/module/modules/web-server.module.js +2 -3
- package/notification/server/module.d.ts +1 -0
- package/notification/server/module.js +5 -1
- package/notification/tests/notification-flow.test.js +2 -2
- package/orm/decorators.d.ts +5 -1
- package/orm/decorators.js +1 -1
- package/orm/server/drizzle/schema-converter.js +17 -30
- package/orm/server/encryption.d.ts +0 -1
- package/orm/server/encryption.js +1 -4
- package/orm/server/index.d.ts +1 -6
- package/orm/server/index.js +1 -6
- package/orm/server/migration.d.ts +19 -0
- package/orm/server/migration.js +72 -0
- package/orm/server/repository.d.ts +1 -1
- package/orm/server/transaction.d.ts +5 -10
- package/orm/server/transaction.js +22 -26
- package/orm/server/transactional.js +3 -3
- package/orm/tests/database-migration.test.d.ts +1 -0
- package/orm/tests/database-migration.test.js +82 -0
- package/orm/tests/encryption.test.js +3 -4
- package/orm/utils.d.ts +17 -2
- package/orm/utils.js +49 -1
- package/package.json +4 -3
- package/rate-limit/postgres/module.d.ts +1 -0
- package/rate-limit/postgres/module.js +5 -1
- package/reflection/decorator-data.js +11 -12
- package/task-queue/README.md +2 -9
- package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
- package/task-queue/postgres/module.d.ts +1 -0
- package/task-queue/postgres/module.js +5 -1
- package/task-queue/postgres/schemas.d.ts +9 -6
- package/task-queue/postgres/schemas.js +4 -3
- package/task-queue/postgres/task-queue.d.ts +2 -12
- package/task-queue/postgres/task-queue.js +431 -354
- package/task-queue/postgres/task.model.d.ts +12 -5
- package/task-queue/postgres/task.model.js +51 -25
- package/task-queue/task-context.d.ts +2 -2
- package/task-queue/task-context.js +7 -7
- package/task-queue/task-queue.d.ts +36 -19
- package/task-queue/task-queue.js +18 -10
- package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
- package/task-queue/tests/cascading-cancellations.test.js +38 -0
- package/task-queue/tests/complex.test.js +44 -228
- package/task-queue/tests/coverage-branch.test.d.ts +1 -0
- package/task-queue/tests/coverage-branch.test.js +407 -0
- package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
- package/task-queue/tests/coverage-enhancement.test.js +144 -0
- package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
- package/task-queue/tests/dag-dependencies.test.js +41 -0
- package/task-queue/tests/dependencies.test.js +26 -26
- package/task-queue/tests/extensive-dependencies.test.js +64 -139
- package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
- package/task-queue/tests/fan-out-spawning.test.js +53 -0
- package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
- package/task-queue/tests/idempotent-replacement.test.js +61 -0
- package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
- package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
- package/task-queue/tests/queue.test.js +33 -24
- package/task-queue/tests/worker.test.js +20 -5
- package/task-queue/tests/zombie-parent.test.d.ts +1 -0
- package/task-queue/tests/zombie-parent.test.js +45 -0
- package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
- package/task-queue/tests/zombie-recovery.test.js +51 -0
- package/test5.js +5 -5
- package/testing/integration-setup.d.ts +4 -4
- package/testing/integration-setup.js +54 -29
- package/text/localization.service.js +2 -2
- 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, [
|
|
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
|
});
|
package/orm/decorators.d.ts
CHANGED
|
@@ -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,
|
|
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 =
|
|
46
|
-
const tableName =
|
|
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,
|
|
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,
|
|
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
|
-
|
|
261
|
-
name: foreignKeyData.options?.name ?? getForeignKeyName(tableName,
|
|
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>>;
|
package/orm/server/encryption.js
CHANGED
|
@@ -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
|
-
|
|
53
|
-
throw new DetailsError('Decrypt error', error);
|
|
50
|
+
throw new Error('Decryption failed.', { cause: error });
|
|
54
51
|
}
|
|
55
52
|
}
|
package/orm/server/index.d.ts
CHANGED
|
@@ -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';
|
package/orm/server/index.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
23
|
-
await expect(decryptBytes(encrypted, key)).rejects.toThrow('
|
|
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[];
|