@pattern-stack/codegen 0.4.0 → 0.4.2
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 +14 -0
- package/dist/src/cli/index.js +1616 -1070
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +3 -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 +850 -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 +302 -0
- package/runtime/subsystems/jobs/job-worker.ts +615 -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,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler base class, JobContext, @JobHandler decorator, and policy types
|
|
3
|
+
* for the job orchestration domain (ADR-022, JOB-2).
|
|
4
|
+
*
|
|
5
|
+
* User-authored jobs subclass `JobHandlerBase<TInput, TOutput>` and decorate
|
|
6
|
+
* the class with `@JobHandler<TInput>('job_type', meta)`. The decorator
|
|
7
|
+
* 1. stores metadata via `Reflect.defineMetadata` so Nest's reflector can
|
|
8
|
+
* pick it up at module boot, and
|
|
9
|
+
* 2. populates `JOB_HANDLER_REGISTRY` — a module-singleton map consumed by
|
|
10
|
+
* `JobWorkerModule` (JOB-5) to materialise `job` rows and resolve
|
|
11
|
+
* handler classes during claim/execute.
|
|
12
|
+
*
|
|
13
|
+
* No runtime orchestration lives here; this file is a pure type + decorator
|
|
14
|
+
* surface so downstream PRs (JOB-3..JOB-5) can implement against a stable
|
|
15
|
+
* shape.
|
|
16
|
+
*/
|
|
17
|
+
// TODO(logging-subsystem): swap to ILogger once ADR-028 lands
|
|
18
|
+
import type { Logger } from '@nestjs/common';
|
|
19
|
+
import type { JobRun } from './job-orchestrator.protocol';
|
|
20
|
+
|
|
21
|
+
// ─── ParentClosePolicy ──────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What happens to running child runs when a parent enters a terminal state.
|
|
25
|
+
* Stored on the child at spawn; changes to the parent after spawn do NOT
|
|
26
|
+
* retroactively rewrite children.
|
|
27
|
+
*/
|
|
28
|
+
export enum ParentClosePolicy {
|
|
29
|
+
Terminate = 'terminate',
|
|
30
|
+
Cancel = 'cancel',
|
|
31
|
+
Abandon = 'abandon',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Policy types ───────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export interface RetryPolicy {
|
|
37
|
+
attempts: number;
|
|
38
|
+
backoff: 'fixed' | 'exponential';
|
|
39
|
+
baseMs: number;
|
|
40
|
+
nonRetryableErrors?: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ConcurrencyPolicy<TInput> {
|
|
44
|
+
key: (input: TInput) => string;
|
|
45
|
+
collisionMode: 'queue' | 'reject' | 'replace';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DedupePolicy<TInput> {
|
|
49
|
+
key: (input: TInput) => string;
|
|
50
|
+
windowMs: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Declarative scope reference. `TScope` is parameterised so JOB-7 can narrow
|
|
55
|
+
* `entity` to the generated `ScopeEntityType` union at the call site without
|
|
56
|
+
* modifying this file (OQ-1 resolution, 2026-04-20).
|
|
57
|
+
*/
|
|
58
|
+
export interface ScopeRef<TInput, TScope extends string = string> {
|
|
59
|
+
entity: TScope;
|
|
60
|
+
from: (input: TInput) => string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface JobHandlerMeta<TInput> {
|
|
64
|
+
pool?: string;
|
|
65
|
+
scope?: ScopeRef<TInput>;
|
|
66
|
+
retry?: RetryPolicy;
|
|
67
|
+
concurrency?: ConcurrencyPolicy<TInput>;
|
|
68
|
+
dedupe?: DedupePolicy<TInput>;
|
|
69
|
+
timeoutMs?: number;
|
|
70
|
+
replayFrom?: 'scratch' | 'last_step' | 'last_checkpoint';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Runtime option shapes ──────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export interface StepOptions {
|
|
76
|
+
retry?: RetryPolicy;
|
|
77
|
+
timeoutMs?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SpawnChildOptions {
|
|
81
|
+
closePolicy?: ParentClosePolicy;
|
|
82
|
+
runAt?: Date;
|
|
83
|
+
priority?: number;
|
|
84
|
+
tags?: Record<string, string>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── JobContext ─────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
export interface JobContext<TInput> {
|
|
90
|
+
readonly input: TInput;
|
|
91
|
+
readonly run: JobRun;
|
|
92
|
+
step<TOutput>(
|
|
93
|
+
stepId: string,
|
|
94
|
+
fn: () => Promise<TOutput>,
|
|
95
|
+
opts?: StepOptions,
|
|
96
|
+
): Promise<TOutput>;
|
|
97
|
+
spawnChild(type: string, input: unknown, opts?: SpawnChildOptions): Promise<JobRun>;
|
|
98
|
+
readonly logger: Logger;
|
|
99
|
+
// NOT in Phase 1 — deferred to ADR-025:
|
|
100
|
+
// waitFor(kind, token, opts)
|
|
101
|
+
// signal(token, payload)
|
|
102
|
+
// sleep(ms)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── JobHandlerBase ─────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
export abstract class JobHandlerBase<TInput, TOutput = unknown> {
|
|
108
|
+
abstract run(ctx: JobContext<TInput>): Promise<TOutput>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Registry + decorator ───────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Module-singleton map keyed by job type. Populated by the `@JobHandler`
|
|
115
|
+
* decorator at class definition time; consumed by `JobWorkerModule` (JOB-5)
|
|
116
|
+
* to upsert `job` rows and resolve handler classes during claim/execute.
|
|
117
|
+
*/
|
|
118
|
+
export const JOB_HANDLER_REGISTRY = new Map<
|
|
119
|
+
string,
|
|
120
|
+
{
|
|
121
|
+
type: string;
|
|
122
|
+
meta: JobHandlerMeta<unknown>;
|
|
123
|
+
handlerClass: new (...args: unknown[]) => JobHandlerBase<unknown>;
|
|
124
|
+
}
|
|
125
|
+
>();
|
|
126
|
+
|
|
127
|
+
export const JOB_HANDLER_METADATA_KEY = Symbol('JobHandlerMeta');
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Class decorator that registers a handler with the job type, the full
|
|
131
|
+
* metadata shape, and the target class constructor.
|
|
132
|
+
*
|
|
133
|
+
* Duplicate-type behaviour (OQ-3, resolved 2026-04-18):
|
|
134
|
+
* - `NODE_ENV === 'production'` → throw; silent overwrite in prod is a
|
|
135
|
+
* correctness bug.
|
|
136
|
+
* - `NODE_ENV === 'test'` → silent overwrite (tests intentionally
|
|
137
|
+
* re-register handlers).
|
|
138
|
+
* - otherwise (dev) → `console.warn` + overwrite. `console`
|
|
139
|
+
* is used intentionally instead of the Nest `Logger` — decorators run
|
|
140
|
+
* at module-load time before any Nest container exists.
|
|
141
|
+
*/
|
|
142
|
+
export function JobHandler<TInput>(
|
|
143
|
+
type: string,
|
|
144
|
+
meta: JobHandlerMeta<TInput>,
|
|
145
|
+
): ClassDecorator {
|
|
146
|
+
return (target) => {
|
|
147
|
+
if (JOB_HANDLER_REGISTRY.has(type)) {
|
|
148
|
+
const env = process.env.NODE_ENV;
|
|
149
|
+
if (env === 'production') {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`[JobHandler] Duplicate registration for job type '${type}'. ` +
|
|
152
|
+
`Each @JobHandler must declare a unique type.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (env !== 'test') {
|
|
156
|
+
// eslint-disable-next-line no-console
|
|
157
|
+
console.warn(
|
|
158
|
+
`[JobHandler] Duplicate registration for job type '${type}'. ` +
|
|
159
|
+
`Overwriting previous handler — this is almost certainly a bug.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
Reflect.defineMetadata(JOB_HANDLER_METADATA_KEY, { type, meta }, target);
|
|
165
|
+
JOB_HANDLER_REGISTRY.set(type, {
|
|
166
|
+
type,
|
|
167
|
+
meta: meta as JobHandlerMeta<unknown>,
|
|
168
|
+
handlerClass: target as unknown as new (
|
|
169
|
+
...args: unknown[]
|
|
170
|
+
) => JobHandlerBase<unknown>,
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── HandlerRegistry — read helpers consumed by JobWorkerModule (JOB-5) ─────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Single entry shape returned by `HandlerRegistry.getAll()` / `.get()` and
|
|
179
|
+
* exposed to `JobWorkerModule.onModuleInit` for boot-time upserts.
|
|
180
|
+
*
|
|
181
|
+
* Structurally compatible with `IJobOrchestrator.upsertJobRows`'s
|
|
182
|
+
* `JobUpsertEntry` so the worker module can pass entries through verbatim
|
|
183
|
+
* without re-mapping.
|
|
184
|
+
*/
|
|
185
|
+
export interface HandlerRegistryEntry {
|
|
186
|
+
type: string;
|
|
187
|
+
meta: JobHandlerMeta<unknown>;
|
|
188
|
+
handlerClass: new (...args: unknown[]) => JobHandlerBase<unknown>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Read facade over `JOB_HANDLER_REGISTRY`. The decorator's write path is
|
|
193
|
+
* unchanged; this namespace exists so consumers (the worker module, tests)
|
|
194
|
+
* don't import the raw `Map` and accidentally mutate it.
|
|
195
|
+
*/
|
|
196
|
+
export namespace HandlerRegistry {
|
|
197
|
+
/** All registered entries in insertion order. */
|
|
198
|
+
export function getAll(): HandlerRegistryEntry[] {
|
|
199
|
+
return Array.from(JOB_HANDLER_REGISTRY.values());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Lookup by job type, or `undefined` if no `@JobHandler` is registered. */
|
|
203
|
+
export function get(type: string): HandlerRegistryEntry | undefined {
|
|
204
|
+
return JOB_HANDLER_REGISTRY.get(type);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle schema for the job orchestration domain (ADR-022).
|
|
3
|
+
*
|
|
4
|
+
* Three tables model the lifecycle of a durable job:
|
|
5
|
+
* - `job` — definitions keyed by handler type (e.g. 'onboarding').
|
|
6
|
+
* - `job_run` — one row per attempt to execute a job; worker claims
|
|
7
|
+
* rows directly via SELECT ... FOR UPDATE SKIP LOCKED.
|
|
8
|
+
* - `job_step` — individual steps within a run; memoises output for replay.
|
|
9
|
+
*
|
|
10
|
+
* Phase 1 ships only this layer. There is no `job_queue` table, no executor
|
|
11
|
+
* port — see ADR-022 and `.claude/skills/jobs/SKILL.md` for the rationale.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
pgEnum,
|
|
15
|
+
pgTable,
|
|
16
|
+
uuid,
|
|
17
|
+
text,
|
|
18
|
+
jsonb,
|
|
19
|
+
integer,
|
|
20
|
+
timestamp,
|
|
21
|
+
index,
|
|
22
|
+
uniqueIndex,
|
|
23
|
+
} from 'drizzle-orm/pg-core';
|
|
24
|
+
import { sql } from 'drizzle-orm';
|
|
25
|
+
import type { InferSelectModel } from 'drizzle-orm';
|
|
26
|
+
|
|
27
|
+
// ─── Internal $type<> helpers ───────────────────────────────────────────────
|
|
28
|
+
// Annotation types for jsonb columns only. JOB-2 defines the public protocol
|
|
29
|
+
// types; these remain private to this file.
|
|
30
|
+
|
|
31
|
+
type RetryPolicy = {
|
|
32
|
+
attempts: number;
|
|
33
|
+
backoff: 'fixed' | 'exponential';
|
|
34
|
+
baseMs: number;
|
|
35
|
+
nonRetryableErrors?: string[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type JobRunError = {
|
|
39
|
+
message: string;
|
|
40
|
+
stack?: string;
|
|
41
|
+
retryable: boolean;
|
|
42
|
+
attempt: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ─── Enums ──────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export const jobRunStatusEnum = pgEnum('job_run_status', [
|
|
48
|
+
'pending',
|
|
49
|
+
'running',
|
|
50
|
+
'waiting',
|
|
51
|
+
'completed',
|
|
52
|
+
'failed',
|
|
53
|
+
'timed_out',
|
|
54
|
+
'canceled',
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
// extended in ADR-027: tool_call | llm_call | wait | checkpoint | message
|
|
58
|
+
export const jobStepKindEnum = pgEnum('job_step_kind', ['task']);
|
|
59
|
+
|
|
60
|
+
export const jobStepStatusEnum = pgEnum('job_step_status', [
|
|
61
|
+
'pending',
|
|
62
|
+
'running',
|
|
63
|
+
'completed',
|
|
64
|
+
'failed',
|
|
65
|
+
'skipped',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
export const collisionModeEnum = pgEnum('job_collision_mode', [
|
|
69
|
+
'queue',
|
|
70
|
+
'reject',
|
|
71
|
+
'replace',
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
export const replayFromEnum = pgEnum('job_replay_from', [
|
|
75
|
+
'scratch',
|
|
76
|
+
'last_step',
|
|
77
|
+
'last_checkpoint',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
export const parentClosePolicyEnum = pgEnum('job_parent_close_policy', [
|
|
81
|
+
'terminate',
|
|
82
|
+
'cancel',
|
|
83
|
+
'abandon',
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
// Phase 3 placeholder — see ADR-025
|
|
87
|
+
export const waitKindEnum = pgEnum('job_wait_kind', ['signal']);
|
|
88
|
+
|
|
89
|
+
// Phase 2 may add more sources; requires Atlas migration
|
|
90
|
+
export const triggerSourceEnum = pgEnum('job_trigger_source', [
|
|
91
|
+
'manual',
|
|
92
|
+
'schedule',
|
|
93
|
+
'event',
|
|
94
|
+
'parent',
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
// ─── job ────────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export const jobs = pgTable('job', {
|
|
100
|
+
type: text('type').primaryKey(),
|
|
101
|
+
version: integer('version').notNull().default(1),
|
|
102
|
+
pool: text('pool').notNull(),
|
|
103
|
+
scopeEntityType: text('scope_entity_type'),
|
|
104
|
+
retryPolicy: jsonb('retry_policy').notNull().$type<RetryPolicy>(),
|
|
105
|
+
timeoutMs: integer('timeout_ms'),
|
|
106
|
+
concurrencyKeyTemplate: text('concurrency_key_template'),
|
|
107
|
+
collisionMode: collisionModeEnum('collision_mode').notNull().default('queue'),
|
|
108
|
+
dedupeKeyTemplate: text('dedupe_key_template'),
|
|
109
|
+
dedupeWindowMs: integer('dedupe_window_ms'),
|
|
110
|
+
priorityDefault: integer('priority_default').notNull().default(0),
|
|
111
|
+
replayFrom: replayFromEnum('replay_from').notNull().default('last_checkpoint'),
|
|
112
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
113
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export type JobDefinitionRow = InferSelectModel<typeof jobs>;
|
|
117
|
+
|
|
118
|
+
// ─── job_run ────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export const jobRuns = pgTable(
|
|
121
|
+
'job_run',
|
|
122
|
+
{
|
|
123
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
124
|
+
jobType: text('job_type').notNull().references(() => jobs.type),
|
|
125
|
+
jobVersion: integer('job_version').notNull(),
|
|
126
|
+
parentRunId: uuid('parent_run_id').references((): any => jobRuns.id),
|
|
127
|
+
/**
|
|
128
|
+
* Service generates `id` client-side via randomUUID() and sets
|
|
129
|
+
* root_run_id = id for root runs (single INSERT, no self-FK race).
|
|
130
|
+
*/
|
|
131
|
+
rootRunId: uuid('root_run_id').notNull(),
|
|
132
|
+
parentClosePolicy: parentClosePolicyEnum('parent_close_policy')
|
|
133
|
+
.notNull()
|
|
134
|
+
.default('terminate'),
|
|
135
|
+
scopeEntityType: text('scope_entity_type'),
|
|
136
|
+
scopeEntityId: text('scope_entity_id'),
|
|
137
|
+
tenantId: text('tenant_id'),
|
|
138
|
+
tags: jsonb('tags').notNull().default({}).$type<Record<string, string>>(),
|
|
139
|
+
pool: text('pool').notNull(),
|
|
140
|
+
priority: integer('priority').notNull().default(0),
|
|
141
|
+
concurrencyKey: text('concurrency_key'),
|
|
142
|
+
dedupeKey: text('dedupe_key'),
|
|
143
|
+
status: jobRunStatusEnum('status').notNull().default('pending'),
|
|
144
|
+
input: jsonb('input').notNull().$type<Record<string, unknown>>(),
|
|
145
|
+
output: jsonb('output').$type<Record<string, unknown>>(),
|
|
146
|
+
error: jsonb('error').$type<JobRunError>(),
|
|
147
|
+
triggerSource: triggerSourceEnum('trigger_source').notNull(),
|
|
148
|
+
triggerRef: text('trigger_ref'),
|
|
149
|
+
runAt: timestamp('run_at', { withTimezone: true }).notNull().defaultNow(),
|
|
150
|
+
startedAt: timestamp('started_at', { withTimezone: true }),
|
|
151
|
+
finishedAt: timestamp('finished_at', { withTimezone: true }),
|
|
152
|
+
claimedAt: timestamp('claimed_at', { withTimezone: true }),
|
|
153
|
+
attempts: integer('attempts').notNull().default(0),
|
|
154
|
+
// Phase 3 placeholder — see ADR-025
|
|
155
|
+
waitKind: waitKindEnum('wait_kind'),
|
|
156
|
+
// Phase 3 placeholder — see ADR-025
|
|
157
|
+
resumeToken: text('resume_token'),
|
|
158
|
+
// Phase 3 placeholder — see ADR-025
|
|
159
|
+
waitDeadline: timestamp('wait_deadline', { withTimezone: true }),
|
|
160
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
161
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
162
|
+
},
|
|
163
|
+
(t) => ({
|
|
164
|
+
/** Claim query: ORDER BY priority DESC, run_at ASC. */
|
|
165
|
+
idxJobRunClaim: index('idx_job_run_claim').on(t.status, t.pool, t.runAt),
|
|
166
|
+
/** Tree traversal / cascade cancel. */
|
|
167
|
+
idxJobRunRoot: index('idx_job_run_root').on(t.rootRunId),
|
|
168
|
+
/** listForScope query. */
|
|
169
|
+
idxJobRunScope: index('idx_job_run_scope').on(t.scopeEntityType, t.scopeEntityId),
|
|
170
|
+
/** Idempotency collapse — partial index. */
|
|
171
|
+
idxJobRunDedupe: index('idx_job_run_dedupe')
|
|
172
|
+
.on(t.jobType, t.dedupeKey)
|
|
173
|
+
.where(sql`${t.dedupeKey} IS NOT NULL`),
|
|
174
|
+
/** Collision check — partial index. */
|
|
175
|
+
idxJobRunConcurrency: index('idx_job_run_concurrency')
|
|
176
|
+
.on(t.concurrencyKey)
|
|
177
|
+
.where(
|
|
178
|
+
sql`${t.concurrencyKey} IS NOT NULL AND ${t.status} IN ('pending','running')`,
|
|
179
|
+
),
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
export type JobRunRow = InferSelectModel<typeof jobRuns>;
|
|
184
|
+
|
|
185
|
+
// ─── job_step ───────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
export const jobSteps = pgTable(
|
|
188
|
+
'job_step',
|
|
189
|
+
{
|
|
190
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
191
|
+
jobRunId: uuid('job_run_id').notNull().references(() => jobRuns.id),
|
|
192
|
+
stepId: text('step_id').notNull(),
|
|
193
|
+
kind: jobStepKindEnum('kind').notNull().default('task'),
|
|
194
|
+
/**
|
|
195
|
+
* Monotonic within run. integer (max ~2B per run) is sufficient —
|
|
196
|
+
* downgraded from ADR-022's bigint; revisit only if a single run
|
|
197
|
+
* ever exceeds 2 billion steps.
|
|
198
|
+
*/
|
|
199
|
+
seq: integer('seq').notNull(),
|
|
200
|
+
status: jobStepStatusEnum('status').notNull().default('pending'),
|
|
201
|
+
input: jsonb('input').$type<Record<string, unknown>>(),
|
|
202
|
+
/** Memoised on success for replay. */
|
|
203
|
+
output: jsonb('output').$type<Record<string, unknown>>(),
|
|
204
|
+
error: jsonb('error').$type<JobRunError>(),
|
|
205
|
+
attempts: integer('attempts').notNull().default(0),
|
|
206
|
+
startedAt: timestamp('started_at', { withTimezone: true }),
|
|
207
|
+
finishedAt: timestamp('finished_at', { withTimezone: true }),
|
|
208
|
+
},
|
|
209
|
+
(t) => ({
|
|
210
|
+
/** No duplicate step IDs per run. */
|
|
211
|
+
idxJobStepRunStep: uniqueIndex('idx_job_step_run_step').on(t.jobRunId, t.stepId),
|
|
212
|
+
/** Ordered timeline reads. */
|
|
213
|
+
idxJobStepTimeline: index('idx_job_step_timeline').on(t.jobRunId, t.seq),
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
export type JobStepRow = InferSelectModel<typeof jobSteps>;
|