@pattern-stack/codegen 0.4.1 → 0.4.3
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 +6 -0
- package/dist/runtime/subsystems/bridge/bridge.module.d.ts +1 -0
- package/dist/runtime/subsystems/bridge/bridge.module.js +38 -21
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.d.ts +1 -0
- package/dist/runtime/subsystems/bridge/index.js +29 -12
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/index.js +31 -14
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/index.d.ts +1 -0
- package/dist/runtime/subsystems/jobs/index.js +27 -10
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +3 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +9 -4
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.d.ts +3 -1
- package/dist/runtime/subsystems/jobs/job-worker.js +6 -2
- package/dist/runtime/subsystems/jobs/job-worker.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +3 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.js +27 -10
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +9 -4
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/src/cli/index.js +29 -2
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +2 -1
- package/runtime/analytics/index.ts +31 -0
- package/runtime/analytics/metrics.ts +85 -0
- package/runtime/analytics/packs/crm-entity-measures.ts +20 -0
- package/runtime/analytics/packs/index.ts +5 -0
- package/runtime/analytics/packs/monetary-measures.ts +20 -0
- package/runtime/analytics/specs.ts +54 -0
- package/runtime/analytics/types.ts +105 -0
- package/runtime/base-classes/activity-entity-repository.ts +50 -0
- package/runtime/base-classes/activity-entity-service.ts +48 -0
- package/runtime/base-classes/base-read-use-cases.ts +88 -0
- package/runtime/base-classes/base-repository.ts +289 -0
- package/runtime/base-classes/base-service.ts +183 -0
- package/runtime/base-classes/index.ts +38 -0
- package/runtime/base-classes/knowledge-entity-repository.ts +12 -0
- package/runtime/base-classes/knowledge-entity-service.ts +14 -0
- package/runtime/base-classes/lifecycle-events.ts +152 -0
- package/runtime/base-classes/metadata-entity-repository.ts +80 -0
- package/runtime/base-classes/metadata-entity-service.ts +48 -0
- package/runtime/base-classes/synced-entity-repository.ts +57 -0
- package/runtime/base-classes/synced-entity-service.ts +50 -0
- package/runtime/base-classes/with-analytics.ts +22 -0
- package/runtime/constants/tokens.ts +29 -0
- package/runtime/eav-helpers.ts +74 -0
- package/runtime/pipes/zod-validation.pipe.ts +64 -0
- package/runtime/shared/openapi/error-response.dto.ts +24 -0
- package/runtime/shared/openapi/errors.ts +39 -0
- package/runtime/shared/openapi/index.ts +20 -0
- package/runtime/shared/openapi/registry.tokens.ts +13 -0
- package/runtime/shared/openapi/registry.ts +151 -0
- package/runtime/subsystems/analytics/analytics-query.protocol.ts +37 -0
- package/runtime/subsystems/analytics/analytics.module.ts +64 -0
- package/runtime/subsystems/analytics/analytics.tokens.ts +24 -0
- package/runtime/subsystems/analytics/cube-backend.ts +75 -0
- package/runtime/subsystems/analytics/index.ts +15 -0
- package/runtime/subsystems/analytics/noop-backend.ts +27 -0
- package/runtime/subsystems/auth/auth.module.ts +91 -0
- package/runtime/subsystems/auth/auth.tokens.ts +27 -0
- package/runtime/subsystems/auth/backends/encryption-key/env.ts +76 -0
- package/runtime/subsystems/auth/backends/oauth-state-store/in-memory.ts +42 -0
- package/runtime/subsystems/auth/index.ts +77 -0
- package/runtime/subsystems/auth/protocols/auth-strategy.ts +46 -0
- package/runtime/subsystems/auth/protocols/encryption-key.ts +21 -0
- package/runtime/subsystems/auth/protocols/integration-store.ts +66 -0
- package/runtime/subsystems/auth/protocols/oauth-state-store.ts +16 -0
- package/runtime/subsystems/auth/runtime/integration-broken.error.ts +21 -0
- package/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts +189 -0
- package/runtime/subsystems/auth/runtime/session-expired.error.ts +39 -0
- package/runtime/subsystems/auth/runtime/with-auth-retry.ts +50 -0
- package/runtime/subsystems/bridge/assert-tenant-id.ts +57 -0
- package/runtime/subsystems/bridge/bridge-delivery-handler.ts +220 -0
- package/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.ts +149 -0
- package/runtime/subsystems/bridge/bridge-delivery.memory-backend.ts +140 -0
- package/runtime/subsystems/bridge/bridge-delivery.schema.ts +142 -0
- package/runtime/subsystems/bridge/bridge-errors.ts +112 -0
- package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +175 -0
- package/runtime/subsystems/bridge/bridge.module.ts +160 -0
- package/runtime/subsystems/bridge/bridge.protocol.ts +351 -0
- package/runtime/subsystems/bridge/bridge.tokens.ts +68 -0
- package/runtime/subsystems/bridge/event-flow.service.ts +175 -0
- package/runtime/subsystems/bridge/generated/.gitkeep +0 -0
- package/runtime/subsystems/bridge/generated/registry.ts +6 -0
- package/runtime/subsystems/bridge/index.ts +84 -0
- package/runtime/subsystems/bridge/reserved-pools.ts +36 -0
- package/runtime/subsystems/cache/cache.drizzle-backend.ts +150 -0
- package/runtime/subsystems/cache/cache.memory-backend.ts +116 -0
- package/runtime/subsystems/cache/cache.module.ts +115 -0
- package/runtime/subsystems/cache/cache.protocol.ts +45 -0
- package/runtime/subsystems/cache/cache.schema.ts +27 -0
- package/runtime/subsystems/cache/cache.tokens.ts +17 -0
- package/runtime/subsystems/cache/index.ts +22 -0
- package/runtime/subsystems/events/domain-events.schema.ts +77 -0
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +327 -0
- package/runtime/subsystems/events/event-bus.memory-backend.ts +142 -0
- package/runtime/subsystems/events/event-bus.protocol.ts +86 -0
- package/runtime/subsystems/events/event-bus.redis-backend.ts +304 -0
- package/runtime/subsystems/events/events-errors.ts +30 -0
- package/runtime/subsystems/events/events.module.ts +230 -0
- package/runtime/subsystems/events/events.tokens.ts +62 -0
- package/runtime/subsystems/events/generated/bus.ts +103 -0
- package/runtime/subsystems/events/generated/index.ts +7 -0
- package/runtime/subsystems/events/generated/registry.ts +84 -0
- package/runtime/subsystems/events/generated/schemas.ts +59 -0
- package/runtime/subsystems/events/generated/types.ts +94 -0
- package/runtime/subsystems/events/index.ts +21 -0
- package/runtime/subsystems/index.ts +63 -0
- package/runtime/subsystems/jobs/generated/job-orchestration.schema.multi-tenant.ts +217 -0
- package/runtime/subsystems/jobs/generated/job-orchestration.schema.single-tenant.ts +217 -0
- package/runtime/subsystems/jobs/generated/scope-entity-type.ts +10 -0
- package/runtime/subsystems/jobs/index.ts +120 -0
- package/runtime/subsystems/jobs/job-handler.base.ts +206 -0
- package/runtime/subsystems/jobs/job-orchestration.schema.ts +217 -0
- package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +536 -0
- package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +860 -0
- package/runtime/subsystems/jobs/job-orchestrator.protocol.ts +179 -0
- package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +171 -0
- package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +165 -0
- package/runtime/subsystems/jobs/job-run-service.protocol.ts +79 -0
- package/runtime/subsystems/jobs/job-step-service.drizzle-backend.ts +66 -0
- package/runtime/subsystems/jobs/job-step-service.memory-backend.ts +119 -0
- package/runtime/subsystems/jobs/job-step-service.protocol.ts +53 -0
- package/runtime/subsystems/jobs/job-worker.module.ts +312 -0
- package/runtime/subsystems/jobs/job-worker.ts +624 -0
- package/runtime/subsystems/jobs/jobs-domain.module.ts +119 -0
- package/runtime/subsystems/jobs/jobs-domain.tokens.ts +30 -0
- package/runtime/subsystems/jobs/jobs-errors.ts +150 -0
- package/runtime/subsystems/jobs/memory-job-store.ts +35 -0
- package/runtime/subsystems/jobs/pool-config.loader.ts +218 -0
- package/runtime/subsystems/storage/index.ts +18 -0
- package/runtime/subsystems/storage/storage.local-backend.ts +113 -0
- package/runtime/subsystems/storage/storage.memory-backend.ts +78 -0
- package/runtime/subsystems/storage/storage.module.ts +60 -0
- package/runtime/subsystems/storage/storage.protocol.ts +78 -0
- package/runtime/subsystems/storage/storage.tokens.ts +9 -0
- package/runtime/subsystems/storage/storage.utils.ts +20 -0
- package/runtime/subsystems/sync/deep-equal.differ.ts +198 -0
- package/runtime/subsystems/sync/execute-sync.use-case.ts +334 -0
- package/runtime/subsystems/sync/index.ts +98 -0
- package/runtime/subsystems/sync/sync-audit.schema.ts +300 -0
- package/runtime/subsystems/sync/sync-change-source.protocol.ts +99 -0
- package/runtime/subsystems/sync/sync-cursor-store.drizzle-backend.ts +104 -0
- package/runtime/subsystems/sync/sync-cursor-store.memory-backend.ts +64 -0
- package/runtime/subsystems/sync/sync-cursor-store.protocol.ts +53 -0
- package/runtime/subsystems/sync/sync-errors.ts +54 -0
- package/runtime/subsystems/sync/sync-field-diff.protocol.ts +61 -0
- package/runtime/subsystems/sync/sync-loopback.protocol.ts +33 -0
- package/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.ts +123 -0
- package/runtime/subsystems/sync/sync-run-recorder.memory-backend.ts +143 -0
- package/runtime/subsystems/sync/sync-run-recorder.protocol.ts +86 -0
- package/runtime/subsystems/sync/sync-sink.protocol.ts +55 -0
- package/runtime/subsystems/sync/sync.module.ts +156 -0
- package/runtime/subsystems/sync/sync.tokens.ts +57 -0
- package/runtime/types/drizzle.ts +23 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IJobOrchestrator — the primary port for the job orchestration domain
|
|
3
|
+
* (ADR-022, JOB-2).
|
|
4
|
+
*
|
|
5
|
+
* Consumers (use cases, event subscribers) inject this via
|
|
6
|
+
* `@Inject(JOB_ORCHESTRATOR)` and call `start` / `cancel` / `replay`.
|
|
7
|
+
* Concrete backends (JOB-3 Drizzle, JOB-4 Memory) satisfy this contract.
|
|
8
|
+
*
|
|
9
|
+
* Single-layer architecture reminder: there is no `IJobQueue` executor port.
|
|
10
|
+
* The orchestrator writes `job_run` rows directly; the `JobWorker` of JOB-3
|
|
11
|
+
* polls `job_run` via `SELECT ... FOR UPDATE SKIP LOCKED`.
|
|
12
|
+
*/
|
|
13
|
+
import type { JobRunRow } from './job-orchestration.schema';
|
|
14
|
+
import type { JobHandlerMeta, ParentClosePolicy } from './job-handler.base';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Public return type for orchestrator reads. Re-exported as `JobRun` so
|
|
18
|
+
* protocols and consumer code don't import the raw Drizzle row name.
|
|
19
|
+
*/
|
|
20
|
+
export type JobRun = JobRunRow;
|
|
21
|
+
|
|
22
|
+
export interface StartOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Optional scope attachment. `listForScope` queries use this pair; the
|
|
25
|
+
* column is free-text (no CHECK constraint) — type safety for `entityType`
|
|
26
|
+
* lives at the TS layer via JOB-7's generated `ScopeEntityType` union.
|
|
27
|
+
*/
|
|
28
|
+
scope?: { entityType: string; entityId: string };
|
|
29
|
+
|
|
30
|
+
/** Overrides the pool declared in `@JobHandler({ pool })` metadata. */
|
|
31
|
+
pool?: string;
|
|
32
|
+
|
|
33
|
+
/** Schedule the run. When omitted, run as soon as the worker claims it. */
|
|
34
|
+
runAt?: Date;
|
|
35
|
+
|
|
36
|
+
/** 0 = default; higher values claimed first by `ORDER BY priority DESC`. */
|
|
37
|
+
priority?: number;
|
|
38
|
+
|
|
39
|
+
/** Free-form routing/audit tags. Persisted on `job_run.tags`. */
|
|
40
|
+
tags?: Record<string, string>;
|
|
41
|
+
|
|
42
|
+
/** Must align with `triggerSourceEnum` values landed in JOB-1. */
|
|
43
|
+
triggerSource?: 'manual' | 'schedule' | 'event' | 'parent';
|
|
44
|
+
|
|
45
|
+
/** Optional reference to the triggering event, schedule, etc. */
|
|
46
|
+
triggerRef?: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What happens to this run's children if this run reaches a terminal
|
|
50
|
+
* state. Stored on the child at spawn time; see `ParentClosePolicy`.
|
|
51
|
+
*/
|
|
52
|
+
parentClosePolicy?: ParentClosePolicy;
|
|
53
|
+
|
|
54
|
+
/** Internal — set by `ctx.spawnChild`. User code should not pass this. */
|
|
55
|
+
parentRunId?: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Multi-tenancy opt-in (JOB-8). When `JobsDomainModule` is configured
|
|
59
|
+
* with `multiTenant: true`, this field is required:
|
|
60
|
+
* - `string` — tenant the run belongs to (written to `job_run.tenant_id`).
|
|
61
|
+
* - `null` — cross-tenant background work; row persisted with NULL.
|
|
62
|
+
* - `undefined` — throws `MissingTenantIdError` at the backend.
|
|
63
|
+
* When `multiTenant: false`, the field is ignored and the column is
|
|
64
|
+
* always written as `NULL`.
|
|
65
|
+
*/
|
|
66
|
+
tenantId?: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CancelOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Conceptually defaults to `true` for root cancellation — cascading via
|
|
72
|
+
* `root_run_id` is the expected behaviour when an operator cancels a
|
|
73
|
+
* run. Backends in JOB-3/JOB-4 implement the default; callers passing
|
|
74
|
+
* `false` opt into "cancel only this node, leave descendants".
|
|
75
|
+
*/
|
|
76
|
+
cascade?: boolean;
|
|
77
|
+
reason?: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Multi-tenancy gate (JOB-8). When `multiTenant: true`, the backend
|
|
81
|
+
* additionally filters `WHERE tenant_id = :tenantId` — cancelling a run
|
|
82
|
+
* that belongs to a different tenant is a **no-op** (not an error), so
|
|
83
|
+
* cross-tenant cancellation attempts are silent rather than leaking
|
|
84
|
+
* existence information. `undefined` throws `MissingTenantIdError`;
|
|
85
|
+
* explicit `null` matches `tenant_id IS NULL` rows.
|
|
86
|
+
*/
|
|
87
|
+
tenantId?: string | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Boot-time upsert payload — one entry per registered `@JobHandler` class.
|
|
92
|
+
* Constructed by `JobWorkerModule.onModuleInit` from `HandlerRegistry.getAll()`
|
|
93
|
+
* and handed to the orchestrator so each backend can persist `job` definitions
|
|
94
|
+
* in whatever way it stores them (Drizzle: `ON CONFLICT (type) DO UPDATE`
|
|
95
|
+
* gated by metadata content; memory: populate `MemoryJobStore.jobs`).
|
|
96
|
+
*/
|
|
97
|
+
export interface JobUpsertEntry {
|
|
98
|
+
type: string;
|
|
99
|
+
meta: JobHandlerMeta<unknown>;
|
|
100
|
+
/**
|
|
101
|
+
* Handler class constructor — the memory backend keeps a reference for
|
|
102
|
+
* `tick()` execution. Drizzle backend ignores this (worker resolves the
|
|
103
|
+
* class via `JOB_HANDLER_REGISTRY` at claim time).
|
|
104
|
+
*/
|
|
105
|
+
handlerClass: new (...args: unknown[]) => unknown;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Pool definition surface as the orchestrator needs it for boot-time row
|
|
110
|
+
* materialisation. Defined locally here (not imported from
|
|
111
|
+
* `pool-config.loader.ts`) so the protocol layer keeps zero dependencies on
|
|
112
|
+
* runtime config wiring — the loader's `PoolDefinition` is structurally
|
|
113
|
+
* compatible.
|
|
114
|
+
*/
|
|
115
|
+
export interface JobPoolDef {
|
|
116
|
+
queue: string;
|
|
117
|
+
concurrency: number;
|
|
118
|
+
reserved: boolean;
|
|
119
|
+
description?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface IJobOrchestrator {
|
|
123
|
+
/**
|
|
124
|
+
* Create a `pending` `job_run` row and return it. Does NOT block waiting
|
|
125
|
+
* for the worker to pick the run up; consumers that need completion
|
|
126
|
+
* semantics should subscribe to the emitted completion event.
|
|
127
|
+
*
|
|
128
|
+
* **Optional `tx` last-arg** (added 2026-04-22 for ADR-023 BRIDGE-7):
|
|
129
|
+
* pass an in-flight Drizzle transaction to thread the row insert + any
|
|
130
|
+
* dedupe/collision lookups onto an existing tx. Used by
|
|
131
|
+
* `EventFlowService.publishAndStart` to bundle the outbox insert,
|
|
132
|
+
* eager `job_run`, and `bridge_delivery` Case-B pre-write into one
|
|
133
|
+
* atomic transaction. Memory backend ignores the parameter (its
|
|
134
|
+
* "transaction" is a process-wide mutex). Drizzle backend uses the
|
|
135
|
+
* standard `tx ?? this.db` pattern.
|
|
136
|
+
*/
|
|
137
|
+
start(
|
|
138
|
+
type: string,
|
|
139
|
+
input: unknown,
|
|
140
|
+
opts?: StartOptions,
|
|
141
|
+
tx?: import('../events/event-bus.protocol').DrizzleTransaction,
|
|
142
|
+
): Promise<JobRun>;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Cancel a run (and, by default, its entire root-run subtree). Idempotent
|
|
146
|
+
* — cancelling an already-terminal run is a no-op.
|
|
147
|
+
*/
|
|
148
|
+
cancel(runId: string, opts?: CancelOptions): Promise<void>;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Re-run from the policy declared in `@JobHandler({ replayFrom })`.
|
|
152
|
+
* Returns the new `job_run` row (replay always spawns a fresh row —
|
|
153
|
+
* the original is preserved for audit).
|
|
154
|
+
*/
|
|
155
|
+
replay(runId: string): Promise<JobRun>;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Boot-time materialisation of `job` definitions from `@JobHandler`
|
|
159
|
+
* metadata. Called once per process by `JobWorkerModule.onModuleInit`.
|
|
160
|
+
*
|
|
161
|
+
* Drizzle backend: hash-gated `INSERT … ON CONFLICT (type) DO UPDATE …
|
|
162
|
+
* WHERE` (Q3 resolution 2026-04-19). The `UPDATE` branch executes only
|
|
163
|
+
* when one of the persisted metadata fields differs from the incoming
|
|
164
|
+
* payload; `version` bumps only on a real change; concurrent boots with
|
|
165
|
+
* identical content are idempotent no-ops.
|
|
166
|
+
*
|
|
167
|
+
* Memory backend: populates `MemoryJobStore.jobs` and the in-process
|
|
168
|
+
* handler-class registry consumed by `MemoryJobOrchestrator.tick`.
|
|
169
|
+
*
|
|
170
|
+
* Returns the orphaned types — types present in DB but absent from
|
|
171
|
+
* `entries`. The caller (boot validator) decides whether to throw or
|
|
172
|
+
* warn. Memory backend always returns `[]` (Q4 resolution 2026-04-19 —
|
|
173
|
+
* validator skipped in memory mode).
|
|
174
|
+
*/
|
|
175
|
+
upsertJobRows(
|
|
176
|
+
entries: JobUpsertEntry[],
|
|
177
|
+
poolConfig: ReadonlyMap<string, JobPoolDef>,
|
|
178
|
+
): Promise<{ orphaned: string[] }>;
|
|
179
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DrizzleJobRunService — scope-oriented reads and bulk operations against
|
|
3
|
+
* `job_run` (ADR-022, JOB-3).
|
|
4
|
+
*
|
|
5
|
+
* Separate from the orchestrator because the access pattern differs: this
|
|
6
|
+
* service scans by `(scope_entity_type, scope_entity_id)` via
|
|
7
|
+
* `idx_job_run_scope`, whereas orchestrator mutates individual runs by id.
|
|
8
|
+
*/
|
|
9
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
10
|
+
import { and, asc, desc, eq, inArray, isNull } from 'drizzle-orm';
|
|
11
|
+
import type { DrizzleClient } from '../../types/drizzle';
|
|
12
|
+
import { DRIZZLE } from '../../constants/tokens';
|
|
13
|
+
import { jobRuns, type JobRunRow } from './job-orchestration.schema';
|
|
14
|
+
import type { JobRun } from './job-orchestrator.protocol';
|
|
15
|
+
import type {
|
|
16
|
+
IJobRunService,
|
|
17
|
+
ListForScopeOptions,
|
|
18
|
+
CancelForScopeOptions,
|
|
19
|
+
RescheduleForScopeOptions,
|
|
20
|
+
} from './job-run-service.protocol';
|
|
21
|
+
import type { IJobOrchestrator } from './job-orchestrator.protocol';
|
|
22
|
+
import { JOB_ORCHESTRATOR, JOBS_MULTI_TENANT } from './jobs-domain.tokens';
|
|
23
|
+
import { MissingTenantIdError } from './jobs-errors';
|
|
24
|
+
|
|
25
|
+
const NON_TERMINAL_STATUSES: JobRunRow['status'][] = [
|
|
26
|
+
'pending',
|
|
27
|
+
'running',
|
|
28
|
+
'waiting',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
@Injectable()
|
|
32
|
+
export class DrizzleJobRunService implements IJobRunService {
|
|
33
|
+
constructor(
|
|
34
|
+
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
35
|
+
@Inject(JOB_ORCHESTRATOR) private readonly orchestrator: IJobOrchestrator,
|
|
36
|
+
@Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* JOB-8 — produce the tenant WHERE fragment (or `null` to opt out).
|
|
41
|
+
* Returns `null` when multi-tenancy is off (caller skips the predicate).
|
|
42
|
+
* Throws `MissingTenantIdError` when on + `undefined`.
|
|
43
|
+
* When on + explicit `null`, filters `tenant_id IS NULL`.
|
|
44
|
+
*/
|
|
45
|
+
private tenantCondition(
|
|
46
|
+
method: string,
|
|
47
|
+
tenantId: string | null | undefined,
|
|
48
|
+
) {
|
|
49
|
+
if (!this.multiTenant) return null;
|
|
50
|
+
if (tenantId === undefined) throw new MissingTenantIdError(method);
|
|
51
|
+
return tenantId === null
|
|
52
|
+
? isNull(jobRuns.tenantId)
|
|
53
|
+
: eq(jobRuns.tenantId, tenantId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async listForScope(
|
|
57
|
+
entityType: string,
|
|
58
|
+
entityId: string,
|
|
59
|
+
opts: ListForScopeOptions = {},
|
|
60
|
+
): Promise<JobRun[]> {
|
|
61
|
+
const conditions = [
|
|
62
|
+
eq(jobRuns.scopeEntityType, entityType),
|
|
63
|
+
eq(jobRuns.scopeEntityId, entityId),
|
|
64
|
+
];
|
|
65
|
+
const tenantCond = this.tenantCondition('listForScope', opts.tenantId);
|
|
66
|
+
if (tenantCond) conditions.push(tenantCond);
|
|
67
|
+
if (opts.status) {
|
|
68
|
+
if (Array.isArray(opts.status)) {
|
|
69
|
+
conditions.push(inArray(jobRuns.status, opts.status));
|
|
70
|
+
} else {
|
|
71
|
+
conditions.push(eq(jobRuns.status, opts.status));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (opts.jobType) {
|
|
75
|
+
conditions.push(eq(jobRuns.jobType, opts.jobType));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const orderCol = (() => {
|
|
79
|
+
switch (opts.orderBy) {
|
|
80
|
+
case 'created_at asc':
|
|
81
|
+
return asc(jobRuns.createdAt);
|
|
82
|
+
case 'run_at desc':
|
|
83
|
+
return desc(jobRuns.runAt);
|
|
84
|
+
case 'run_at asc':
|
|
85
|
+
return asc(jobRuns.runAt);
|
|
86
|
+
case 'created_at desc':
|
|
87
|
+
default:
|
|
88
|
+
return desc(jobRuns.createdAt);
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
|
|
92
|
+
let q = this.db
|
|
93
|
+
.select()
|
|
94
|
+
.from(jobRuns)
|
|
95
|
+
.where(and(...conditions))
|
|
96
|
+
.orderBy(orderCol)
|
|
97
|
+
.$dynamic();
|
|
98
|
+
|
|
99
|
+
if (typeof opts.limit === 'number') {
|
|
100
|
+
q = q.limit(opts.limit);
|
|
101
|
+
}
|
|
102
|
+
if (typeof opts.offset === 'number') {
|
|
103
|
+
q = q.offset(opts.offset);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const rows = await q;
|
|
107
|
+
return rows as JobRun[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async cancelForScope(
|
|
111
|
+
entityType: string,
|
|
112
|
+
entityId: string,
|
|
113
|
+
opts: CancelForScopeOptions = {},
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
const tenantCond = this.tenantCondition('cancelForScope', opts.tenantId);
|
|
116
|
+
const conditions = [
|
|
117
|
+
eq(jobRuns.scopeEntityType, entityType),
|
|
118
|
+
eq(jobRuns.scopeEntityId, entityId),
|
|
119
|
+
inArray(jobRuns.status, NON_TERMINAL_STATUSES),
|
|
120
|
+
];
|
|
121
|
+
if (tenantCond) conditions.push(tenantCond);
|
|
122
|
+
|
|
123
|
+
const rows = await this.db
|
|
124
|
+
.select({ id: jobRuns.id })
|
|
125
|
+
.from(jobRuns)
|
|
126
|
+
.where(and(...conditions));
|
|
127
|
+
|
|
128
|
+
for (const { id } of rows) {
|
|
129
|
+
// Propagate the tenant gate into cascade-cancel. The scope query has
|
|
130
|
+
// already narrowed to this tenant; passing `tenantId` through keeps
|
|
131
|
+
// the orchestrator's per-row guard consistent under multi-tenant mode.
|
|
132
|
+
await this.orchestrator.cancel(id, {
|
|
133
|
+
cascade: true,
|
|
134
|
+
tenantId: opts.tenantId,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async rescheduleForScope(
|
|
140
|
+
entityType: string,
|
|
141
|
+
entityId: string,
|
|
142
|
+
newRunAt: Date,
|
|
143
|
+
opts: RescheduleForScopeOptions = {},
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const tenantCond = this.tenantCondition('rescheduleForScope', opts.tenantId);
|
|
146
|
+
const conditions = [
|
|
147
|
+
eq(jobRuns.scopeEntityType, entityType),
|
|
148
|
+
eq(jobRuns.scopeEntityId, entityId),
|
|
149
|
+
eq(jobRuns.status, 'pending'),
|
|
150
|
+
];
|
|
151
|
+
if (tenantCond) conditions.push(tenantCond);
|
|
152
|
+
|
|
153
|
+
await this.db
|
|
154
|
+
.update(jobRuns)
|
|
155
|
+
.set({ runAt: newRunAt, updatedAt: new Date() })
|
|
156
|
+
.where(and(...conditions));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Internal helper used by cascade paths (not on the public protocol).
|
|
161
|
+
* Exposed as a public method on the concrete class so infrastructure
|
|
162
|
+
* code (cascade tests, debug tools) can call it without a cast.
|
|
163
|
+
*/
|
|
164
|
+
async findByRootRunId(rootRunId: string): Promise<JobRun[]> {
|
|
165
|
+
const rows = await this.db
|
|
166
|
+
.select()
|
|
167
|
+
.from(jobRuns)
|
|
168
|
+
.where(eq(jobRuns.rootRunId, rootRunId));
|
|
169
|
+
return rows as JobRun[];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryJobRunService — scope-oriented queries and bulk ops over the
|
|
3
|
+
* in-memory run store (ADR-022, JOB-4).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `DrizzleJobRunService` but scans `MemoryJobStore.runs.values()`.
|
|
6
|
+
* Cancel delegates back to the orchestrator so cascade semantics stay in
|
|
7
|
+
* one place.
|
|
8
|
+
*/
|
|
9
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
10
|
+
import type { JobRunRow } from './job-orchestration.schema';
|
|
11
|
+
import type { JobRun } from './job-orchestrator.protocol';
|
|
12
|
+
import type {
|
|
13
|
+
IJobRunService,
|
|
14
|
+
ListForScopeOptions,
|
|
15
|
+
CancelForScopeOptions,
|
|
16
|
+
RescheduleForScopeOptions,
|
|
17
|
+
} from './job-run-service.protocol';
|
|
18
|
+
import type { IJobOrchestrator } from './job-orchestrator.protocol';
|
|
19
|
+
import { JOB_ORCHESTRATOR, JOBS_MULTI_TENANT } from './jobs-domain.tokens';
|
|
20
|
+
import { MissingTenantIdError } from './jobs-errors';
|
|
21
|
+
import { MemoryJobStore } from './memory-job-store';
|
|
22
|
+
|
|
23
|
+
const NON_TERMINAL_STATUSES: JobRunRow['status'][] = [
|
|
24
|
+
'pending',
|
|
25
|
+
'running',
|
|
26
|
+
'waiting',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
@Injectable()
|
|
30
|
+
export class MemoryJobRunService implements IJobRunService {
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly store: MemoryJobStore,
|
|
33
|
+
@Inject(JOB_ORCHESTRATOR) private readonly orchestrator: IJobOrchestrator,
|
|
34
|
+
@Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* JOB-8 — produce a per-row predicate for the tenant gate.
|
|
39
|
+
* Returns `null` when multi-tenancy is off (caller doesn't check).
|
|
40
|
+
* Throws when on + `undefined`; matches `tenant_id IS NULL` on explicit
|
|
41
|
+
* `null` to support cross-tenant background work.
|
|
42
|
+
*/
|
|
43
|
+
private tenantPredicate(
|
|
44
|
+
method: string,
|
|
45
|
+
tenantId: string | null | undefined,
|
|
46
|
+
): ((r: JobRunRow) => boolean) | null {
|
|
47
|
+
if (!this.multiTenant) return null;
|
|
48
|
+
if (tenantId === undefined) throw new MissingTenantIdError(method);
|
|
49
|
+
return (r) => r.tenantId === tenantId;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async listForScope(
|
|
53
|
+
entityType: string,
|
|
54
|
+
entityId: string,
|
|
55
|
+
opts: ListForScopeOptions = {},
|
|
56
|
+
): Promise<JobRun[]> {
|
|
57
|
+
const statusFilter = opts.status
|
|
58
|
+
? Array.isArray(opts.status)
|
|
59
|
+
? new Set(opts.status)
|
|
60
|
+
: new Set([opts.status])
|
|
61
|
+
: null;
|
|
62
|
+
const tenantCheck = this.tenantPredicate('listForScope', opts.tenantId);
|
|
63
|
+
|
|
64
|
+
const rows: JobRunRow[] = [];
|
|
65
|
+
for (const r of this.store.runs.values()) {
|
|
66
|
+
if (r.scopeEntityType !== entityType) continue;
|
|
67
|
+
if (r.scopeEntityId !== entityId) continue;
|
|
68
|
+
if (statusFilter && !statusFilter.has(r.status)) continue;
|
|
69
|
+
if (opts.jobType && r.jobType !== opts.jobType) continue;
|
|
70
|
+
if (tenantCheck && !tenantCheck(r)) continue;
|
|
71
|
+
rows.push(r);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const orderBy = opts.orderBy ?? 'created_at desc';
|
|
75
|
+
rows.sort((a, b) => compareBy(a, b, orderBy));
|
|
76
|
+
|
|
77
|
+
const offset = opts.offset ?? 0;
|
|
78
|
+
const limit = opts.limit;
|
|
79
|
+
const sliced =
|
|
80
|
+
typeof limit === 'number' ? rows.slice(offset, offset + limit) : rows.slice(offset);
|
|
81
|
+
return sliced as JobRun[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async cancelForScope(
|
|
85
|
+
entityType: string,
|
|
86
|
+
entityId: string,
|
|
87
|
+
opts: CancelForScopeOptions = {},
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
const tenantCheck = this.tenantPredicate('cancelForScope', opts.tenantId);
|
|
90
|
+
|
|
91
|
+
const ids: string[] = [];
|
|
92
|
+
for (const r of this.store.runs.values()) {
|
|
93
|
+
if (r.scopeEntityType !== entityType) continue;
|
|
94
|
+
if (r.scopeEntityId !== entityId) continue;
|
|
95
|
+
if (!NON_TERMINAL_STATUSES.includes(r.status)) continue;
|
|
96
|
+
if (tenantCheck && !tenantCheck(r)) continue;
|
|
97
|
+
ids.push(r.id);
|
|
98
|
+
}
|
|
99
|
+
for (const id of ids) {
|
|
100
|
+
// Propagate the tenant gate through the orchestrator's cancel so the
|
|
101
|
+
// internal per-row guard passes (no surprise MissingTenantIdError
|
|
102
|
+
// once the scope query has already narrowed to this tenant).
|
|
103
|
+
await this.orchestrator.cancel(id, {
|
|
104
|
+
cascade: true,
|
|
105
|
+
tenantId: opts.tenantId,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async rescheduleForScope(
|
|
111
|
+
entityType: string,
|
|
112
|
+
entityId: string,
|
|
113
|
+
newRunAt: Date,
|
|
114
|
+
opts: RescheduleForScopeOptions = {},
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const tenantCheck = this.tenantPredicate('rescheduleForScope', opts.tenantId);
|
|
117
|
+
for (const r of this.store.runs.values()) {
|
|
118
|
+
if (r.scopeEntityType !== entityType) continue;
|
|
119
|
+
if (r.scopeEntityId !== entityId) continue;
|
|
120
|
+
if (r.status !== 'pending') continue;
|
|
121
|
+
if (tenantCheck && !tenantCheck(r)) continue;
|
|
122
|
+
this.store.runs.set(r.id, {
|
|
123
|
+
...r,
|
|
124
|
+
runAt: newRunAt,
|
|
125
|
+
updatedAt: new Date(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Direct lookup. Not on the protocol — concrete-class convenience for
|
|
132
|
+
* tests. Matches `DrizzleJobRunService.findByRootRunId` in spirit; both
|
|
133
|
+
* are debug / test helpers that sidestep the orchestrator.
|
|
134
|
+
*/
|
|
135
|
+
findById(runId: string): JobRun | null {
|
|
136
|
+
return (this.store.runs.get(runId) ?? null) as JobRun | null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Public counterpart to the Drizzle backend's `findByRootRunId` helper. */
|
|
140
|
+
findByRootRunId(rootRunId: string): JobRun[] {
|
|
141
|
+
const out: JobRunRow[] = [];
|
|
142
|
+
for (const r of this.store.runs.values()) {
|
|
143
|
+
if (r.rootRunId === rootRunId) out.push(r);
|
|
144
|
+
}
|
|
145
|
+
return out as JobRun[];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function compareBy(
|
|
150
|
+
a: JobRunRow,
|
|
151
|
+
b: JobRunRow,
|
|
152
|
+
order: Exclude<ListForScopeOptions['orderBy'], undefined>,
|
|
153
|
+
): number {
|
|
154
|
+
switch (order) {
|
|
155
|
+
case 'created_at asc':
|
|
156
|
+
return a.createdAt.getTime() - b.createdAt.getTime();
|
|
157
|
+
case 'run_at desc':
|
|
158
|
+
return b.runAt.getTime() - a.runAt.getTime();
|
|
159
|
+
case 'run_at asc':
|
|
160
|
+
return a.runAt.getTime() - b.runAt.getTime();
|
|
161
|
+
case 'created_at desc':
|
|
162
|
+
default:
|
|
163
|
+
return b.createdAt.getTime() - a.createdAt.getTime();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IJobRunService — scope-oriented queries and bulk operations over
|
|
3
|
+
* `job_run` rows (ADR-022, JOB-2).
|
|
4
|
+
*
|
|
5
|
+
* This is a separate port from `IJobOrchestrator` because the access
|
|
6
|
+
* pattern is different: orchestrator mutates individual runs by id;
|
|
7
|
+
* run service scans by `(scope_entity_type, scope_entity_id)`.
|
|
8
|
+
*/
|
|
9
|
+
import type { JobRun } from './job-orchestrator.protocol';
|
|
10
|
+
|
|
11
|
+
export interface ListForScopeOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Single status or set. Widens to the full `JobRun['status']` union so
|
|
14
|
+
* callers can pass values straight from `jobRunStatusEnum`.
|
|
15
|
+
*/
|
|
16
|
+
status?: JobRun['status'] | JobRun['status'][];
|
|
17
|
+
jobType?: string;
|
|
18
|
+
limit?: number;
|
|
19
|
+
offset?: number;
|
|
20
|
+
orderBy?:
|
|
21
|
+
| 'created_at desc'
|
|
22
|
+
| 'created_at asc'
|
|
23
|
+
| 'run_at desc'
|
|
24
|
+
| 'run_at asc';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Multi-tenancy gate (JOB-8). When `multiTenant: true`, the backend adds
|
|
28
|
+
* `AND tenant_id = :tenantId` to the scope query. `undefined` throws
|
|
29
|
+
* `MissingTenantIdError`; explicit `null` matches `tenant_id IS NULL`
|
|
30
|
+
* rows (cross-tenant background work).
|
|
31
|
+
*/
|
|
32
|
+
tenantId?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* JOB-8 — scoped bulk ops take the same tenant gate as `listForScope`.
|
|
37
|
+
* Added in JOB-8; pre-JOB-8 callers passing nothing continue to compile.
|
|
38
|
+
*/
|
|
39
|
+
export interface CancelForScopeOptions {
|
|
40
|
+
tenantId?: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RescheduleForScopeOptions {
|
|
44
|
+
tenantId?: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface IJobRunService {
|
|
48
|
+
/**
|
|
49
|
+
* Return runs attached to `(entityType, entityId)`. Backed by
|
|
50
|
+
* `idx_job_run_scope` for efficient reads.
|
|
51
|
+
*/
|
|
52
|
+
listForScope(
|
|
53
|
+
entityType: string,
|
|
54
|
+
entityId: string,
|
|
55
|
+
opts?: ListForScopeOptions,
|
|
56
|
+
): Promise<JobRun[]>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Cancel every non-terminal run attached to `(entityType, entityId)`,
|
|
60
|
+
* cascading via `root_run_id`. Used e.g. when an Opportunity is closed
|
|
61
|
+
* and all its background work should stop.
|
|
62
|
+
*/
|
|
63
|
+
cancelForScope(
|
|
64
|
+
entityType: string,
|
|
65
|
+
entityId: string,
|
|
66
|
+
opts?: CancelForScopeOptions,
|
|
67
|
+
): Promise<void>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Push `run_at` forward on every `pending` run attached to the scope.
|
|
71
|
+
* Useful for "pause this account's background work until tomorrow".
|
|
72
|
+
*/
|
|
73
|
+
rescheduleForScope(
|
|
74
|
+
entityType: string,
|
|
75
|
+
entityId: string,
|
|
76
|
+
newRunAt: Date,
|
|
77
|
+
opts?: RescheduleForScopeOptions,
|
|
78
|
+
): Promise<void>;
|
|
79
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DrizzleJobStepService — upsert + lookup on `job_step` for replay-safe
|
|
3
|
+
* memoization (ADR-022, JOB-3).
|
|
4
|
+
*
|
|
5
|
+
* `recordStep` upserts on the `(job_run_id, step_id)` unique index — each
|
|
6
|
+
* step row is written as `running` first, then transitioned to a terminal
|
|
7
|
+
* state (`completed` / `failed` / `skipped`). `findStep` is the hot path
|
|
8
|
+
* that `ctx.step()` consults on every invocation; null on miss.
|
|
9
|
+
*/
|
|
10
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
11
|
+
import { and, eq } from 'drizzle-orm';
|
|
12
|
+
import type { DrizzleClient } from '../../types/drizzle';
|
|
13
|
+
import { DRIZZLE } from '../../constants/tokens';
|
|
14
|
+
import { jobSteps, type JobStepRow } from './job-orchestration.schema';
|
|
15
|
+
import type {
|
|
16
|
+
IJobStepService,
|
|
17
|
+
JobStep,
|
|
18
|
+
RecordStepInput,
|
|
19
|
+
} from './job-step-service.protocol';
|
|
20
|
+
|
|
21
|
+
@Injectable()
|
|
22
|
+
export class DrizzleJobStepService implements IJobStepService {
|
|
23
|
+
constructor(@Inject(DRIZZLE) private readonly db: DrizzleClient) {}
|
|
24
|
+
|
|
25
|
+
async recordStep(input: RecordStepInput): Promise<JobStep> {
|
|
26
|
+
const values = {
|
|
27
|
+
jobRunId: input.jobRunId,
|
|
28
|
+
stepId: input.stepId,
|
|
29
|
+
kind: input.kind,
|
|
30
|
+
seq: input.seq,
|
|
31
|
+
status: input.status,
|
|
32
|
+
input: (input.input ?? null) as Record<string, unknown> | null,
|
|
33
|
+
output: (input.output ?? null) as Record<string, unknown> | null,
|
|
34
|
+
error: input.error ?? null,
|
|
35
|
+
attempts: input.attempts ?? 0,
|
|
36
|
+
startedAt: input.startedAt ?? null,
|
|
37
|
+
finishedAt: input.finishedAt ?? null,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const [row] = await this.db
|
|
41
|
+
.insert(jobSteps)
|
|
42
|
+
.values(values)
|
|
43
|
+
.onConflictDoUpdate({
|
|
44
|
+
target: [jobSteps.jobRunId, jobSteps.stepId],
|
|
45
|
+
set: {
|
|
46
|
+
status: values.status,
|
|
47
|
+
output: values.output,
|
|
48
|
+
error: values.error,
|
|
49
|
+
finishedAt: values.finishedAt,
|
|
50
|
+
attempts: values.attempts,
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
.returning();
|
|
54
|
+
|
|
55
|
+
return row as JobStep;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async findStep(runId: string, stepId: string): Promise<JobStep | null> {
|
|
59
|
+
const [row] = await this.db
|
|
60
|
+
.select()
|
|
61
|
+
.from(jobSteps)
|
|
62
|
+
.where(and(eq(jobSteps.jobRunId, runId), eq(jobSteps.stepId, stepId)))
|
|
63
|
+
.limit(1);
|
|
64
|
+
return ((row as JobStepRow | undefined) ?? null) as JobStep | null;
|
|
65
|
+
}
|
|
66
|
+
}
|