@pattern-stack/codegen 0.4.1 → 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/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 +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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subsystems barrel export
|
|
3
|
+
*
|
|
4
|
+
* Infrastructure subsystems following Protocol → Backend → Factory pattern (ADR-008).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Events
|
|
8
|
+
export { EVENT_BUS } from './events';
|
|
9
|
+
export type { DomainEvent, IEventBus } from './events';
|
|
10
|
+
export { EventsModule, DrizzleEventBus, MemoryEventBus } from './events';
|
|
11
|
+
|
|
12
|
+
// Jobs — orchestration schema only (JOB-1). Protocols / modules land in JOB-2 / JOB-5.
|
|
13
|
+
export { jobs, jobRuns, jobSteps } from './jobs';
|
|
14
|
+
export type { JobDefinitionRow, JobRunRow, JobStepRow } from './jobs';
|
|
15
|
+
export {
|
|
16
|
+
jobRunStatusEnum,
|
|
17
|
+
jobStepKindEnum,
|
|
18
|
+
jobStepStatusEnum,
|
|
19
|
+
collisionModeEnum,
|
|
20
|
+
replayFromEnum,
|
|
21
|
+
parentClosePolicyEnum,
|
|
22
|
+
waitKindEnum,
|
|
23
|
+
triggerSourceEnum,
|
|
24
|
+
} from './jobs';
|
|
25
|
+
|
|
26
|
+
// Cache
|
|
27
|
+
export { CACHE } from './cache';
|
|
28
|
+
export type { ICacheService } from './cache';
|
|
29
|
+
export { CacheModule, DrizzleCacheService, MemoryCacheService } from './cache';
|
|
30
|
+
|
|
31
|
+
// Storage
|
|
32
|
+
export { STORAGE } from './storage';
|
|
33
|
+
export type { IStorageService } from './storage';
|
|
34
|
+
export { StorageModule, LocalStorageBackend, MemoryStorageBackend } from './storage';
|
|
35
|
+
|
|
36
|
+
// Auth
|
|
37
|
+
export {
|
|
38
|
+
ENCRYPTION_KEY,
|
|
39
|
+
OAUTH_STATE_STORE,
|
|
40
|
+
AUTH_INTEGRATION_READER,
|
|
41
|
+
AUTH_INTEGRATION_TOKEN_WRITER,
|
|
42
|
+
AuthModule,
|
|
43
|
+
OAuth2RefreshStrategy,
|
|
44
|
+
withAuthRetry,
|
|
45
|
+
IntegrationBrokenError,
|
|
46
|
+
SessionExpiredError,
|
|
47
|
+
isSessionExpiredError,
|
|
48
|
+
EnvEncryptionKey,
|
|
49
|
+
InMemoryOAuthStateStore,
|
|
50
|
+
} from './auth';
|
|
51
|
+
export type {
|
|
52
|
+
IAuthStrategy,
|
|
53
|
+
IEncryptionKey,
|
|
54
|
+
IOAuthStateStore,
|
|
55
|
+
IIntegrationReader,
|
|
56
|
+
IIntegrationTokenWriter,
|
|
57
|
+
AuthCredentials,
|
|
58
|
+
AuthResolveOptions,
|
|
59
|
+
DecryptedIntegration,
|
|
60
|
+
OAuthStateEntry,
|
|
61
|
+
IntegrationTokenUpdate,
|
|
62
|
+
ParsedRefreshResponse,
|
|
63
|
+
} from './auth';
|
|
@@ -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'), // F9: always emitted (nullable) — runtime enforces on boundary via JOBS_MULTI_TENANT
|
|
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>;
|
|
@@ -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'), // F9: always emitted (nullable) — runtime enforces on boundary via JOBS_MULTI_TENANT
|
|
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>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
|
|
2
|
+
// Run `codegen entity new --all` to refresh.
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
export type ScopeEntityType = 'opportunity';
|
|
7
|
+
|
|
8
|
+
export const SCOPE_ENTITY_TYPES = ['opportunity'] as const;
|
|
9
|
+
|
|
10
|
+
export const scopeEntityTypeSchema = z.enum(['opportunity']);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// ─── JOB-1: Drizzle schema (tables, enums, row types) ──────────────────────
|
|
2
|
+
export { jobs, jobRuns, jobSteps } from './job-orchestration.schema';
|
|
3
|
+
export type {
|
|
4
|
+
JobDefinitionRow,
|
|
5
|
+
JobRunRow,
|
|
6
|
+
JobStepRow,
|
|
7
|
+
} from './job-orchestration.schema';
|
|
8
|
+
export {
|
|
9
|
+
jobRunStatusEnum,
|
|
10
|
+
jobStepKindEnum,
|
|
11
|
+
jobStepStatusEnum,
|
|
12
|
+
collisionModeEnum,
|
|
13
|
+
replayFromEnum,
|
|
14
|
+
parentClosePolicyEnum,
|
|
15
|
+
waitKindEnum,
|
|
16
|
+
triggerSourceEnum,
|
|
17
|
+
} from './job-orchestration.schema';
|
|
18
|
+
|
|
19
|
+
// ─── JOB-2 + JOB-8: domain tokens ──────────────────────────────────────────
|
|
20
|
+
export {
|
|
21
|
+
JOB_ORCHESTRATOR,
|
|
22
|
+
JOB_RUN_SERVICE,
|
|
23
|
+
JOB_STEP_SERVICE,
|
|
24
|
+
JOBS_MULTI_TENANT,
|
|
25
|
+
} from './jobs-domain.tokens';
|
|
26
|
+
|
|
27
|
+
// ─── JOB-2: orchestrator protocol ──────────────────────────────────────────
|
|
28
|
+
export type {
|
|
29
|
+
IJobOrchestrator,
|
|
30
|
+
StartOptions,
|
|
31
|
+
CancelOptions,
|
|
32
|
+
JobRun,
|
|
33
|
+
JobUpsertEntry,
|
|
34
|
+
JobPoolDef,
|
|
35
|
+
} from './job-orchestrator.protocol';
|
|
36
|
+
|
|
37
|
+
// ─── JOB-2: run-service protocol ───────────────────────────────────────────
|
|
38
|
+
export type {
|
|
39
|
+
IJobRunService,
|
|
40
|
+
ListForScopeOptions,
|
|
41
|
+
CancelForScopeOptions,
|
|
42
|
+
RescheduleForScopeOptions,
|
|
43
|
+
} from './job-run-service.protocol';
|
|
44
|
+
|
|
45
|
+
// ─── JOB-2: step-service protocol ──────────────────────────────────────────
|
|
46
|
+
export type {
|
|
47
|
+
IJobStepService,
|
|
48
|
+
RecordStepInput,
|
|
49
|
+
JobStep,
|
|
50
|
+
} from './job-step-service.protocol';
|
|
51
|
+
|
|
52
|
+
// ─── JOB-2: handler base, decorator, registry, policy types ────────────────
|
|
53
|
+
export {
|
|
54
|
+
ParentClosePolicy,
|
|
55
|
+
JobHandlerBase,
|
|
56
|
+
JobHandler,
|
|
57
|
+
JOB_HANDLER_REGISTRY,
|
|
58
|
+
JOB_HANDLER_METADATA_KEY,
|
|
59
|
+
HandlerRegistry,
|
|
60
|
+
} from './job-handler.base';
|
|
61
|
+
export type {
|
|
62
|
+
RetryPolicy,
|
|
63
|
+
ConcurrencyPolicy,
|
|
64
|
+
DedupePolicy,
|
|
65
|
+
ScopeRef,
|
|
66
|
+
JobHandlerMeta,
|
|
67
|
+
StepOptions,
|
|
68
|
+
SpawnChildOptions,
|
|
69
|
+
JobContext,
|
|
70
|
+
HandlerRegistryEntry,
|
|
71
|
+
} from './job-handler.base';
|
|
72
|
+
|
|
73
|
+
// ─── JOB-3: Drizzle backends + JobWorker ────────────────────────────────
|
|
74
|
+
export { DrizzleJobOrchestrator } from './job-orchestrator.drizzle-backend';
|
|
75
|
+
export { DrizzleJobRunService } from './job-run-service.drizzle-backend';
|
|
76
|
+
export { DrizzleJobStepService } from './job-step-service.drizzle-backend';
|
|
77
|
+
export {
|
|
78
|
+
JobWorker,
|
|
79
|
+
JOB_WORKER_OPTIONS,
|
|
80
|
+
computeBackoff,
|
|
81
|
+
classifyError,
|
|
82
|
+
buildClaimQuery,
|
|
83
|
+
buildStaleSweepQuery,
|
|
84
|
+
} from './job-worker';
|
|
85
|
+
export type { JobWorkerOptions } from './job-worker';
|
|
86
|
+
export {
|
|
87
|
+
JobCollisionError,
|
|
88
|
+
JobNotReplayableError,
|
|
89
|
+
JobTemplateFieldMissingError,
|
|
90
|
+
JobTypeNotFoundError,
|
|
91
|
+
MissingTenantIdError,
|
|
92
|
+
BootValidationError,
|
|
93
|
+
ReservedPoolViolationError,
|
|
94
|
+
} from './jobs-errors';
|
|
95
|
+
|
|
96
|
+
// ─── JOB-4: Memory backends + shared in-memory store ───────────────────────
|
|
97
|
+
export { MemoryJobStore } from './memory-job-store';
|
|
98
|
+
export { MemoryJobOrchestrator } from './job-orchestrator.memory-backend';
|
|
99
|
+
export { MemoryJobRunService } from './job-run-service.memory-backend';
|
|
100
|
+
export { MemoryJobStepService } from './job-step-service.memory-backend';
|
|
101
|
+
|
|
102
|
+
// ─── JOB-5: domain + worker modules + pool config loader ───────────────────
|
|
103
|
+
export {
|
|
104
|
+
JobsDomainModule,
|
|
105
|
+
type JobsDomainModuleOptions,
|
|
106
|
+
type DrizzleBackendExtensions,
|
|
107
|
+
} from './jobs-domain.module';
|
|
108
|
+
export {
|
|
109
|
+
JobWorkerModule,
|
|
110
|
+
JobWorkerOrchestrator,
|
|
111
|
+
type JobWorkerModuleOptions,
|
|
112
|
+
} from './job-worker.module';
|
|
113
|
+
export {
|
|
114
|
+
loadPoolConfig,
|
|
115
|
+
allNonReservedPoolNames,
|
|
116
|
+
FRAMEWORK_POOLS,
|
|
117
|
+
RESERVED_POOL_NAMES,
|
|
118
|
+
type PoolConfig,
|
|
119
|
+
type PoolDefinition,
|
|
120
|
+
} from './pool-config.loader';
|