@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.
- 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/client/authentication.service.d.ts +1 -0
- package/authentication/client/authentication.service.js +3 -2
- 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/circuit-breaker.d.ts +6 -4
- package/circuit-breaker/postgres/circuit-breaker.d.ts +1 -0
- package/circuit-breaker/postgres/circuit-breaker.js +8 -5
- package/circuit-breaker/postgres/module.d.ts +1 -0
- package/circuit-breaker/postgres/module.js +5 -1
- package/circuit-breaker/tests/circuit-breaker.test.js +20 -0
- 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-api.test.js +5 -1
- package/notification/tests/notification-flow.test.js +8 -5
- package/orm/decorators.d.ts +22 -5
- package/orm/decorators.js +10 -1
- package/orm/server/bootstrap.d.ts +11 -0
- package/orm/server/bootstrap.js +31 -0
- package/orm/server/drizzle/schema-converter.d.ts +3 -1
- package/orm/server/drizzle/schema-converter.js +85 -56
- package/orm/server/encryption.d.ts +0 -1
- package/orm/server/encryption.js +1 -4
- package/orm/server/extension.d.ts +14 -0
- package/orm/server/extension.js +27 -0
- package/orm/server/index.d.ts +3 -6
- package/orm/server/index.js +3 -6
- package/orm/server/migration.d.ts +18 -0
- package/orm/server/migration.js +58 -0
- package/orm/server/repository.d.ts +2 -1
- package/orm/server/repository.js +19 -9
- package/orm/server/transaction.d.ts +6 -10
- package/orm/server/transaction.js +25 -26
- package/orm/server/transactional.js +3 -3
- package/orm/tests/database-extension.test.js +63 -0
- package/orm/tests/database-migration.test.js +83 -0
- package/orm/tests/encryption.test.js +3 -4
- package/orm/tests/repository-compound-primary-key.test.d.ts +2 -0
- package/orm/tests/repository-compound-primary-key.test.js +234 -0
- package/orm/tests/schema-generation.test.d.ts +1 -0
- package/orm/tests/schema-generation.test.js +52 -5
- package/orm/utils.d.ts +17 -2
- package/orm/utils.js +49 -1
- package/package.json +5 -4
- 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 -10
- package/task-queue/postgres/drizzle/0000_great_gwen_stacy.sql +84 -0
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +250 -89
- 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 +6 -1
- package/task-queue/postgres/schemas.d.ts +15 -6
- package/task-queue/postgres/schemas.js +4 -3
- package/task-queue/postgres/task-queue.d.ts +18 -15
- package/task-queue/postgres/task-queue.js +797 -499
- package/task-queue/postgres/task.model.d.ts +20 -9
- package/task-queue/postgres/task.model.js +65 -39
- package/task-queue/task-context.d.ts +12 -7
- package/task-queue/task-context.js +8 -6
- package/task-queue/task-queue.d.ts +364 -43
- package/task-queue/task-queue.js +153 -41
- package/task-queue/tests/coverage-branch.test.d.ts +1 -0
- package/task-queue/tests/coverage-branch.test.js +395 -0
- package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
- package/task-queue/tests/coverage-enhancement.test.js +150 -0
- package/task-queue/tests/dag.test.d.ts +1 -0
- package/task-queue/tests/dag.test.js +188 -0
- package/task-queue/tests/dependencies.test.js +165 -47
- package/task-queue/tests/enqueue-batch.test.d.ts +1 -0
- package/task-queue/tests/enqueue-batch.test.js +125 -0
- package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
- package/task-queue/tests/fan-out-spawning.test.js +94 -0
- package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
- package/task-queue/tests/idempotent-replacement.test.js +114 -0
- package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
- package/task-queue/tests/missing-idempotent-tasks.test.js +39 -0
- package/task-queue/tests/queue.test.js +294 -49
- package/task-queue/tests/shutdown.test.d.ts +1 -0
- package/task-queue/tests/shutdown.test.js +41 -0
- package/task-queue/tests/transactions.test.d.ts +1 -0
- package/task-queue/tests/transactions.test.js +47 -0
- package/task-queue/tests/worker.test.js +63 -15
- 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 +56 -29
- package/text/localization.service.js +2 -2
- package/utils/file-reader.js +1 -2
- package/utils/timing.d.ts +2 -2
- package/task-queue/postgres/drizzle/0000_simple_invisible_woman.sql +0 -74
- package/task-queue/tests/complex.test.js +0 -306
- package/task-queue/tests/extensive-dependencies.test.js +0 -234
- /package/{task-queue/tests/complex.test.d.ts → orm/tests/database-extension.test.d.ts} +0 -0
- /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
|
+
}
|
package/orm/server/index.d.ts
CHANGED
|
@@ -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';
|
package/orm/server/index.js
CHANGED
|
@@ -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>;
|
package/orm/server/repository.js
CHANGED
|
@@ -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) =>
|
|
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
|
-
:
|
|
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
|
|
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
|
-
:
|
|
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.#
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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,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
|
|
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();
|