@pattern-stack/codegen 0.8.0 → 0.9.0
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/CHANGELOG.md +70 -0
- package/dist/runtime/subsystems/auth/controllers/auth.controller.d.ts +1 -0
- package/dist/runtime/subsystems/auth/index.d.ts +2 -0
- package/dist/runtime/subsystems/auth/index.js +55 -0
- package/dist/runtime/subsystems/auth/index.js.map +1 -1
- package/dist/runtime/subsystems/auth/middleware/requester-context.d.ts +81 -0
- package/dist/runtime/subsystems/auth/middleware/requester-context.js +60 -0
- package/dist/runtime/subsystems/auth/middleware/requester-context.js.map +1 -0
- package/dist/runtime/subsystems/auth/protocols/user-context.d.ts +18 -0
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge.module.d.ts +3 -0
- package/dist/runtime/subsystems/bridge/bridge.module.js +930 -275
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.d.ts +3 -0
- package/dist/runtime/subsystems/bridge/index.js +837 -182
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +3 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +92 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +3 -1
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js +99 -0
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-keyset-cursor.d.ts +32 -0
- package/dist/runtime/subsystems/events/event-keyset-cursor.js +38 -0
- package/dist/runtime/subsystems/events/event-keyset-cursor.js.map +1 -0
- package/dist/runtime/subsystems/events/event-read.protocol.d.ts +94 -0
- package/dist/runtime/subsystems/events/event-read.protocol.js +9 -0
- package/dist/runtime/subsystems/events/event-read.protocol.js.map +1 -0
- package/dist/runtime/subsystems/events/events.module.js +177 -3
- package/dist/runtime/subsystems/events/events.module.js.map +1 -1
- package/dist/runtime/subsystems/events/events.tokens.d.ts +16 -1
- package/dist/runtime/subsystems/events/events.tokens.js +2 -0
- package/dist/runtime/subsystems/events/events.tokens.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/bus.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/index.js.map +1 -1
- package/dist/runtime/subsystems/events/index.d.ts +2 -1
- package/dist/runtime/subsystems/events/index.js +178 -3
- package/dist/runtime/subsystems/events/index.js.map +1 -1
- package/dist/runtime/subsystems/index.d.ts +2 -0
- package/dist/runtime/subsystems/index.js +1198 -264
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +98 -0
- package/dist/runtime/subsystems/jobs/bullmq.config.js +143 -0
- package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -0
- package/dist/runtime/subsystems/jobs/index.d.ts +6 -2
- package/dist/runtime/subsystems/jobs/index.js +861 -201
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +107 -0
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +922 -0
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +52 -0
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js +57 -0
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +81 -1
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +81 -0
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +74 -1
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +48 -0
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +374 -0
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +42 -4
- package/dist/runtime/subsystems/jobs/job-worker.module.js +832 -178
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +10 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +519 -20
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/pool-config.loader.d.ts +9 -1
- package/dist/runtime/subsystems/jobs/pool-config.loader.js +4 -0
- package/dist/runtime/subsystems/jobs/pool-config.loader.js.map +1 -1
- package/dist/runtime/subsystems/observability/index.d.ts +4 -3
- package/dist/runtime/subsystems/observability/index.js +109 -2
- package/dist/runtime/subsystems/observability/index.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.module.js +109 -2
- package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.protocol.d.ts +63 -2
- package/dist/runtime/subsystems/observability/observability.service.d.ts +21 -3
- package/dist/runtime/subsystems/observability/observability.service.js +109 -2
- package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -0
- package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -0
- package/dist/src/cli/index.js +43 -7
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/auth/index.ts +8 -0
- package/runtime/subsystems/auth/middleware/requester-context.ts +141 -0
- package/runtime/subsystems/auth/protocols/user-context.ts +17 -0
- package/runtime/subsystems/bridge/bridge.module.ts +5 -0
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +109 -3
- package/runtime/subsystems/events/event-bus.memory-backend.ts +103 -1
- package/runtime/subsystems/events/event-keyset-cursor.ts +59 -0
- package/runtime/subsystems/events/event-read.protocol.ts +97 -0
- package/runtime/subsystems/events/events.module.ts +18 -2
- package/runtime/subsystems/events/events.tokens.ts +16 -0
- package/runtime/subsystems/events/index.ts +7 -0
- package/runtime/subsystems/jobs/bullmq.config.ts +125 -0
- package/runtime/subsystems/jobs/index.ts +22 -0
- package/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts +381 -0
- package/runtime/subsystems/jobs/job-run-keyset-cursor.ts +88 -0
- package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +59 -1
- package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +53 -0
- package/runtime/subsystems/jobs/job-run-service.protocol.ts +77 -0
- package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +311 -0
- package/runtime/subsystems/jobs/job-worker.module.ts +124 -10
- package/runtime/subsystems/jobs/jobs-domain.module.ts +40 -21
- package/runtime/subsystems/jobs/pool-config.loader.ts +11 -0
- package/runtime/subsystems/observability/index.ts +8 -0
- package/runtime/subsystems/observability/observability.protocol.ts +76 -0
- package/runtime/subsystems/observability/observability.service.ts +148 -1
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +14 -12
- package/templates/relationship/new/prompt.js +8 -5
- package/templates/subsystem/jobs/worker.ejs.t +30 -7
- package/templates/subsystem/sync/sync-audit.schema.ejs.t +12 -16
|
@@ -47,10 +47,19 @@ import type { IJobRunService } from './job-run-service.protocol';
|
|
|
47
47
|
import type { IJobStepService } from './job-step-service.protocol';
|
|
48
48
|
import {
|
|
49
49
|
allNonReservedPoolNames,
|
|
50
|
+
allPoolNames,
|
|
50
51
|
loadPoolConfig,
|
|
51
52
|
type PoolConfig,
|
|
52
53
|
} from './pool-config.loader';
|
|
53
54
|
import { JobWorker, type JobWorkerOptions } from './job-worker';
|
|
55
|
+
import { BullMQJobWorker } from './job-worker.bullmq-backend';
|
|
56
|
+
import type { ConnectionOptions } from 'bullmq';
|
|
57
|
+
import {
|
|
58
|
+
BULLMQ_CONNECTION,
|
|
59
|
+
BULLMQ_RESOLVED_CONFIG,
|
|
60
|
+
resolvePoolQueueName,
|
|
61
|
+
type BullMqResolvedConfig,
|
|
62
|
+
} from './bullmq.config';
|
|
54
63
|
import {
|
|
55
64
|
BootValidationError,
|
|
56
65
|
ReservedPoolViolationError,
|
|
@@ -62,10 +71,11 @@ export interface JobWorkerModuleOptions {
|
|
|
62
71
|
mode: 'embedded' | 'standalone';
|
|
63
72
|
/**
|
|
64
73
|
* Threads into the internal `JobsDomainModule.forRoot({ backend })`
|
|
65
|
-
* import. Default `'drizzle'`. The boot-time validator runs
|
|
66
|
-
*
|
|
74
|
+
* import. Default `'drizzle'`. The boot-time validator runs for both
|
|
75
|
+
* `'drizzle'` and `'bullmq'` (both persist `job` rows to Postgres);
|
|
76
|
+
* `'memory'` skips it.
|
|
67
77
|
*/
|
|
68
|
-
backend?: 'drizzle' | 'memory';
|
|
78
|
+
backend?: 'drizzle' | 'memory' | 'bullmq';
|
|
69
79
|
/**
|
|
70
80
|
* Active pool names. Defaults to every non-reserved pool in the resolved
|
|
71
81
|
* config (i.e. `interactive`, `batch`, plus any user-defined pools).
|
|
@@ -73,6 +83,18 @@ export interface JobWorkerModuleOptions {
|
|
|
73
83
|
* horizontally.
|
|
74
84
|
*/
|
|
75
85
|
pools?: string[];
|
|
86
|
+
/**
|
|
87
|
+
* BULLMQ-1 Phase 1 — when `true`, `onModuleInit` activates **every** pool
|
|
88
|
+
* in the resolved config, including the reserved `events_*` lanes. This is
|
|
89
|
+
* how the standalone worker (`worker.ts`) drains bridge wrappers without
|
|
90
|
+
* the consumer hand-listing `...BRIDGE_RESERVED_POOLS`. Mutually exclusive
|
|
91
|
+
* with an explicit `pools` list — when both are set, `pools` wins (explicit
|
|
92
|
+
* beats blanket) and `allPools` is ignored.
|
|
93
|
+
*
|
|
94
|
+
* `BridgeModule`'s reserved-pool guard short-circuits to "pass" when this
|
|
95
|
+
* is `true`, since every reserved pool is provably being polled.
|
|
96
|
+
*/
|
|
97
|
+
allPools?: boolean;
|
|
76
98
|
/** SIGTERM drain budget. Default 30_000 ms. */
|
|
77
99
|
shutdownTimeoutMs?: number;
|
|
78
100
|
/**
|
|
@@ -128,6 +150,17 @@ export class JobWorkerOrchestrator implements OnModuleInit, OnModuleDestroy {
|
|
|
128
150
|
*/
|
|
129
151
|
@Optional() @Inject(DRIZZLE) private readonly db: DrizzleClient | null = null,
|
|
130
152
|
private readonly moduleRef?: ModuleRef,
|
|
153
|
+
/**
|
|
154
|
+
* BULLMQ-1 — resolved BullMQ connection + config, only bound when the
|
|
155
|
+
* inner `JobsDomainModule` was booted with `backend: 'bullmq'`. `@Optional()`
|
|
156
|
+
* so drizzle/memory boots see `null`.
|
|
157
|
+
*/
|
|
158
|
+
@Optional()
|
|
159
|
+
@Inject(BULLMQ_CONNECTION)
|
|
160
|
+
private readonly bullConnection: ConnectionOptions | null = null,
|
|
161
|
+
@Optional()
|
|
162
|
+
@Inject(BULLMQ_RESOLVED_CONFIG)
|
|
163
|
+
private readonly bullConfig: BullMqResolvedConfig | null = null,
|
|
131
164
|
) {}
|
|
132
165
|
|
|
133
166
|
// ============================================================================
|
|
@@ -166,8 +199,14 @@ export class JobWorkerOrchestrator implements OnModuleInit, OnModuleDestroy {
|
|
|
166
199
|
}
|
|
167
200
|
|
|
168
201
|
// (6) Resolve active pool list and spawn one worker per pool.
|
|
169
|
-
|
|
170
|
-
|
|
202
|
+
// Precedence: explicit `pools` > `allPools` (incl. reserved) >
|
|
203
|
+
// non-reserved default. BULLMQ-1 Phase 1 adds the `allPools` rung so
|
|
204
|
+
// the standalone worker drains the reserved `events_*` bridge lanes.
|
|
205
|
+
const activePools = this.options.pools
|
|
206
|
+
? this.options.pools
|
|
207
|
+
: this.options.allPools
|
|
208
|
+
? allPoolNames(poolConfig)
|
|
209
|
+
: allNonReservedPoolNames(poolConfig);
|
|
171
210
|
|
|
172
211
|
for (const poolName of activePools) {
|
|
173
212
|
const def = poolConfig.get(poolName);
|
|
@@ -193,14 +232,20 @@ export class JobWorkerOrchestrator implements OnModuleInit, OnModuleDestroy {
|
|
|
193
232
|
};
|
|
194
233
|
const worker = this.options.workerFactory
|
|
195
234
|
? this.options.workerFactory(workerOptions)
|
|
196
|
-
:
|
|
235
|
+
: backend === 'bullmq'
|
|
236
|
+
? this.spawnBullMQWorker(poolName, def.queue, def.concurrency, poolConfig)
|
|
237
|
+
: this.spawnWorker(workerOptions);
|
|
197
238
|
// `JobWorker` extends Nest's lifecycle hooks but the worker isn't
|
|
198
239
|
// a Nest provider here (we manage the array ourselves). Call
|
|
199
|
-
// `onModuleInit`
|
|
200
|
-
|
|
240
|
+
// `onModuleInit` to start the loop. The Drizzle/stub workers return
|
|
241
|
+
// void; `BullMQJobWorker.onModuleInit` is async (it lazily loads the
|
|
242
|
+
// optional `bullmq` package), so we `await` — awaiting a `void` is a
|
|
243
|
+
// harmless no-op for the synchronous workers.
|
|
244
|
+
await worker.onModuleInit();
|
|
201
245
|
this.workers.push(worker);
|
|
202
246
|
this.logger.log(
|
|
203
|
-
`JobWorker started: pool='${poolName}' (queue='${def.queue}')
|
|
247
|
+
`JobWorker started: pool='${poolName}' (queue='${def.queue}') ` +
|
|
248
|
+
`concurrency=${def.concurrency} backend='${backend}'`,
|
|
204
249
|
);
|
|
205
250
|
}
|
|
206
251
|
}
|
|
@@ -220,6 +265,20 @@ export class JobWorkerOrchestrator implements OnModuleInit, OnModuleDestroy {
|
|
|
220
265
|
}
|
|
221
266
|
}
|
|
222
267
|
this.workers.length = 0;
|
|
268
|
+
|
|
269
|
+
// BULLMQ-1 — close the orchestrator's producer-side Queue/FlowProducer
|
|
270
|
+
// connections so the process can exit cleanly. The orchestrator is the
|
|
271
|
+
// BullMQ producer; workers are the consumers (closed above).
|
|
272
|
+
const orch = this.orchestrator as { closeConnections?: () => Promise<void> };
|
|
273
|
+
if (typeof orch.closeConnections === 'function') {
|
|
274
|
+
try {
|
|
275
|
+
await orch.closeConnections();
|
|
276
|
+
} catch (err) {
|
|
277
|
+
this.logger.error(
|
|
278
|
+
`BullMQ orchestrator connection close failed: ${(err as Error).message}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
223
282
|
}
|
|
224
283
|
|
|
225
284
|
// ============================================================================
|
|
@@ -296,6 +355,54 @@ export class JobWorkerOrchestrator implements OnModuleInit, OnModuleDestroy {
|
|
|
296
355
|
this.moduleRef,
|
|
297
356
|
);
|
|
298
357
|
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* BULLMQ-1 — spawn a per-pool `BullMQJobWorker`. Requires the Drizzle
|
|
361
|
+
* client (the worker drives `job_run` as the source of truth) AND the
|
|
362
|
+
* resolved BullMQ connection (bound by `JobsDomainModule` when
|
|
363
|
+
* `backend: 'bullmq'`). The queue name is derived identically to the
|
|
364
|
+
* orchestrator's `dispatch` via `resolvePoolQueueName(pool, …)` so producer
|
|
365
|
+
* and consumer agree.
|
|
366
|
+
*/
|
|
367
|
+
private spawnBullMQWorker(
|
|
368
|
+
pool: string,
|
|
369
|
+
_queueAlias: string,
|
|
370
|
+
concurrency: number,
|
|
371
|
+
poolConfig: PoolConfig,
|
|
372
|
+
): BullMQJobWorker {
|
|
373
|
+
if (!this.db) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`JobWorkerModule: BullMQ worker spawning requires the Drizzle client ` +
|
|
376
|
+
`(no DRIZZLE provider available) — job_run remains the source of truth.`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (!this.bullConnection) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`JobWorkerModule: BullMQ worker spawning requires a resolved ` +
|
|
382
|
+
`BULLMQ_CONNECTION. Ensure JobsDomainModule was booted with ` +
|
|
383
|
+
`backend: 'bullmq'.`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (!this.moduleRef) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`JobWorkerModule: ModuleRef not available — cannot construct ` +
|
|
389
|
+
`BullMQJobWorker with handler DI support.`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
const queueName = resolvePoolQueueName(pool, this.bullConfig, poolConfig);
|
|
393
|
+
return new BullMQJobWorker(
|
|
394
|
+
this.db,
|
|
395
|
+
this.orchestrator,
|
|
396
|
+
this.stepService,
|
|
397
|
+
{
|
|
398
|
+
pool,
|
|
399
|
+
queueName,
|
|
400
|
+
concurrency,
|
|
401
|
+
connection: this.bullConnection,
|
|
402
|
+
},
|
|
403
|
+
this.moduleRef,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
299
406
|
}
|
|
300
407
|
|
|
301
408
|
@Module({})
|
|
@@ -314,7 +421,14 @@ export class JobWorkerModule {
|
|
|
314
421
|
{ provide: JOB_WORKER_MODULE_OPTIONS, useValue: opts },
|
|
315
422
|
JobWorkerOrchestrator,
|
|
316
423
|
],
|
|
317
|
-
|
|
424
|
+
// BULLMQ-1 Phase 1 — export the options token so `BridgeModule`'s
|
|
425
|
+
// reserved-pool guard (`onModuleInit`) can actually inject it.
|
|
426
|
+
// Previously `exports: []` left the `@Optional()` inject resolving to
|
|
427
|
+
// `undefined` and the guard silently no-opped (a dead check). With the
|
|
428
|
+
// token exported the guard fires for real; consumers that omit the
|
|
429
|
+
// reserved pools (and don't set `allPools`) now fail fast with
|
|
430
|
+
// `BridgeReservedPoolsNotPolledError` — which is correct.
|
|
431
|
+
exports: [JOB_WORKER_MODULE_OPTIONS],
|
|
318
432
|
};
|
|
319
433
|
}
|
|
320
434
|
}
|
|
@@ -24,10 +24,17 @@ import {
|
|
|
24
24
|
import { DrizzleJobOrchestrator } from './job-orchestrator.drizzle-backend';
|
|
25
25
|
import { DrizzleJobRunService } from './job-run-service.drizzle-backend';
|
|
26
26
|
import { DrizzleJobStepService } from './job-step-service.drizzle-backend';
|
|
27
|
+
import { BullMQJobOrchestrator } from './job-orchestrator.bullmq-backend';
|
|
27
28
|
import { MemoryJobOrchestrator } from './job-orchestrator.memory-backend';
|
|
28
29
|
import { MemoryJobRunService } from './job-run-service.memory-backend';
|
|
29
30
|
import { MemoryJobStepService } from './job-step-service.memory-backend';
|
|
30
31
|
import { MemoryJobStore } from './memory-job-store';
|
|
32
|
+
import {
|
|
33
|
+
BULLMQ_CONNECTION,
|
|
34
|
+
BULLMQ_RESOLVED_CONFIG,
|
|
35
|
+
resolveBullMqConfig,
|
|
36
|
+
type BullMqExtensionsConfig,
|
|
37
|
+
} from './bullmq.config';
|
|
31
38
|
|
|
32
39
|
/**
|
|
33
40
|
* Drizzle backend extensions surface. None are wired into the Drizzle
|
|
@@ -44,19 +51,8 @@ export interface DrizzleBackendExtensions {
|
|
|
44
51
|
pollIntervalMs?: number;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
// Phase 6+ — typed-but-unimplemented BullMQ extension slot. Kept as a
|
|
48
|
-
// commented-out interface to make the future shape discoverable without
|
|
49
|
-
// shipping dead runtime code. Per CLAUDE.md "no feature-flag-guarded dead
|
|
50
|
-
// code" we don't ship the option in `JobsDomainModuleOptions.extensions`
|
|
51
|
-
// either; flip it on when JOB-Phase-6 lands the BullMQ orchestrator.
|
|
52
|
-
//
|
|
53
|
-
// export interface BullMqBackendExtensions {
|
|
54
|
-
// bullBoard?: { enabled: boolean; mountPath?: string };
|
|
55
|
-
// redisUrl?: string;
|
|
56
|
-
// }
|
|
57
|
-
|
|
58
54
|
export interface JobsDomainModuleOptions {
|
|
59
|
-
backend: 'drizzle' | 'memory';
|
|
55
|
+
backend: 'drizzle' | 'memory' | 'bullmq';
|
|
60
56
|
/**
|
|
61
57
|
* Backend-specific extensions. Only the matching backend's extensions
|
|
62
58
|
* are read at boot; non-matching keys are ignored. This is the
|
|
@@ -64,7 +60,12 @@ export interface JobsDomainModuleOptions {
|
|
|
64
60
|
*/
|
|
65
61
|
extensions?: {
|
|
66
62
|
drizzle?: DrizzleBackendExtensions;
|
|
67
|
-
|
|
63
|
+
/**
|
|
64
|
+
* BullMQ backend extensions (BULLMQ-1). Snake_case mirrors the YAML
|
|
65
|
+
* under `jobs.extensions.bullmq`. `redis_url` falls back to
|
|
66
|
+
* `process.env.REDIS_URL` then `redis://localhost:6379`.
|
|
67
|
+
*/
|
|
68
|
+
bullmq?: BullMqExtensionsConfig;
|
|
68
69
|
};
|
|
69
70
|
/** Multi-tenancy opt-in. Wired by JOB-8; module signature stays stable. */
|
|
70
71
|
multiTenant?: boolean;
|
|
@@ -73,8 +74,6 @@ export interface JobsDomainModuleOptions {
|
|
|
73
74
|
@Module({})
|
|
74
75
|
export class JobsDomainModule {
|
|
75
76
|
static forRoot(opts: JobsDomainModuleOptions): DynamicModule {
|
|
76
|
-
void opts.extensions; // typed reservation; consumed by Phase 6+ wiring
|
|
77
|
-
|
|
78
77
|
const multiTenant = opts.multiTenant ?? false;
|
|
79
78
|
|
|
80
79
|
const providers: Provider[] = [
|
|
@@ -98,22 +97,42 @@ export class JobsDomainModule {
|
|
|
98
97
|
providers.push({ provide: JOB_ORCHESTRATOR, useExisting: MemoryJobOrchestrator });
|
|
99
98
|
providers.push(MemoryJobRunService);
|
|
100
99
|
providers.push({ provide: JOB_RUN_SERVICE, useExisting: MemoryJobRunService });
|
|
100
|
+
} else if (opts.backend === 'bullmq') {
|
|
101
|
+
// BULLMQ-1 — BullMQ orchestrator over a Postgres source of truth. The
|
|
102
|
+
// run/step services stay Drizzle (domain reads + `listForScope` are
|
|
103
|
+
// Postgres queries, unchanged per spec). Only the orchestrator's
|
|
104
|
+
// claim/dispatch half swaps to BullMQ.
|
|
105
|
+
const resolved = resolveBullMqConfig(opts.extensions?.bullmq);
|
|
106
|
+
providers.push({ provide: BULLMQ_CONNECTION, useValue: resolved.connection });
|
|
107
|
+
providers.push({ provide: BULLMQ_RESOLVED_CONFIG, useValue: resolved });
|
|
108
|
+
providers.push({ provide: JOB_ORCHESTRATOR, useClass: BullMQJobOrchestrator });
|
|
109
|
+
providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
|
|
110
|
+
providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
|
|
101
111
|
} else {
|
|
102
112
|
providers.push({ provide: JOB_ORCHESTRATOR, useClass: DrizzleJobOrchestrator });
|
|
103
113
|
providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
|
|
104
114
|
providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
|
|
105
115
|
}
|
|
106
116
|
|
|
117
|
+
const exports = [
|
|
118
|
+
JOB_ORCHESTRATOR,
|
|
119
|
+
JOB_RUN_SERVICE,
|
|
120
|
+
JOB_STEP_SERVICE,
|
|
121
|
+
JOBS_MULTI_TENANT,
|
|
122
|
+
];
|
|
123
|
+
// BULLMQ-1 — only export the BullMQ tokens when they were actually
|
|
124
|
+
// provided. Nest throws "exported but not provided" otherwise. Exported so
|
|
125
|
+
// JobWorkerModule (which imports this module) can read the resolved
|
|
126
|
+
// connection/config to spawn BullMQ workers.
|
|
127
|
+
if (opts.backend === 'bullmq') {
|
|
128
|
+
exports.push(BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG);
|
|
129
|
+
}
|
|
130
|
+
|
|
107
131
|
return {
|
|
108
132
|
module: JobsDomainModule,
|
|
109
133
|
global: true,
|
|
110
134
|
providers,
|
|
111
|
-
exports
|
|
112
|
-
JOB_ORCHESTRATOR,
|
|
113
|
-
JOB_RUN_SERVICE,
|
|
114
|
-
JOB_STEP_SERVICE,
|
|
115
|
-
JOBS_MULTI_TENANT,
|
|
116
|
-
],
|
|
135
|
+
exports,
|
|
117
136
|
};
|
|
118
137
|
}
|
|
119
138
|
}
|
|
@@ -194,6 +194,17 @@ export function allNonReservedPoolNames(config: PoolConfig): string[] {
|
|
|
194
194
|
return out;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Names of **every** pool in the resolved config, reserved `events_*` lanes
|
|
199
|
+
* included. The activation set for a standalone worker booted with
|
|
200
|
+
* `JobWorkerModule.forRoot({ allPools: true })` (BULLMQ-1 Phase 1) — the
|
|
201
|
+
* single worker process drains both user pools and the bridge's reserved
|
|
202
|
+
* pools so wrapper `job_run` rows are never stranded.
|
|
203
|
+
*/
|
|
204
|
+
export function allPoolNames(config: PoolConfig): string[] {
|
|
205
|
+
return [...config.keys()];
|
|
206
|
+
}
|
|
207
|
+
|
|
197
208
|
// ─── internals ──────────────────────────────────────────────────────────────
|
|
198
209
|
|
|
199
210
|
interface UserPoolShape {
|
|
@@ -30,6 +30,14 @@ export type {
|
|
|
30
30
|
IObservability,
|
|
31
31
|
PoolStatusCount,
|
|
32
32
|
JobRunFailure,
|
|
33
|
+
JobRunSummary,
|
|
34
|
+
JobRunPage,
|
|
35
|
+
ListJobRunsQuery,
|
|
36
|
+
EventSummary,
|
|
37
|
+
EventPage,
|
|
38
|
+
ListEventsQuery,
|
|
39
|
+
CorrelationTimeline,
|
|
40
|
+
CorrelationTimelineEntry,
|
|
33
41
|
StatusHistogram,
|
|
34
42
|
SyncRunSummary,
|
|
35
43
|
CursorSnapshot,
|
|
@@ -21,12 +21,49 @@
|
|
|
21
21
|
|
|
22
22
|
import type {
|
|
23
23
|
JobRunFailure,
|
|
24
|
+
JobRunPage,
|
|
25
|
+
JobRunSummary,
|
|
26
|
+
ListJobRunsQuery,
|
|
24
27
|
PoolStatusCount,
|
|
25
28
|
} from '../jobs/job-run-service.protocol';
|
|
29
|
+
import type {
|
|
30
|
+
EventPage,
|
|
31
|
+
EventSummary,
|
|
32
|
+
ListEventsQuery,
|
|
33
|
+
} from '../events/event-read.protocol';
|
|
26
34
|
import type { StatusHistogram } from '../bridge/bridge.protocol';
|
|
27
35
|
import type { SyncRunSummary } from '../sync/sync-run-recorder.protocol';
|
|
28
36
|
import type { CursorSnapshot } from '../sync/sync-cursor-store.protocol';
|
|
29
37
|
|
|
38
|
+
/**
|
|
39
|
+
* One chronological entry in a correlation timeline (OBS-LIST-1). Either a
|
|
40
|
+
* `job_run` or a `domain_event` sharing the same `rootRunId`, tagged with a
|
|
41
|
+
* `kind` discriminator and a single `at` timestamp used for ordering.
|
|
42
|
+
*/
|
|
43
|
+
export type CorrelationTimelineEntry =
|
|
44
|
+
| { kind: 'job_run'; at: Date; run: JobRunSummary }
|
|
45
|
+
| { kind: 'event'; at: Date; event: EventSummary };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stitched view of everything correlated to a single `rootRunId`
|
|
49
|
+
* (OBS-LIST-1): the job runs sharing that root plus the domain events whose
|
|
50
|
+
* `metadata.rootRunId` matches, merged into one ascending timeline with a
|
|
51
|
+
* small roll-up summary.
|
|
52
|
+
*/
|
|
53
|
+
export interface CorrelationTimeline {
|
|
54
|
+
rootRunId: string;
|
|
55
|
+
/** Ascending by `at`. Job runs ordered by `createdAt`; events by `occurredAt`. */
|
|
56
|
+
entries: CorrelationTimelineEntry[];
|
|
57
|
+
summary: {
|
|
58
|
+
runCount: number;
|
|
59
|
+
eventCount: number;
|
|
60
|
+
/** Earliest `at` across all entries, or `null` when empty. */
|
|
61
|
+
startedAt: Date | null;
|
|
62
|
+
/** Latest `at` across all entries, or `null` when empty. */
|
|
63
|
+
lastActivityAt: Date | null;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
30
67
|
export interface IObservability {
|
|
31
68
|
/**
|
|
32
69
|
* Live `(pool, status)` counts across `job_run`. Delegates to
|
|
@@ -79,6 +116,39 @@ export interface IObservability {
|
|
|
79
116
|
* Empty array when the sync subsystem is not installed.
|
|
80
117
|
*/
|
|
81
118
|
getCursors(tenantId?: string | null): Promise<CursorSnapshot[]>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Paginated, filterable `job_run` list (OBS-LIST-1). Delegates to
|
|
122
|
+
* `IJobRunService.listJobRuns`. Keyset pagination on `created_at`.
|
|
123
|
+
*
|
|
124
|
+
* Returns an empty page (`{ items: [], nextCursor: null }`) when the jobs
|
|
125
|
+
* subsystem is not installed.
|
|
126
|
+
*/
|
|
127
|
+
listJobRuns(query?: ListJobRunsQuery): Promise<JobRunPage>;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Paginated, filterable `domain_events` list (OBS-LIST-1). Delegates to
|
|
131
|
+
* `IEventReadPort.listEvents`. Keyset pagination on `occurred_at`.
|
|
132
|
+
*
|
|
133
|
+
* Returns an empty page when the events read port is not installed (e.g.
|
|
134
|
+
* the events subsystem is absent, or its backend is `redis` which retains
|
|
135
|
+
* no history).
|
|
136
|
+
*/
|
|
137
|
+
listEvents(query?: ListEventsQuery): Promise<EventPage>;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Stitch the job runs and domain events sharing a `rootRunId` into a
|
|
141
|
+
* single ascending timeline + summary (OBS-LIST-1). Composes
|
|
142
|
+
* `IJobRunService.listJobRuns` (filtered by the run tree) and
|
|
143
|
+
* `IEventReadPort.listEvents({ rootRunId })`.
|
|
144
|
+
*
|
|
145
|
+
* Returns an empty timeline (zero counts, null bounds) when neither the
|
|
146
|
+
* jobs subsystem nor the events read port is installed.
|
|
147
|
+
*/
|
|
148
|
+
getCorrelationTimeline(
|
|
149
|
+
rootRunId: string,
|
|
150
|
+
tenantId?: string | null,
|
|
151
|
+
): Promise<CorrelationTimeline>;
|
|
82
152
|
}
|
|
83
153
|
|
|
84
154
|
// Re-export composed return types so consumers of IObservability can import
|
|
@@ -86,6 +156,12 @@ export interface IObservability {
|
|
|
86
156
|
export type {
|
|
87
157
|
PoolStatusCount,
|
|
88
158
|
JobRunFailure,
|
|
159
|
+
JobRunSummary,
|
|
160
|
+
JobRunPage,
|
|
161
|
+
ListJobRunsQuery,
|
|
162
|
+
EventSummary,
|
|
163
|
+
EventPage,
|
|
164
|
+
ListEventsQuery,
|
|
89
165
|
StatusHistogram,
|
|
90
166
|
SyncRunSummary,
|
|
91
167
|
CursorSnapshot,
|
|
@@ -33,9 +33,20 @@ import { JOB_RUN_SERVICE } from '../jobs/jobs-domain.tokens';
|
|
|
33
33
|
import type {
|
|
34
34
|
IJobRunService,
|
|
35
35
|
JobRunFailure,
|
|
36
|
+
JobRunPage,
|
|
37
|
+
JobRunSummary,
|
|
38
|
+
ListJobRunsQuery,
|
|
36
39
|
PoolStatusCount,
|
|
37
40
|
} from '../jobs/job-run-service.protocol';
|
|
38
41
|
|
|
42
|
+
import { EVENT_READ_PORT } from '../events/events.tokens';
|
|
43
|
+
import type {
|
|
44
|
+
EventPage,
|
|
45
|
+
EventSummary,
|
|
46
|
+
IEventReadPort,
|
|
47
|
+
ListEventsQuery,
|
|
48
|
+
} from '../events/event-read.protocol';
|
|
49
|
+
|
|
39
50
|
import { BRIDGE_DELIVERY_REPO } from '../bridge/bridge.tokens';
|
|
40
51
|
import type { IJobBridge, StatusHistogram } from '../bridge/bridge.protocol';
|
|
41
52
|
|
|
@@ -49,7 +60,19 @@ import type {
|
|
|
49
60
|
ICursorStore,
|
|
50
61
|
} from '../sync/sync-cursor-store.protocol';
|
|
51
62
|
|
|
52
|
-
import type {
|
|
63
|
+
import type {
|
|
64
|
+
CorrelationTimeline,
|
|
65
|
+
CorrelationTimelineEntry,
|
|
66
|
+
IObservability,
|
|
67
|
+
} from './observability.protocol';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Safety bound on how many pages the correlation timeline will walk when
|
|
71
|
+
* draining a sibling port. A single run tree producing more than
|
|
72
|
+
* 50 pages × default page size of correlated rows is pathological; cap to
|
|
73
|
+
* keep the stitch bounded rather than unbounded-loop on bad data.
|
|
74
|
+
*/
|
|
75
|
+
const MAX_TIMELINE_PAGES = 50;
|
|
53
76
|
|
|
54
77
|
@Injectable()
|
|
55
78
|
export class ObservabilityService implements IObservability {
|
|
@@ -65,6 +88,16 @@ export class ObservabilityService implements IObservability {
|
|
|
65
88
|
failed: 0,
|
|
66
89
|
};
|
|
67
90
|
|
|
91
|
+
/** Empty page used when a sibling read port is absent. */
|
|
92
|
+
private static readonly EMPTY_JOB_RUN_PAGE: JobRunPage = {
|
|
93
|
+
items: [],
|
|
94
|
+
nextCursor: null,
|
|
95
|
+
};
|
|
96
|
+
private static readonly EMPTY_EVENT_PAGE: EventPage = {
|
|
97
|
+
items: [],
|
|
98
|
+
nextCursor: null,
|
|
99
|
+
};
|
|
100
|
+
|
|
68
101
|
constructor(
|
|
69
102
|
@Optional()
|
|
70
103
|
@Inject(JOB_RUN_SERVICE)
|
|
@@ -78,6 +111,9 @@ export class ObservabilityService implements IObservability {
|
|
|
78
111
|
@Optional()
|
|
79
112
|
@Inject(SYNC_CURSOR_STORE)
|
|
80
113
|
private readonly cursors?: ICursorStore,
|
|
114
|
+
@Optional()
|
|
115
|
+
@Inject(EVENT_READ_PORT)
|
|
116
|
+
private readonly events?: IEventReadPort | null,
|
|
81
117
|
) {}
|
|
82
118
|
|
|
83
119
|
async getPoolDepths(tenantId?: string | null): Promise<PoolStatusCount[]> {
|
|
@@ -114,4 +150,115 @@ export class ObservabilityService implements IObservability {
|
|
|
114
150
|
if (!this.cursors) return [];
|
|
115
151
|
return this.cursors.listAll(tenantId);
|
|
116
152
|
}
|
|
153
|
+
|
|
154
|
+
async listJobRuns(query?: ListJobRunsQuery): Promise<JobRunPage> {
|
|
155
|
+
if (!this.jobRuns) {
|
|
156
|
+
return { ...ObservabilityService.EMPTY_JOB_RUN_PAGE };
|
|
157
|
+
}
|
|
158
|
+
return this.jobRuns.listJobRuns(query);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async listEvents(query?: ListEventsQuery): Promise<EventPage> {
|
|
162
|
+
if (!this.events) {
|
|
163
|
+
return { ...ObservabilityService.EMPTY_EVENT_PAGE };
|
|
164
|
+
}
|
|
165
|
+
return this.events.listEvents(query);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async getCorrelationTimeline(
|
|
169
|
+
rootRunId: string,
|
|
170
|
+
tenantId?: string | null,
|
|
171
|
+
): Promise<CorrelationTimeline> {
|
|
172
|
+
const runs = await this.collectRuns(rootRunId, tenantId);
|
|
173
|
+
const events = await this.collectEvents(rootRunId, tenantId);
|
|
174
|
+
|
|
175
|
+
const entries: CorrelationTimelineEntry[] = [
|
|
176
|
+
...runs.map(
|
|
177
|
+
(run): CorrelationTimelineEntry => ({
|
|
178
|
+
kind: 'job_run',
|
|
179
|
+
at: run.createdAt,
|
|
180
|
+
run,
|
|
181
|
+
}),
|
|
182
|
+
),
|
|
183
|
+
...events.map(
|
|
184
|
+
(event): CorrelationTimelineEntry => ({
|
|
185
|
+
kind: 'event',
|
|
186
|
+
at: event.occurredAt,
|
|
187
|
+
event,
|
|
188
|
+
}),
|
|
189
|
+
),
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
// Ascending chronological order. Stable tie-break: job runs before
|
|
193
|
+
// events at the same instant (the run that emits an event precedes it).
|
|
194
|
+
entries.sort((a, b) => {
|
|
195
|
+
const dt = a.at.getTime() - b.at.getTime();
|
|
196
|
+
if (dt !== 0) return dt;
|
|
197
|
+
if (a.kind === b.kind) return 0;
|
|
198
|
+
return a.kind === 'job_run' ? -1 : 1;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const startedAt = entries.length > 0 ? entries[0]!.at : null;
|
|
202
|
+
const lastActivityAt =
|
|
203
|
+
entries.length > 0 ? entries[entries.length - 1]!.at : null;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
rootRunId,
|
|
207
|
+
entries,
|
|
208
|
+
summary: {
|
|
209
|
+
runCount: runs.length,
|
|
210
|
+
eventCount: events.length,
|
|
211
|
+
startedAt,
|
|
212
|
+
lastActivityAt,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Drain every `job_run` sharing `rootRunId` by walking the keyset cursor.
|
|
219
|
+
* Empty when the jobs subsystem is absent.
|
|
220
|
+
*/
|
|
221
|
+
private async collectRuns(
|
|
222
|
+
rootRunId: string,
|
|
223
|
+
tenantId?: string | null,
|
|
224
|
+
): Promise<JobRunSummary[]> {
|
|
225
|
+
if (!this.jobRuns) return [];
|
|
226
|
+
const out: JobRunSummary[] = [];
|
|
227
|
+
let cursor: string | undefined;
|
|
228
|
+
for (let page = 0; page < MAX_TIMELINE_PAGES; page += 1) {
|
|
229
|
+
const result = await this.jobRuns.listJobRuns({
|
|
230
|
+
rootRunId,
|
|
231
|
+
tenantId,
|
|
232
|
+
cursor,
|
|
233
|
+
});
|
|
234
|
+
out.push(...result.items);
|
|
235
|
+
if (!result.nextCursor) break;
|
|
236
|
+
cursor = result.nextCursor;
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Drain every `domain_event` whose `metadata.rootRunId` matches by walking
|
|
243
|
+
* the keyset cursor. Empty when the events read port is absent.
|
|
244
|
+
*/
|
|
245
|
+
private async collectEvents(
|
|
246
|
+
rootRunId: string,
|
|
247
|
+
tenantId?: string | null,
|
|
248
|
+
): Promise<EventSummary[]> {
|
|
249
|
+
if (!this.events) return [];
|
|
250
|
+
const out: EventSummary[] = [];
|
|
251
|
+
let cursor: string | undefined;
|
|
252
|
+
for (let page = 0; page < MAX_TIMELINE_PAGES; page += 1) {
|
|
253
|
+
const result = await this.events.listEvents({
|
|
254
|
+
rootRunId,
|
|
255
|
+
tenantId,
|
|
256
|
+
cursor,
|
|
257
|
+
});
|
|
258
|
+
out.push(...result.items);
|
|
259
|
+
if (!result.nextCursor) break;
|
|
260
|
+
cursor = result.nextCursor;
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
117
264
|
}
|
|
@@ -495,28 +495,30 @@ function collectDrizzleImports(processedFields, belongsTo, hasTimestamps, hasSof
|
|
|
495
495
|
function zodChainForCreate(field) {
|
|
496
496
|
const { type, nullable, required, hasDefault, hasChoices, choices } = field;
|
|
497
497
|
|
|
498
|
+
// Apply nullability and optionality INDEPENDENTLY. A nullable column accepts
|
|
499
|
+
// null (`.nullable()`); a field without `required: true` may be omitted from
|
|
500
|
+
// the create payload (`.optional()`). A field that is both gets
|
|
501
|
+
// `.nullable().optional()`. Previously the `nullable` branch returned early,
|
|
502
|
+
// so a nullable-and-optional field never got `.optional()` — forcing callers
|
|
503
|
+
// to send an explicit `null` for every optional column (e.g. POST /accounts
|
|
504
|
+
// rejecting a body that omits `domain`/`industry`).
|
|
498
505
|
if (hasChoices) {
|
|
499
|
-
|
|
500
|
-
if (
|
|
501
|
-
if (
|
|
506
|
+
let base = `z.enum([${choices.map((c) => `'${c}'`).join(', ')}])`;
|
|
507
|
+
if (nullable) base += '.nullable()';
|
|
508
|
+
if (!required) base += '.optional()';
|
|
502
509
|
return base;
|
|
503
510
|
}
|
|
504
511
|
|
|
505
512
|
let base = ZOD_TYPE_MAP[type] || 'z.unknown()';
|
|
506
513
|
|
|
507
514
|
if (type === 'boolean' && hasDefault) {
|
|
515
|
+
// `.default()` already makes the input optional in Zod.
|
|
508
516
|
base += `.default(${field.default ?? false})`;
|
|
509
517
|
return base;
|
|
510
518
|
}
|
|
511
519
|
|
|
512
|
-
if (nullable)
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (!required) {
|
|
517
|
-
return base + '.optional()';
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
+
if (nullable) base += '.nullable()';
|
|
521
|
+
if (!required) base += '.optional()';
|
|
520
522
|
return base;
|
|
521
523
|
}
|
|
522
524
|
|
|
@@ -1132,7 +1134,7 @@ export function buildCleanLitePsLocals(definition, baseLocals) {
|
|
|
1132
1134
|
// FK fields from belongs_to for create/output DTOs
|
|
1133
1135
|
const belongsToFkFields = belongsTo.map((rel) => ({
|
|
1134
1136
|
camelName: rel.camelField,
|
|
1135
|
-
zodChainCreate: rel.nullable ? 'z.string().uuid().nullable()' : 'z.string().uuid()',
|
|
1137
|
+
zodChainCreate: rel.nullable ? 'z.string().uuid().nullable().optional()' : 'z.string().uuid()',
|
|
1136
1138
|
zodChainOutput: rel.nullable ? 'z.string().uuid().nullable()' : 'z.string().uuid()',
|
|
1137
1139
|
nullable: rel.nullable,
|
|
1138
1140
|
}));
|