@tstdl/base 0.93.141 → 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/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_great_gwen_stacy.sql +84 -0
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +151 -68
- 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 +593 -372
- package/task-queue/postgres/task.model.d.ts +9 -5
- package/task-queue/postgres/task.model.js +26 -26
- package/task-queue/task-context.d.ts +10 -5
- package/task-queue/task-context.js +5 -3
- package/task-queue/task-queue.d.ts +339 -35
- package/task-queue/task-queue.js +135 -31
- package/task-queue/tests/coverage-branch.test.js +45 -57
- package/task-queue/tests/coverage-enhancement.test.js +123 -117
- package/task-queue/tests/{extensive-dependencies.test.js → dag.test.js} +61 -32
- package/task-queue/tests/dependencies.test.js +139 -21
- package/task-queue/tests/enqueue-batch.test.js +125 -0
- package/task-queue/tests/fan-out-spawning.test.js +43 -2
- 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 +1 -1
- 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
|
@@ -27,6 +27,7 @@ export declare class AuthenticationClientService<AdditionalTokenPayload extends
|
|
|
27
27
|
private readonly logger;
|
|
28
28
|
private readonly disposeSignal;
|
|
29
29
|
private readonly forceRefreshRequested;
|
|
30
|
+
private readonly forceRefreshRequested$;
|
|
30
31
|
private clockOffset;
|
|
31
32
|
private initialized;
|
|
32
33
|
private refreshLoopPromise;
|
|
@@ -70,6 +70,7 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
70
70
|
logger = inject(Logger, 'AuthenticationService');
|
|
71
71
|
disposeSignal = inject(CancellationSignal).fork();
|
|
72
72
|
forceRefreshRequested = signal(false);
|
|
73
|
+
forceRefreshRequested$ = toObservable(this.forceRefreshRequested);
|
|
73
74
|
clockOffset = 0;
|
|
74
75
|
initialized = false;
|
|
75
76
|
refreshLoopPromise;
|
|
@@ -435,7 +436,7 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
435
436
|
this.token$.pipe(filter((t) => t != currentToken)),
|
|
436
437
|
];
|
|
437
438
|
if (!forceRefresh) {
|
|
438
|
-
wakeUpSignals.push(
|
|
439
|
+
wakeUpSignals.push(this.forceRefreshRequested$.pipe(filter((requested) => requested)));
|
|
439
440
|
}
|
|
440
441
|
if (delay > 0) {
|
|
441
442
|
await firstValueFrom(race([timer(delay), ...wakeUpSignals]), { defaultValue: undefined });
|
|
@@ -451,7 +452,7 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
451
452
|
timer(2500),
|
|
452
453
|
from(this.disposeSignal),
|
|
453
454
|
this.token$.pipe(filter((t) => t != currentToken)),
|
|
454
|
-
|
|
455
|
+
this.forceRefreshRequested$.pipe(filter((requested) => requested), skip(this.forceRefreshRequested() ? 1 : 0)),
|
|
455
456
|
]), { defaultValue: undefined });
|
|
456
457
|
}
|
|
457
458
|
}
|
|
@@ -6,15 +6,15 @@ export declare const CircuitBreakerState: {
|
|
|
6
6
|
readonly HalfOpen: "half-open";
|
|
7
7
|
};
|
|
8
8
|
export type CircuitBreakerState = EnumType<typeof CircuitBreakerState>;
|
|
9
|
-
export
|
|
9
|
+
export type CircuitBreakerConfig = {
|
|
10
10
|
threshold: number;
|
|
11
11
|
resetTimeout: number;
|
|
12
|
-
}
|
|
13
|
-
export
|
|
12
|
+
};
|
|
13
|
+
export type CircuitBreakerCheckResult = {
|
|
14
14
|
allowed: boolean;
|
|
15
15
|
state: CircuitBreakerState;
|
|
16
16
|
isProbe?: boolean;
|
|
17
|
-
}
|
|
17
|
+
};
|
|
18
18
|
export type CircuitBreakerArgument = string | (CircuitBreakerConfig & {
|
|
19
19
|
key: string;
|
|
20
20
|
});
|
|
@@ -29,4 +29,6 @@ export declare abstract class CircuitBreaker implements Resolvable<CircuitBreake
|
|
|
29
29
|
abstract recordSuccess(): Promise<void>;
|
|
30
30
|
/** Records a failure, potentially tripping the breaker to Open. */
|
|
31
31
|
abstract recordFailure(): Promise<void>;
|
|
32
|
+
/** Records multiple failures at once. */
|
|
33
|
+
abstract recordFailures(count: number): Promise<void>;
|
|
32
34
|
}
|
|
@@ -43,8 +43,11 @@ let PostgresCircuitBreakerService = class PostgresCircuitBreakerService extends
|
|
|
43
43
|
await this.#repository.tryDeleteByQuery({ key: this.#key });
|
|
44
44
|
}
|
|
45
45
|
async recordFailure() {
|
|
46
|
+
await this.recordFailures(1);
|
|
47
|
+
}
|
|
48
|
+
async recordFailures(count) {
|
|
46
49
|
const table = this.#repository.table;
|
|
47
|
-
const initialTrip =
|
|
50
|
+
const initialTrip = count >= this.#threshold;
|
|
48
51
|
const initialState = initialTrip ? CircuitBreakerState.Open : CircuitBreakerState.Closed;
|
|
49
52
|
const initialResetTimestamp = initialTrip
|
|
50
53
|
? sql `${TRANSACTION_TIMESTAMP} + ${interval(this.#resetTimeout, 'milliseconds')}`
|
|
@@ -52,16 +55,16 @@ let PostgresCircuitBreakerService = class PostgresCircuitBreakerService extends
|
|
|
52
55
|
await this.#repository.upsert(['key'], {
|
|
53
56
|
key: this.#key,
|
|
54
57
|
state: initialState,
|
|
55
|
-
failureCount:
|
|
58
|
+
failureCount: count,
|
|
56
59
|
resetTimestamp: initialResetTimestamp,
|
|
57
60
|
}, {
|
|
58
|
-
failureCount: sql `${table.failureCount} +
|
|
61
|
+
failureCount: sql `${table.failureCount} + ${count}`,
|
|
59
62
|
state: sql `CASE
|
|
60
|
-
WHEN ${table.failureCount} +
|
|
63
|
+
WHEN ${table.failureCount} + ${count} >= ${this.#threshold} THEN ${CircuitBreakerState.Open}
|
|
61
64
|
ELSE ${table.state}
|
|
62
65
|
END`,
|
|
63
66
|
resetTimestamp: sql `CASE
|
|
64
|
-
WHEN ${table.failureCount} +
|
|
67
|
+
WHEN ${table.failureCount} + ${count} >= ${this.#threshold} THEN ${TRANSACTION_TIMESTAMP} + ${interval(this.#resetTimeout, 'milliseconds')}
|
|
65
68
|
ELSE ${table.resetTimestamp}
|
|
66
69
|
END`,
|
|
67
70
|
});
|
|
@@ -90,4 +90,24 @@ describe('Circuit Breaker (Standalone) Tests', () => {
|
|
|
90
90
|
expect(result.allowed).toBe(false);
|
|
91
91
|
expect(result.state).toBe(CircuitBreakerState.Open);
|
|
92
92
|
});
|
|
93
|
+
it('should trip to Open after threshold failures via recordFailures', async () => {
|
|
94
|
+
const breaker = provider.provide(`cb-bulk-trip-${Date.now()}`, { threshold: 5, resetTimeout: 1000 });
|
|
95
|
+
// 1. Bulk Failure (Threshold Not Reached)
|
|
96
|
+
await breaker.recordFailures(3);
|
|
97
|
+
let result = await breaker.check();
|
|
98
|
+
expect(result.state).toBe(CircuitBreakerState.Closed);
|
|
99
|
+
// 2. Bulk Failure (Threshold Reached)
|
|
100
|
+
await breaker.recordFailures(2);
|
|
101
|
+
result = await breaker.check();
|
|
102
|
+
expect(result.allowed).toBe(false);
|
|
103
|
+
expect(result.state).toBe(CircuitBreakerState.Open);
|
|
104
|
+
});
|
|
105
|
+
it('should trip to Open after threshold failures via single large recordFailures', async () => {
|
|
106
|
+
const breaker = provider.provide(`cb-single-bulk-trip-${Date.now()}`, { threshold: 5, resetTimeout: 1000 });
|
|
107
|
+
// Single large bulk failure exceeding threshold
|
|
108
|
+
await breaker.recordFailures(10);
|
|
109
|
+
const result = await breaker.check();
|
|
110
|
+
expect(result.allowed).toBe(false);
|
|
111
|
+
expect(result.state).toBe(CircuitBreakerState.Open);
|
|
112
|
+
});
|
|
93
113
|
});
|
|
@@ -24,7 +24,7 @@ import { PrettyPrintLogFormatter, provideConsoleLogTransport } from '../../logge
|
|
|
24
24
|
import { configureLocalMessageBus } from '../../message-bus/index.js';
|
|
25
25
|
import { WebServerModule } from '../../module/index.js';
|
|
26
26
|
import { configureS3ObjectStorage } from '../../object-storage/s3/index.js';
|
|
27
|
-
import { configureOrm,
|
|
27
|
+
import { configureOrm, provideOrm } from '../../orm/server/index.js';
|
|
28
28
|
import { configurePostgresRateLimiter } from '../../rate-limit/postgres/module.js';
|
|
29
29
|
import { configureDefaultSignalsImplementation } from '../../signals/implementation/configure.js';
|
|
30
30
|
import { configurePostgresTaskQueue } from '../../task-queue/postgres/index.js';
|
|
@@ -173,5 +173,5 @@ Application.run('DocumentManagementTest', [
|
|
|
173
173
|
provideModule(main, WebServerModule),
|
|
174
174
|
provideSignalHandler(),
|
|
175
175
|
provideConsoleLogTransport(PrettyPrintLogFormatter),
|
|
176
|
-
|
|
176
|
+
provideOrm(),
|
|
177
177
|
]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Subject } from 'rxjs';
|
|
2
|
-
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
3
3
|
import { SubjectService } from '../../authentication/server/subject.service.js';
|
|
4
4
|
import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
|
|
5
5
|
import { NotificationChannel } from '../models/index.js';
|
|
@@ -44,6 +44,10 @@ describe('Notification API (Integration)', () => {
|
|
|
44
44
|
beforeEach(() => {
|
|
45
45
|
vi.clearAllMocks();
|
|
46
46
|
});
|
|
47
|
+
afterEach(async () => {
|
|
48
|
+
await clearTenantData(database, schema, ['in_app', 'in_app_archive', 'log', 'preference', 'web_push_subscription'], tenantId);
|
|
49
|
+
await clearTenantData(database, 'authentication', ['user', 'subject'], tenantId);
|
|
50
|
+
});
|
|
47
51
|
const createMockContext = (params = {}) => ({
|
|
48
52
|
parameters: params,
|
|
49
53
|
abortSignal: new AbortController().signal,
|
|
@@ -4,13 +4,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
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;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
7
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
8
8
|
import { SubjectService } from '../../authentication/server/subject.service.js';
|
|
9
9
|
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
|
-
import { clearTenantData, setupIntegrationTest
|
|
13
|
-
import { InAppNotification,
|
|
12
|
+
import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
|
|
13
|
+
import { InAppNotification, NotificationChannel, NotificationLogEntity, NotificationStatus, 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';
|
|
@@ -34,7 +34,7 @@ describe('Notification Flow (Integration)', () => {
|
|
|
34
34
|
let subjectService;
|
|
35
35
|
let mailServiceMock;
|
|
36
36
|
const schema = 'notification';
|
|
37
|
-
|
|
37
|
+
let tenantId;
|
|
38
38
|
beforeAll(async () => {
|
|
39
39
|
({ injector, database } = await setupIntegrationTest({
|
|
40
40
|
orm: { schema },
|
|
@@ -63,10 +63,13 @@ describe('Notification Flow (Integration)', () => {
|
|
|
63
63
|
worker.registerProvider(NotificationChannel.InApp, injector.resolve(InAppChannelProvider));
|
|
64
64
|
});
|
|
65
65
|
beforeEach(async () => {
|
|
66
|
-
|
|
67
|
-
await clearTenantData(database, 'authentication', ['user', 'subject'], tenantId);
|
|
66
|
+
tenantId = crypto.randomUUID();
|
|
68
67
|
vi.clearAllMocks();
|
|
69
68
|
});
|
|
69
|
+
afterEach(async () => {
|
|
70
|
+
await clearTenantData(database, schema, ['in_app', 'in_app_archive', 'log', 'preference', 'web_push_subscription'], tenantId);
|
|
71
|
+
await clearTenantData(database, 'authentication', ['user', 'subject'], tenantId);
|
|
72
|
+
});
|
|
70
73
|
test('should execute full notification flow with escalation', async () => {
|
|
71
74
|
await runInInjectionContext(injector, async () => {
|
|
72
75
|
const logRepo = injectRepository(NotificationLogEntity);
|
package/orm/decorators.d.ts
CHANGED
|
@@ -40,8 +40,7 @@ export type WhereBuilder<T extends BaseEntity = BaseEntity> = SqlBuilder<T, Quer
|
|
|
40
40
|
export type OrmTableReflectionData<T extends BaseEntity = BaseEntity> = {
|
|
41
41
|
name?: string;
|
|
42
42
|
schema?: string;
|
|
43
|
-
|
|
44
|
-
compoundPrimaryKeyNaming?: NamingStrategy;
|
|
43
|
+
primaryKey?: PrimaryKeyReflectionData<T>;
|
|
45
44
|
unique?: UniqueReflectionData[];
|
|
46
45
|
index?: IndexReflectionData[];
|
|
47
46
|
paradeIndex?: ParadeIndexReflectionData<T>;
|
|
@@ -90,11 +89,18 @@ type ReferenceReflectionData<T extends AnyEntity = AnyEntity> = {
|
|
|
90
89
|
onDelete?: UpdateDeleteAction;
|
|
91
90
|
onUpdate?: UpdateDeleteAction;
|
|
92
91
|
};
|
|
92
|
+
export type PrimaryKeyReflectionData<T extends BaseEntity = any> = {
|
|
93
|
+
columns?: ValueOrProvider<(TargetColumnPath<T> | SQL | ExtraConfigColumn)[], ExtraConfigColumnsFromType<EntityType<T>>>;
|
|
94
|
+
options?: {
|
|
95
|
+
name?: string;
|
|
96
|
+
naming?: NamingStrategy;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
93
99
|
/**
|
|
94
100
|
* Reflection data for unique constraints.
|
|
95
101
|
*/
|
|
96
|
-
export type UniqueReflectionData = {
|
|
97
|
-
columns?:
|
|
102
|
+
export type UniqueReflectionData<T extends BaseEntity = any> = {
|
|
103
|
+
columns?: ValueOrProvider<(TargetColumnPath<T> | SQL | ExtraConfigColumn)[], ExtraConfigColumnsFromType<EntityType<T>>>;
|
|
98
104
|
options?: {
|
|
99
105
|
name?: string;
|
|
100
106
|
naming?: NamingStrategy;
|
|
@@ -294,6 +300,13 @@ export declare function Table(name?: string, options?: TableOptions): ClassDecor
|
|
|
294
300
|
* @param options Table options including name and schema.
|
|
295
301
|
*/
|
|
296
302
|
export declare function Table(options?: TableOptions): ClassDecorator;
|
|
303
|
+
/**
|
|
304
|
+
* Define a composite primary key on multiple columns.
|
|
305
|
+
* @template T The entity type.
|
|
306
|
+
* @param columns An array of property names included in the primary key.
|
|
307
|
+
* @param options Additional primary key options.
|
|
308
|
+
*/
|
|
309
|
+
export declare function PrimaryKey<T extends AnyEntity>(columns: Columns<T>, options?: PrimaryKeyReflectionData['options']): ClassDecorator;
|
|
297
310
|
/**
|
|
298
311
|
* Define a foreign key relationship.
|
|
299
312
|
* @param target A function returning the referenced entity type.
|
package/orm/decorators.js
CHANGED
|
@@ -113,6 +113,15 @@ export function Table(nameOrOptions, optionsOrNothing) {
|
|
|
113
113
|
: filterUndefinedObjectProperties({ name, schema });
|
|
114
114
|
return createTableDecorator(data);
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Define a composite primary key on multiple columns.
|
|
118
|
+
* @template T The entity type.
|
|
119
|
+
* @param columns An array of property names included in the primary key.
|
|
120
|
+
* @param options Additional primary key options.
|
|
121
|
+
*/
|
|
122
|
+
export function PrimaryKey(columns, options) {
|
|
123
|
+
return createTableDecorator({ primaryKey: { columns, options } });
|
|
124
|
+
}
|
|
116
125
|
/**
|
|
117
126
|
* Define a foreign key relationship.
|
|
118
127
|
* @param target A function returning the referenced entity type.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ProvidersItem } from '../../injector/injector.js';
|
|
2
|
+
/**
|
|
3
|
+
* Provides the ORM bootstrap functionality by returning an initializer provider.
|
|
4
|
+
* This will run database extensions and migrations during application startup.
|
|
5
|
+
* @returns A provider item.
|
|
6
|
+
*/
|
|
7
|
+
export declare function provideOrm(): ProvidersItem;
|
|
8
|
+
/**
|
|
9
|
+
* Bootstraps the ORM by running database extensions and migrations within an advisory lock.
|
|
10
|
+
*/
|
|
11
|
+
export declare function bootstrapOrm(): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { provideInitializer } from '../../application/providers.js';
|
|
3
|
+
import { inject, runInInjectionContext } from '../../injector/inject.js';
|
|
4
|
+
import { Injector } from '../../injector/injector.js';
|
|
5
|
+
import { Database } from './database.js';
|
|
6
|
+
import { runDatabaseExtensions } from './extension.js';
|
|
7
|
+
import { runDatabaseMigrationsCore } from './migration.js';
|
|
8
|
+
/**
|
|
9
|
+
* Provides the ORM bootstrap functionality by returning an initializer provider.
|
|
10
|
+
* This will run database extensions and migrations during application startup.
|
|
11
|
+
* @returns A provider item.
|
|
12
|
+
*/
|
|
13
|
+
export function provideOrm() {
|
|
14
|
+
return provideInitializer(bootstrapOrm);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Bootstraps the ORM by running database extensions and migrations within an advisory lock.
|
|
18
|
+
*/
|
|
19
|
+
export async function bootstrapOrm() {
|
|
20
|
+
const injector = inject(Injector);
|
|
21
|
+
const database = inject(Database);
|
|
22
|
+
const lockId = 123456789; // Fixed lock ID for database setup
|
|
23
|
+
await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
|
|
24
|
+
try {
|
|
25
|
+
await runInInjectionContext(injector, async () => await runDatabaseExtensions());
|
|
26
|
+
await runInInjectionContext(injector, async () => await runDatabaseMigrationsCore());
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PgEnum, type PgSchema, type PgTableWithColumns } from 'drizzle-orm/pg-core';
|
|
1
|
+
import { type PgColumn, type PgEnum, type PgSchema, type PgTableWithColumns } from 'drizzle-orm/pg-core';
|
|
2
2
|
import type { AbstractConstructor, Enumeration } from '../../../types/index.js';
|
|
3
3
|
import { type EntityType } from '../../entity.js';
|
|
4
4
|
import type { ColumnDefinition, ColumnDefinitionsMap, PgTableFromType } from '../types.js';
|
|
@@ -13,4 +13,6 @@ export declare function getColumnDefinitionsMap(table: PgTableWithColumns<any>):
|
|
|
13
13
|
export declare function _getDrizzleTableFromType<T extends EntityType, S extends string>(type: T, fallbackSchemaName?: S): PgTableFromType<T, S>;
|
|
14
14
|
export declare function isTableOwning(type: AbstractConstructor): boolean;
|
|
15
15
|
export declare function getPgEnum(schema: string | PgSchema, enumeration: Enumeration, context?: ConverterContext): PgEnum<[string, ...string[]]>;
|
|
16
|
+
export declare function getPrimaryKeyColumnDefinitions(type: EntityType, table: PgTableFromType, columnDefinitions?: ColumnDefinition[], columnDefinitionsMap?: ColumnDefinitionsMap): ColumnDefinition[];
|
|
17
|
+
export declare function getPrimaryKeyColumns(type: EntityType, table: PgTableFromType, columnDefinitions?: ColumnDefinition[], columnDefinitionsMap?: ColumnDefinitionsMap): PgColumn[];
|
|
16
18
|
export {};
|
|
@@ -15,7 +15,7 @@ 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, isArray, isDefined, isNotNull, isNull, isString, isUndefined } from '../../../utils/type-guards.js';
|
|
18
|
+
import { assertDefined, assertDefinedPass, 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';
|
|
@@ -47,8 +47,13 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
|
|
|
47
47
|
const dbSchema = getDbSchema(schema);
|
|
48
48
|
const inheritanceMetadata = getInheritanceMetadata(type);
|
|
49
49
|
const allColumnDefinitions = getPostgresColumnEntries(type, dbSchema, tableName, undefined, '', { filterInherited: false }, type);
|
|
50
|
+
const primaryKeyPropertyNames = new Set(isDefined(mergedTableReflectionData.primaryKey?.columns)
|
|
51
|
+
? resolveValueOrProvider(mergedTableReflectionData.primaryKey.columns, {}).filter((c) => isString(c))
|
|
52
|
+
: []);
|
|
50
53
|
const columnDefinitions = allColumnDefinitions.filter((cd) => !isChildEntity(type)
|
|
51
54
|
|| !cd.inherited
|
|
55
|
+
|| (cd.reflectionData?.primaryKey == true)
|
|
56
|
+
|| primaryKeyPropertyNames.has(cd.objectPath.path)
|
|
52
57
|
|| (cd.objectPath.path == 'id')
|
|
53
58
|
|| (cd.objectPath.path == 'tenantId')
|
|
54
59
|
|| (cd.objectPath.path == inheritanceMetadata?.discriminatorColumn));
|
|
@@ -100,23 +105,43 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
|
|
|
100
105
|
}
|
|
101
106
|
return builder;
|
|
102
107
|
}
|
|
103
|
-
function
|
|
104
|
-
const inheritanceMetadata =
|
|
108
|
+
function getInheritanceColumns(inheritanceTable, inheritanceType) {
|
|
109
|
+
const inheritanceMetadata = getInheritanceMetadata(inheritanceType);
|
|
105
110
|
const discriminatorColumn = inheritanceMetadata.discriminatorColumn;
|
|
106
|
-
const
|
|
111
|
+
const pkColumnDefinitions = getPrimaryKeyColumnDefinitions(inheritanceType, inheritanceTable, columnDefinitions, columnDefinitionsMap);
|
|
112
|
+
const discriminatorColumnObject = inheritanceTable[discriminatorColumn];
|
|
113
|
+
const hasTenantId = pkColumnDefinitions.some((d) => d.objectPath.path == 'tenantId');
|
|
114
|
+
const tenantIdColumn = hasTenantId ? inheritanceTable['tenantId'] : undefined;
|
|
107
115
|
const columns = [
|
|
108
|
-
...(
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
...(isDefined(tenantIdColumn) ? [tenantIdColumn] : []),
|
|
117
|
+
discriminatorColumnObject,
|
|
118
|
+
...pkColumnDefinitions.filter((d) => d.objectPath.path != 'tenantId').map((d) => inheritanceTable[d.name]),
|
|
119
|
+
];
|
|
120
|
+
const identifierParts = [
|
|
121
|
+
...(hasTenantId ? ['tenantId'] : []),
|
|
122
|
+
discriminatorColumn,
|
|
123
|
+
...pkColumnDefinitions.filter((d) => d.objectPath.path != 'tenantId').map((d) => d.objectPath.path),
|
|
111
124
|
];
|
|
125
|
+
return { columns, identifierParts };
|
|
126
|
+
}
|
|
127
|
+
function buildInheritanceCompositeUniqueConstraint(table) {
|
|
128
|
+
const { columns } = getInheritanceColumns(table, type);
|
|
112
129
|
return unique(getUniqueName(tableName, columns)).on(...columns);
|
|
113
130
|
}
|
|
114
131
|
function buildPrimaryKey(table) {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
132
|
+
const primaryKeyData = mergedTableReflectionData.primaryKey;
|
|
133
|
+
const resolvedColumns = isDefined(primaryKeyData?.columns) ? resolveValueOrProvider(primaryKeyData.columns, table) : undefined;
|
|
134
|
+
const columns = match(resolvedColumns)
|
|
135
|
+
.with(P.nonNullable, (primaryColumns) => primaryColumns.map((columnValue) => {
|
|
136
|
+
if (columnValue instanceof SQL) {
|
|
137
|
+
return columnValue;
|
|
138
|
+
}
|
|
139
|
+
return getColumn(table, columnValue);
|
|
140
|
+
}))
|
|
141
|
+
.with(undefined, () => getPrimaryKeyColumns(type, table, columnDefinitions, columnDefinitionsMap))
|
|
142
|
+
.exhaustive();
|
|
118
143
|
return primaryKey({
|
|
119
|
-
name:
|
|
144
|
+
name: primaryKeyData?.options?.name ?? getPrimaryKeyName(tableName, columns, { naming: primaryKeyData?.options?.naming }),
|
|
120
145
|
columns,
|
|
121
146
|
});
|
|
122
147
|
}
|
|
@@ -128,22 +153,14 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
|
|
|
128
153
|
constraints.push(buildInheritanceCompositeUniqueConstraint(table));
|
|
129
154
|
}
|
|
130
155
|
if (isDefined(inheritanceMetadata) && isDefined(childEntityMetadata)) {
|
|
131
|
-
const
|
|
156
|
+
const parentType = reflectionRegistry.getMetadata(type).parent;
|
|
157
|
+
const parentTable = getDrizzleTableFromType(parentType, schema);
|
|
158
|
+
const { columns: foreignKeyColumns, identifierParts: foreignKeyIdentifierParts } = getInheritanceColumns(table, type);
|
|
159
|
+
const { columns: parentForeignKeyColumns } = getInheritanceColumns(parentTable, parentType);
|
|
132
160
|
constraints.push(check(getIdentifier(tableName, discriminatorColumn, 'check'), eq(table[discriminatorColumn], sql.raw(`'${childEntityMetadata.discriminatorValue}'`))), foreignKey({
|
|
133
|
-
name: getForeignKeyName(tableName, getEntityTableName(
|
|
134
|
-
columns:
|
|
135
|
-
|
|
136
|
-
table[discriminatorColumn],
|
|
137
|
-
table['id'],
|
|
138
|
-
],
|
|
139
|
-
foreignColumns: (() => {
|
|
140
|
-
const parentTable = getDrizzleTableFromType(reflectionRegistry.getMetadata(type).parent, schema);
|
|
141
|
-
return [
|
|
142
|
-
...(hasTenantId ? [parentTable['tenantId']] : []),
|
|
143
|
-
parentTable[discriminatorColumn],
|
|
144
|
-
parentTable.id,
|
|
145
|
-
];
|
|
146
|
-
})(),
|
|
161
|
+
name: getForeignKeyName(tableName, getEntityTableName(parentType), foreignKeyIdentifierParts),
|
|
162
|
+
columns: foreignKeyColumns,
|
|
163
|
+
foreignColumns: parentForeignKeyColumns,
|
|
147
164
|
}).onDelete('cascade'));
|
|
148
165
|
}
|
|
149
166
|
return constraints;
|
|
@@ -222,7 +239,7 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
|
|
|
222
239
|
return builder;
|
|
223
240
|
}
|
|
224
241
|
const primaryKeyColumnDefinitions = columnDefinitions.filter((columnDefinition) => (columnDefinition.reflectionData?.primaryKey == true) || (columnDefinition.objectPath.path == 'id' && columnDefinition.reflectionData?.primaryKey != false));
|
|
225
|
-
const skipPrimaryKey = primaryKeyColumnDefinitions.length > 1;
|
|
242
|
+
const skipPrimaryKey = (primaryKeyColumnDefinitions.length > 1) || isDefined(mergedTableReflectionData.primaryKey?.columns);
|
|
226
243
|
const columnEntries = columnDefinitions.map((entry) => [entry.name, entry.buildType({ skipPrimaryKey })]);
|
|
227
244
|
const drizzleSchema = dbSchema.table(tableName, fromEntries(columnEntries), (drizzleTable) => {
|
|
228
245
|
const table = drizzleTable;
|
|
@@ -231,7 +248,7 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
|
|
|
231
248
|
const bm25Index = buildParadeIndex(table);
|
|
232
249
|
const checks = tableReflectionDatas.flatMap((tableReflectionData) => tableReflectionData.checks).filter(isDefined).map((data) => check(data.name, data.builder(table)));
|
|
233
250
|
return [
|
|
234
|
-
...(
|
|
251
|
+
...(skipPrimaryKey
|
|
235
252
|
? [buildPrimaryKey(table)]
|
|
236
253
|
: []),
|
|
237
254
|
...buildInheritanceConstraints(table),
|
|
@@ -272,7 +289,8 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
|
|
|
272
289
|
}) ?? [];
|
|
273
290
|
}),
|
|
274
291
|
...tableReflectionDatas.flatMap((tableReflectionData) => tableReflectionData.unique).filter(isDefined).map((data) => {
|
|
275
|
-
const
|
|
292
|
+
const resolvedColumns = resolveValueOrProvider(data.columns, table);
|
|
293
|
+
const columns = resolvedColumns.map((column) => getColumn(table, column));
|
|
276
294
|
let constraint = unique(data.options?.name ?? getUniqueName(tableName, columns, { naming: data.options?.naming })).on(...columns);
|
|
277
295
|
if (data.options?.nulls == 'not distinct') {
|
|
278
296
|
constraint = constraint.nullsNotDistinct();
|
|
@@ -469,6 +487,30 @@ export function getPgEnum(schema, enumeration, context) {
|
|
|
469
487
|
}
|
|
470
488
|
return dbEnum;
|
|
471
489
|
}
|
|
490
|
+
export function getPrimaryKeyColumnDefinitions(type, table, columnDefinitions, columnDefinitionsMap) {
|
|
491
|
+
const tableReflectionDatas = getTableReflectionDatas(type);
|
|
492
|
+
const mergedTableReflectionData = tableReflectionDatas.reduceRight((merged, data) => ({ ...merged, ...data }), {});
|
|
493
|
+
const resolvedColumnDefinitions = columnDefinitions ?? getColumnDefinitions(table);
|
|
494
|
+
const resolvedColumnDefinitionsMap = columnDefinitionsMap ?? getColumnDefinitionsMap(table);
|
|
495
|
+
const primaryKeyData = mergedTableReflectionData.primaryKey;
|
|
496
|
+
let definitions;
|
|
497
|
+
if (isDefined(primaryKeyData?.columns)) {
|
|
498
|
+
const resolvedColumns = resolveValueOrProvider(primaryKeyData.columns, table);
|
|
499
|
+
definitions = resolvedColumns.map((columnValue) => {
|
|
500
|
+
if (columnValue instanceof SQL) {
|
|
501
|
+
throw new NotSupportedError('SQL in primary key is not supported by Repository.');
|
|
502
|
+
}
|
|
503
|
+
return assertDefinedPass(resolvedColumnDefinitionsMap.get(columnValue), `Could not map primary key property ${columnValue} to column.`);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
definitions = resolvedColumnDefinitions.filter((columnDefinition) => (columnDefinition.reflectionData?.primaryKey == true) || (columnDefinition.objectPath.path == 'id' && columnDefinition.reflectionData?.primaryKey != false));
|
|
508
|
+
}
|
|
509
|
+
return definitions.toSorted(compareByValueSelectionToOrder(['tenantId', 'id', orderRest], (columnDefinition) => columnDefinition.objectPath.path));
|
|
510
|
+
}
|
|
511
|
+
export function getPrimaryKeyColumns(type, table, columnDefinitions, columnDefinitionsMap) {
|
|
512
|
+
return getPrimaryKeyColumnDefinitions(type, table, columnDefinitions, columnDefinitionsMap).map((def) => table[def.name]);
|
|
513
|
+
}
|
|
472
514
|
function getPrimaryKeyName(tableName, columnsOrBaseName, options) {
|
|
473
515
|
return getIdentifier(tableName, columnsOrBaseName, 'pk', options);
|
|
474
516
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Injector } from '../../injector/index.js';
|
|
2
|
+
export type DatabaseExtension = {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
export declare const DATABASE_EXTENSION: import("../../injector/index.js").InjectionToken<DatabaseExtension, never>;
|
|
6
|
+
/**
|
|
7
|
+
* Registers a database extension in the provided injector.
|
|
8
|
+
* @param name - The name of the extension.
|
|
9
|
+
* @param options - Optional injector.
|
|
10
|
+
*/
|
|
11
|
+
export declare function registerDatabaseExtension(name: string, { injector }?: {
|
|
12
|
+
injector?: Injector;
|
|
13
|
+
}): void;
|
|
14
|
+
export declare function runDatabaseExtensions(): Promise<void>;
|
|
@@ -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
package/orm/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Injector
|
|
1
|
+
import { Injector } from '../../injector/index.js';
|
|
2
2
|
export type DatabaseMigration = {
|
|
3
3
|
name: string;
|
|
4
4
|
migrate: () => void | Promise<void>;
|
|
@@ -15,5 +15,4 @@ export declare function registerDatabaseMigration(name: string, migrate: () => v
|
|
|
15
15
|
injector?: Injector;
|
|
16
16
|
dependencies?: string[];
|
|
17
17
|
}): void;
|
|
18
|
-
export declare function
|
|
19
|
-
export declare function runDatabaseMigrations(): Promise<void>;
|
|
18
|
+
export declare function runDatabaseMigrationsCore(migrations?: DatabaseMigration[]): Promise<void>;
|
package/orm/server/migration.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { provideInitializer } from '../../application/providers.js';
|
|
3
1
|
import { injectionToken, Injector } from '../../injector/index.js';
|
|
4
2
|
import { inject, injectAll, runInInjectionContext } from '../../injector/inject.js';
|
|
5
3
|
import { Logger } from '../../logger/index.js';
|
|
6
4
|
import { isDefined } from '../../utils/type-guards.js';
|
|
7
|
-
import { Database } from './database.js';
|
|
8
5
|
export const DATABASE_MIGRATION = injectionToken('DatabaseMigration');
|
|
9
6
|
/**
|
|
10
7
|
* Registers a database migration in the provided injector.
|
|
@@ -16,28 +13,17 @@ export function registerDatabaseMigration(name, migrate, { injector, dependencie
|
|
|
16
13
|
const targetInjector = injector ?? Injector;
|
|
17
14
|
targetInjector.register(DATABASE_MIGRATION, { useValue: { name, migrate, dependencies } }, { multi: true });
|
|
18
15
|
}
|
|
19
|
-
export function
|
|
20
|
-
return provideInitializer(runDatabaseMigrations);
|
|
21
|
-
}
|
|
22
|
-
export async function runDatabaseMigrations() {
|
|
16
|
+
export async function runDatabaseMigrationsCore(migrations) {
|
|
23
17
|
const injector = inject(Injector);
|
|
24
|
-
const database = inject(Database);
|
|
25
18
|
const logger = inject(Logger, 'DatabaseMigrationOrchestrator');
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
19
|
+
const actualMigrations = migrations ?? injectAll(DATABASE_MIGRATION, undefined, { optional: true });
|
|
20
|
+
if (actualMigrations.length == 0) {
|
|
28
21
|
return;
|
|
29
22
|
}
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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})`);
|
|
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());
|
|
41
27
|
}
|
|
42
28
|
}
|
|
43
29
|
function sortMigrations(migrations) {
|
|
@@ -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';
|