@tstdl/base 0.93.141 → 0.93.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/authentication/client/authentication.service.d.ts +1 -0
- package/authentication/client/authentication.service.js +3 -2
- 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/tests/circuit-breaker.test.js +20 -0
- package/examples/document-management/main.js +2 -2
- package/notification/tests/notification-api.test.js +5 -1
- package/notification/tests/notification-flow.test.js +9 -6
- package/orm/decorators.d.ts +17 -4
- package/orm/decorators.js +9 -0
- 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 +71 -29
- package/orm/server/extension.d.ts +14 -0
- package/orm/server/extension.js +27 -0
- package/orm/server/index.d.ts +2 -0
- package/orm/server/index.js +2 -0
- package/orm/server/migration.d.ts +2 -3
- package/orm/server/migration.js +7 -21
- package/orm/server/repository.d.ts +1 -0
- package/orm/server/repository.js +19 -9
- package/orm/server/transaction.d.ts +1 -0
- package/orm/server/transaction.js +3 -0
- package/orm/tests/database-extension.test.js +63 -0
- package/orm/tests/database-migration.test.js +7 -6
- 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/package.json +4 -4
- package/task-queue/README.md +0 -1
- package/task-queue/postgres/drizzle/0000_faithful_daimon_hellstrom.sql +84 -0
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +155 -72
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
- package/task-queue/postgres/module.js +2 -1
- package/task-queue/postgres/schemas.d.ts +6 -0
- package/task-queue/postgres/task-queue.d.ts +18 -5
- package/task-queue/postgres/task-queue.js +577 -357
- package/task-queue/postgres/task.model.d.ts +10 -6
- package/task-queue/postgres/task.model.js +28 -28
- package/task-queue/task-context.d.ts +10 -5
- package/task-queue/task-context.js +5 -3
- package/task-queue/task-queue.d.ts +342 -38
- package/task-queue/task-queue.js +135 -31
- package/task-queue/tests/coverage-branch.test.js +46 -58
- package/task-queue/tests/coverage-enhancement.test.js +123 -117
- package/task-queue/tests/{extensive-dependencies.test.js → dag.test.js} +66 -37
- package/task-queue/tests/dependencies.test.js +143 -25
- package/task-queue/tests/enqueue-batch.test.js +125 -0
- package/task-queue/tests/fan-out-spawning.test.js +46 -5
- package/task-queue/tests/idempotent-replacement.test.js +54 -1
- package/task-queue/tests/missing-idempotent-tasks.test.js +9 -8
- package/task-queue/tests/queue.test.js +261 -25
- 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 +46 -13
- package/task-queue/tests/zombie-parent.test.js +4 -4
- package/task-queue/tests/zombie-recovery.test.js +3 -3
- package/testing/integration-setup.js +5 -3
- package/utils/timing.d.ts +2 -2
- package/task-queue/postgres/drizzle/0000_wakeful_sunspot.sql +0 -82
- package/task-queue/tests/cascading-cancellations.test.js +0 -38
- package/task-queue/tests/complex.test.js +0 -122
- package/task-queue/tests/dag-dependencies.test.js +0 -41
- /package/{task-queue/tests/cascading-cancellations.test.d.ts → orm/tests/database-extension.test.d.ts} +0 -0
- /package/task-queue/tests/{complex.test.d.ts → dag.test.d.ts} +0 -0
- /package/task-queue/tests/{dag-dependencies.test.d.ts → enqueue-batch.test.d.ts} +0 -0
- /package/task-queue/tests/{extensive-dependencies.test.d.ts → shutdown.test.d.ts} +0 -0
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;
|
|
@@ -11,6 +11,7 @@ export declare class Transaction implements AsyncDisposable {
|
|
|
11
11
|
readonly afterCommit: import("../../utils/async-hook/index.js").AsyncHook<never, never, unknown>;
|
|
12
12
|
readonly parent?: Transaction;
|
|
13
13
|
manualCommit: boolean;
|
|
14
|
+
get isDone(): boolean;
|
|
14
15
|
constructor(pgTransaction: PgTransaction, parent?: Transaction);
|
|
15
16
|
static create(session: Database | PgTransaction, config?: TransactionConfig, parent?: Transaction): Promise<Transaction>;
|
|
16
17
|
[Symbol.asyncDispose](): Promise<void>;
|
|
@@ -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
|
+
});
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { APPLICATION_INITIALIZER } from '../../application/application.js';
|
|
2
2
|
import { runInInjectionContext } from '../../injector/inject.js';
|
|
3
|
-
import {
|
|
3
|
+
import { bootstrapOrm, provideOrm } from '../../orm/server/bootstrap.js';
|
|
4
|
+
import { DATABASE_MIGRATION } from '../../orm/server/migration.js';
|
|
4
5
|
import { setupIntegrationTest } from '../../testing/index.js';
|
|
5
6
|
import { describe, expect, it } from 'vitest';
|
|
6
7
|
describe('Database Migration Orchestration', () => {
|
|
7
8
|
it('should have DATABASE_MIGRATION token defined', () => {
|
|
8
9
|
expect(DATABASE_MIGRATION).toBeDefined();
|
|
9
10
|
});
|
|
10
|
-
it('should provide
|
|
11
|
-
const provider =
|
|
11
|
+
it('should provide ORM bootstrap as application initializer', () => {
|
|
12
|
+
const provider = provideOrm();
|
|
12
13
|
expect(provider.provide).toBe(APPLICATION_INITIALIZER);
|
|
13
|
-
expect(provider.useValue).toBe(
|
|
14
|
+
expect(provider.useValue).toBe(bootstrapOrm);
|
|
14
15
|
expect(provider.multi).toBe(true);
|
|
15
16
|
});
|
|
16
17
|
it('should register and resolve a migration', async () => {
|
|
@@ -57,7 +58,7 @@ describe('Database Migration Orchestration', () => {
|
|
|
57
58
|
injector.register(DATABASE_MIGRATION, { useValue: migrationB }, { multi: true });
|
|
58
59
|
injector.register(DATABASE_MIGRATION, { useValue: migrationC }, { multi: true });
|
|
59
60
|
await runInInjectionContext(injector, async () => {
|
|
60
|
-
await
|
|
61
|
+
await bootstrapOrm();
|
|
61
62
|
});
|
|
62
63
|
expect(executionOrder).toEqual(['b', 'a', 'c']);
|
|
63
64
|
});
|
|
@@ -76,7 +77,7 @@ describe('Database Migration Orchestration', () => {
|
|
|
76
77
|
injector.register(DATABASE_MIGRATION, { useValue: migrationA }, { multi: true });
|
|
77
78
|
injector.register(DATABASE_MIGRATION, { useValue: migrationB }, { multi: true });
|
|
78
79
|
await runInInjectionContext(injector, async () => {
|
|
79
|
-
await expect(
|
|
80
|
+
await expect(bootstrapOrm()).rejects.toThrow('Circular dependency detected');
|
|
80
81
|
});
|
|
81
82
|
});
|
|
82
83
|
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
import { Injector, runInInjectionContext } from '../../injector/index.js';
|
|
12
|
+
import { StringProperty } from '../../schema/index.js';
|
|
13
|
+
import { dropTables, setupIntegrationTest } from '../../testing/index.js';
|
|
14
|
+
import { sql } from 'drizzle-orm';
|
|
15
|
+
import { getTableConfig } from 'drizzle-orm/pg-core';
|
|
16
|
+
import { beforeAll, beforeEach, describe, expect, test } from 'vitest';
|
|
17
|
+
import { ChildEntity, Inheritance, PrimaryKey, Table } from '../decorators.js';
|
|
18
|
+
import { Entity } from '../entity.js';
|
|
19
|
+
import { getDrizzleTableFromType } from '../server/drizzle/index.js';
|
|
20
|
+
import { Database, injectRepository } from '../server/index.js';
|
|
21
|
+
describe('ORM Repository Compound Primary Key (Integration)', () => {
|
|
22
|
+
let injector;
|
|
23
|
+
let database;
|
|
24
|
+
const schema = 'test_orm_compound_pk';
|
|
25
|
+
let CompoundEntity = class CompoundEntity extends Entity {
|
|
26
|
+
namespace;
|
|
27
|
+
data;
|
|
28
|
+
};
|
|
29
|
+
__decorate([
|
|
30
|
+
StringProperty(),
|
|
31
|
+
__metadata("design:type", String)
|
|
32
|
+
], CompoundEntity.prototype, "namespace", void 0);
|
|
33
|
+
__decorate([
|
|
34
|
+
StringProperty(),
|
|
35
|
+
__metadata("design:type", String)
|
|
36
|
+
], CompoundEntity.prototype, "data", void 0);
|
|
37
|
+
CompoundEntity = __decorate([
|
|
38
|
+
Table('compound_entities', { schema }),
|
|
39
|
+
PrimaryKey(['namespace', 'id'])
|
|
40
|
+
], CompoundEntity);
|
|
41
|
+
let ParentEntity = class ParentEntity extends Entity {
|
|
42
|
+
namespace;
|
|
43
|
+
type;
|
|
44
|
+
};
|
|
45
|
+
__decorate([
|
|
46
|
+
StringProperty(),
|
|
47
|
+
__metadata("design:type", String)
|
|
48
|
+
], ParentEntity.prototype, "namespace", void 0);
|
|
49
|
+
__decorate([
|
|
50
|
+
StringProperty(),
|
|
51
|
+
__metadata("design:type", String)
|
|
52
|
+
], ParentEntity.prototype, "type", void 0);
|
|
53
|
+
ParentEntity = __decorate([
|
|
54
|
+
Table('parent_entities', { schema }),
|
|
55
|
+
PrimaryKey(['namespace', 'id']),
|
|
56
|
+
Inheritance({ strategy: 'joined', discriminatorColumn: 'type' })
|
|
57
|
+
], ParentEntity);
|
|
58
|
+
let ChildEntityModel = class ChildEntityModel extends ParentEntity {
|
|
59
|
+
childData;
|
|
60
|
+
};
|
|
61
|
+
__decorate([
|
|
62
|
+
StringProperty(),
|
|
63
|
+
__metadata("design:type", String)
|
|
64
|
+
], ChildEntityModel.prototype, "childData", void 0);
|
|
65
|
+
ChildEntityModel = __decorate([
|
|
66
|
+
Table('child_entities', { schema }),
|
|
67
|
+
ChildEntity('child')
|
|
68
|
+
], ChildEntityModel);
|
|
69
|
+
let GrandchildEntity = class GrandchildEntity extends ChildEntityModel {
|
|
70
|
+
grandchildData;
|
|
71
|
+
};
|
|
72
|
+
__decorate([
|
|
73
|
+
StringProperty(),
|
|
74
|
+
__metadata("design:type", String)
|
|
75
|
+
], GrandchildEntity.prototype, "grandchildData", void 0);
|
|
76
|
+
GrandchildEntity = __decorate([
|
|
77
|
+
Table('grandchild_entities', { schema }),
|
|
78
|
+
ChildEntity('grandchild')
|
|
79
|
+
], GrandchildEntity);
|
|
80
|
+
beforeAll(async () => {
|
|
81
|
+
({ injector, database } = await setupIntegrationTest({ orm: { schema } }));
|
|
82
|
+
await database.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
83
|
+
await dropTables(database, schema, ['grandchild_entities', 'child_entities', 'parent_entities', 'compound_entities']);
|
|
84
|
+
await database.execute(sql `
|
|
85
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('compound_entities')} (
|
|
86
|
+
namespace TEXT NOT NULL,
|
|
87
|
+
id UUID NOT NULL,
|
|
88
|
+
data TEXT NOT NULL,
|
|
89
|
+
revision INTEGER NOT NULL,
|
|
90
|
+
revision_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
91
|
+
create_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
92
|
+
delete_timestamp TIMESTAMP WITH TIME ZONE,
|
|
93
|
+
attributes JSONB NOT NULL DEFAULT '{}',
|
|
94
|
+
PRIMARY KEY (namespace, id)
|
|
95
|
+
)
|
|
96
|
+
`);
|
|
97
|
+
await database.execute(sql `
|
|
98
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('parent_entities')} (
|
|
99
|
+
namespace TEXT NOT NULL,
|
|
100
|
+
id UUID NOT NULL,
|
|
101
|
+
type TEXT NOT NULL,
|
|
102
|
+
revision INTEGER NOT NULL,
|
|
103
|
+
revision_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
104
|
+
create_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
105
|
+
delete_timestamp TIMESTAMP WITH TIME ZONE,
|
|
106
|
+
attributes JSONB NOT NULL DEFAULT '{}',
|
|
107
|
+
PRIMARY KEY (namespace, id),
|
|
108
|
+
UNIQUE (namespace, id, type)
|
|
109
|
+
)
|
|
110
|
+
`);
|
|
111
|
+
await database.execute(sql `
|
|
112
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('child_entities')} (
|
|
113
|
+
namespace TEXT NOT NULL,
|
|
114
|
+
id UUID NOT NULL,
|
|
115
|
+
type TEXT NOT NULL CHECK (type IN ('child', 'grandchild')),
|
|
116
|
+
child_data TEXT NOT NULL,
|
|
117
|
+
PRIMARY KEY (namespace, id),
|
|
118
|
+
FOREIGN KEY (namespace, id, type) REFERENCES ${sql.identifier(schema)}.${sql.identifier('parent_entities')} (namespace, id, type) ON DELETE CASCADE,
|
|
119
|
+
UNIQUE (namespace, id, type)
|
|
120
|
+
)
|
|
121
|
+
`);
|
|
122
|
+
await database.execute(sql `
|
|
123
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('grandchild_entities')} (
|
|
124
|
+
namespace TEXT NOT NULL,
|
|
125
|
+
id UUID NOT NULL,
|
|
126
|
+
type TEXT NOT NULL CHECK (type = 'grandchild'),
|
|
127
|
+
grandchild_data TEXT NOT NULL,
|
|
128
|
+
PRIMARY KEY (namespace, id),
|
|
129
|
+
FOREIGN KEY (namespace, id, type) REFERENCES ${sql.identifier(schema)}.${sql.identifier('child_entities')} (namespace, id, type) ON DELETE CASCADE
|
|
130
|
+
)
|
|
131
|
+
`);
|
|
132
|
+
});
|
|
133
|
+
beforeEach(async () => {
|
|
134
|
+
await database.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('grandchild_entities')}, ${sql.identifier(schema)}.${sql.identifier('child_entities')}, ${sql.identifier(schema)}.${sql.identifier('parent_entities')}, ${sql.identifier(schema)}.${sql.identifier('compound_entities')} CASCADE`);
|
|
135
|
+
});
|
|
136
|
+
test('should insert and load compound entity', async () => {
|
|
137
|
+
await runInInjectionContext(injector, async () => {
|
|
138
|
+
const repository = injectRepository(CompoundEntity);
|
|
139
|
+
const entity = new CompoundEntity();
|
|
140
|
+
entity.id = '00000000-0000-0000-0000-000000000001';
|
|
141
|
+
entity.namespace = 'ns1';
|
|
142
|
+
entity.data = 'test data';
|
|
143
|
+
await repository.insert(entity);
|
|
144
|
+
const loaded = await repository.loadByQuery({ namespace: 'ns1', id: '00000000-0000-0000-0000-000000000001' });
|
|
145
|
+
expect(loaded.data).toBe('test data');
|
|
146
|
+
expect(loaded.namespace).toBe('ns1');
|
|
147
|
+
expect(loaded.id).toBe('00000000-0000-0000-0000-000000000001');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
test('should upsert compound entity', async () => {
|
|
151
|
+
await runInInjectionContext(injector, async () => {
|
|
152
|
+
const repository = injectRepository(CompoundEntity);
|
|
153
|
+
const entity = new CompoundEntity();
|
|
154
|
+
entity.id = '00000000-0000-0000-0000-000000000010';
|
|
155
|
+
entity.namespace = 'ns1';
|
|
156
|
+
entity.data = 'initial data';
|
|
157
|
+
await repository.insert(entity);
|
|
158
|
+
const updateEntity = new CompoundEntity();
|
|
159
|
+
updateEntity.id = '00000000-0000-0000-0000-000000000010';
|
|
160
|
+
updateEntity.namespace = 'ns1';
|
|
161
|
+
updateEntity.data = 'updated data';
|
|
162
|
+
await repository.upsert(['namespace', 'id'], updateEntity);
|
|
163
|
+
const loaded = await repository.loadByQuery({ namespace: 'ns1', id: '00000000-0000-0000-0000-000000000010' });
|
|
164
|
+
expect(loaded.data).toBe('updated data');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
test('should insert and load child entity with compound PK', async () => {
|
|
168
|
+
await runInInjectionContext(injector, async () => {
|
|
169
|
+
const repository = injectRepository(ChildEntityModel);
|
|
170
|
+
const entity = new ChildEntityModel();
|
|
171
|
+
entity.id = '00000000-0000-0000-0000-000000000020';
|
|
172
|
+
entity.namespace = 'ns2';
|
|
173
|
+
entity.type = 'child';
|
|
174
|
+
entity.childData = 'child specific data';
|
|
175
|
+
await repository.insert(entity);
|
|
176
|
+
const loaded = await repository.loadByQuery({ namespace: 'ns2', id: '00000000-0000-0000-0000-000000000020' });
|
|
177
|
+
expect(loaded.childData).toBe('child specific data');
|
|
178
|
+
expect(loaded.namespace).toBe('ns2');
|
|
179
|
+
expect(loaded.id).toBe('00000000-0000-0000-0000-000000000020');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
test('should insert and load grandchild entity with compound PK', async () => {
|
|
183
|
+
await runInInjectionContext(injector, async () => {
|
|
184
|
+
const repository = injectRepository(GrandchildEntity);
|
|
185
|
+
const entity = new GrandchildEntity();
|
|
186
|
+
entity.id = '00000000-0000-0000-0000-000000000050';
|
|
187
|
+
entity.namespace = 'ns5';
|
|
188
|
+
entity.type = 'grandchild';
|
|
189
|
+
entity.childData = 'child part';
|
|
190
|
+
entity.grandchildData = 'grandchild part';
|
|
191
|
+
await repository.insert(entity);
|
|
192
|
+
const loaded = await repository.loadByQuery({ namespace: 'ns5', id: '00000000-0000-0000-0000-000000000050' });
|
|
193
|
+
expect(loaded.grandchildData).toBe('grandchild part');
|
|
194
|
+
expect(loaded.childData).toBe('child part');
|
|
195
|
+
expect(loaded.namespace).toBe('ns5');
|
|
196
|
+
expect(loaded.id).toBe('00000000-0000-0000-0000-000000000050');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
test('should updateManyByQuery with compound PK', async () => {
|
|
200
|
+
await runInInjectionContext(injector, async () => {
|
|
201
|
+
const repository = injectRepository(CompoundEntity);
|
|
202
|
+
const entity1 = new CompoundEntity();
|
|
203
|
+
entity1.id = '00000000-0000-0000-0000-000000000030';
|
|
204
|
+
entity1.namespace = 'ns3';
|
|
205
|
+
entity1.data = 'data 1';
|
|
206
|
+
const entity2 = new CompoundEntity();
|
|
207
|
+
entity2.id = '00000000-0000-0000-0000-000000000040';
|
|
208
|
+
entity2.namespace = 'ns3';
|
|
209
|
+
entity2.data = 'data 2';
|
|
210
|
+
await repository.insertMany([entity1, entity2]);
|
|
211
|
+
await repository.updateManyByQuery({ namespace: 'ns3' }, { data: 'batch updated' });
|
|
212
|
+
const entities = await repository.loadManyByQuery({ namespace: 'ns3' });
|
|
213
|
+
expect(entities).toHaveLength(2);
|
|
214
|
+
expect(entities.every(e => e.data === 'batch updated')).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
test('should respect custom primary key name', () => {
|
|
218
|
+
let CustomPkEntity = class CustomPkEntity extends Entity {
|
|
219
|
+
namespace;
|
|
220
|
+
};
|
|
221
|
+
__decorate([
|
|
222
|
+
StringProperty(),
|
|
223
|
+
__metadata("design:type", String)
|
|
224
|
+
], CustomPkEntity.prototype, "namespace", void 0);
|
|
225
|
+
CustomPkEntity = __decorate([
|
|
226
|
+
Table('custom_pk_table', { schema: 'test' }),
|
|
227
|
+
PrimaryKey(['namespace', 'id'], { name: 'my_custom_pk' })
|
|
228
|
+
], CustomPkEntity);
|
|
229
|
+
const table = getDrizzleTableFromType(CustomPkEntity);
|
|
230
|
+
const config = getTableConfig(table);
|
|
231
|
+
expect(config.primaryKeys).toHaveLength(1);
|
|
232
|
+
expect(config.primaryKeys[0].name).toBe('my_custom_pk');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -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);
|
|
@@ -7,11 +8,11 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
10
|
};
|
|
10
|
-
import { describe, expect, test } from 'vitest';
|
|
11
11
|
import { getTableConfig } from 'drizzle-orm/pg-core';
|
|
12
|
+
import { describe, expect, test } from 'vitest';
|
|
12
13
|
import { StringProperty } from '../../schema/index.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { ChildEntity, Column, Inheritance, ParadeIndex, Table } from '../decorators.js';
|
|
15
|
+
import { Entity, TenantEntity } from '../entity.js';
|
|
15
16
|
import { getDrizzleTableFromType } from '../server/drizzle/schema-converter.js';
|
|
16
17
|
describe('ORM Schema Generation (CTI)', () => {
|
|
17
18
|
test('should generate correct parent table with discriminator and unique constraint', () => {
|
|
@@ -95,6 +96,52 @@ describe('ORM Schema Generation (CTI)', () => {
|
|
|
95
96
|
expect(compositeFk).toBeDefined();
|
|
96
97
|
expect(compositeFk?.reference().columns.map(c => c.name).sort()).toEqual(['id', 'type']);
|
|
97
98
|
});
|
|
99
|
+
test('should generate correct parent and child table for TenantEntity with joined inheritance', () => {
|
|
100
|
+
let BaseTenantUser = class BaseTenantUser extends TenantEntity {
|
|
101
|
+
type;
|
|
102
|
+
};
|
|
103
|
+
__decorate([
|
|
104
|
+
StringProperty(),
|
|
105
|
+
Column({ name: 'type' }),
|
|
106
|
+
__metadata("design:type", String)
|
|
107
|
+
], BaseTenantUser.prototype, "type", void 0);
|
|
108
|
+
BaseTenantUser = __decorate([
|
|
109
|
+
Table('tenant_users', { schema: 'test' }),
|
|
110
|
+
Inheritance({ strategy: 'joined', discriminatorColumn: 'type' })
|
|
111
|
+
], BaseTenantUser);
|
|
112
|
+
let TenantAdmin = class TenantAdmin extends BaseTenantUser {
|
|
113
|
+
role;
|
|
114
|
+
};
|
|
115
|
+
__decorate([
|
|
116
|
+
StringProperty(),
|
|
117
|
+
__metadata("design:type", String)
|
|
118
|
+
], TenantAdmin.prototype, "role", void 0);
|
|
119
|
+
TenantAdmin = __decorate([
|
|
120
|
+
Table('tenant_admins', { schema: 'test' }),
|
|
121
|
+
ChildEntity('admin')
|
|
122
|
+
], TenantAdmin);
|
|
123
|
+
// Test Parent Table
|
|
124
|
+
const parentTable = getDrizzleTableFromType(BaseTenantUser);
|
|
125
|
+
const parentConfig = getTableConfig(parentTable);
|
|
126
|
+
const parentUnique = parentConfig.uniqueConstraints.find((uc) => uc.columns.some((c) => c.name === 'type'));
|
|
127
|
+
expect(parentUnique).toBeDefined();
|
|
128
|
+
// Should be tenant_id, type, id (no duplicates)
|
|
129
|
+
expect(parentUnique.columns.map((c) => c.name)).toEqual(['tenant_id', 'type', 'id']);
|
|
130
|
+
// UNIQUE identifier should use snake_case column names
|
|
131
|
+
expect(parentUnique.name).toBe('tenant_users_tenant_id_type_id_unique');
|
|
132
|
+
// Test Child Table
|
|
133
|
+
const childTable = getDrizzleTableFromType(TenantAdmin);
|
|
134
|
+
const childConfig = getTableConfig(childTable);
|
|
135
|
+
// Child should have tenant_id and id from parent PK, plus type discriminator
|
|
136
|
+
const childColumnNames = childConfig.columns.map((c) => c.name);
|
|
137
|
+
expect(childColumnNames).toContain('tenant_id');
|
|
138
|
+
expect(childColumnNames).toContain('id');
|
|
139
|
+
expect(childColumnNames).toContain('type');
|
|
140
|
+
// Child Foreign Key should link correctly with all 3 columns
|
|
141
|
+
const childFk = childConfig.foreignKeys.find((fk) => fk.reference().columns.length === 3);
|
|
142
|
+
expect(childFk).toBeDefined();
|
|
143
|
+
expect(childFk.reference().columns.map((c) => c.name)).toEqual(['tenant_id', 'type', 'id']);
|
|
144
|
+
});
|
|
98
145
|
test('should generate BM25 index with correct config', () => {
|
|
99
146
|
let SearchableItem = class SearchableItem extends Entity {
|
|
100
147
|
title;
|
|
@@ -112,7 +159,7 @@ describe('ORM Schema Generation (CTI)', () => {
|
|
|
112
159
|
Table('searchable', { schema: 'test' }),
|
|
113
160
|
ParadeIndex({
|
|
114
161
|
columns: ['title', ['body', { tokenizer: 'simple' }]],
|
|
115
|
-
naming: 'abbreviated-table'
|
|
162
|
+
naming: 'abbreviated-table',
|
|
116
163
|
})
|
|
117
164
|
], SearchableItem);
|
|
118
165
|
const table = getDrizzleTableFromType(SearchableItem);
|
|
@@ -122,6 +169,6 @@ describe('ORM Schema Generation (CTI)', () => {
|
|
|
122
169
|
expect(paradeIdx).toBeDefined();
|
|
123
170
|
const extraConfig = paradeIdx.config.with;
|
|
124
171
|
expect(extraConfig).toBeDefined();
|
|
125
|
-
expect(extraConfig.key_field).toBe(
|
|
172
|
+
expect(extraConfig.key_field).toBe(`'id'`);
|
|
126
173
|
});
|
|
127
174
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tstdl/base",
|
|
3
|
-
"version": "0.93.
|
|
3
|
+
"version": "0.93.143",
|
|
4
4
|
"author": "Patrick Hein",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -152,8 +152,8 @@
|
|
|
152
152
|
"type-fest": "^5.4"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
|
-
"@aws-sdk/client-s3": "^3.
|
|
156
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
155
|
+
"@aws-sdk/client-s3": "^3.997",
|
|
156
|
+
"@aws-sdk/s3-request-presigner": "^3.997",
|
|
157
157
|
"@genkit-ai/google-genai": "^1.29",
|
|
158
158
|
"@google-cloud/storage": "^7.19",
|
|
159
159
|
"@toon-format/toon": "^2.1.0",
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"handlebars": "^4.7",
|
|
169
169
|
"mjml": "^4.18",
|
|
170
170
|
"nodemailer": "^8.0",
|
|
171
|
-
"pg": "^8.
|
|
171
|
+
"pg": "^8.19",
|
|
172
172
|
"playwright": "^1.58",
|
|
173
173
|
"preact": "^10.28",
|
|
174
174
|
"preact-render-to-string": "^6.6",
|
package/task-queue/README.md
CHANGED
|
@@ -285,7 +285,6 @@ Passed to the worker handler.
|
|
|
285
285
|
| `maxExecutionTime` | `60m` | Hard limit for `Running` state. |
|
|
286
286
|
| `maxTries` | `3` | Maximum dequeue attempts allowed. |
|
|
287
287
|
| `retention` | `30d` | Duration to retain terminal tasks before archival. |
|
|
288
|
-
| `globalConcurrency` | `null` | Max simultaneous running tasks across all workers. |
|
|
289
288
|
| `circuitBreakerThreshold` | `5` | Failures before tripping the circuit breaker. |
|
|
290
289
|
| `retryDelayMinimum` | `5s` | Floor for exponential backoff delay. |
|
|
291
290
|
| `retryDelayMaximum` | `5m` | Ceiling for exponential backoff delay. |
|