@cosmicdrift/kumiko-framework 0.304.0 → 0.306.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/api/__tests__/extra-route-rejection.test.ts +38 -0
- package/src/api/__tests__/extra-routes.integration.test.ts +30 -0
- package/src/api/__tests__/server-boot-guards.test.ts +1 -0
- package/src/api/__tests__/server-error-logging.test.ts +104 -0
- package/src/api/api-constants.ts +13 -0
- package/src/api/extra-route.ts +33 -4
- package/src/api/index.ts +1 -0
- package/src/api/request-context.ts +5 -4
- package/src/api/routes.ts +26 -1
- package/src/api/server.ts +8 -2
- package/src/bun-db/__tests__/closed-connection-retry.integration.test.ts +159 -0
- package/src/bun-db/__tests__/select-many-retry.integration.test.ts +138 -0
- package/src/bun-db/query.ts +42 -18
- package/src/changes.json +108 -0
- package/src/db/__tests__/pg-error.test.ts +14 -0
- package/src/db/__tests__/system-db-view-export.test.ts +107 -0
- package/src/db/__tests__/tenant-db-no-raw.test.ts +10 -0
- package/src/db/__tests__/with-systemdb-unsafe-raw-grant.test.ts +71 -0
- package/src/db/event-store-executor-write.ts +7 -0
- package/src/db/index.ts +1 -1
- package/src/db/pg-error.ts +13 -0
- package/src/db/queries/__tests__/{unsafe-read-retrying.test.ts → unsafe-read-retrying.integration.test.ts} +28 -28
- package/src/db/tenant-db.ts +140 -16
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +6 -1
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +2 -2
- package/src/engine/__tests__/boot-validator.test.ts +1 -1
- package/src/engine/__tests__/tier-resolver-extension.test.ts +1 -1
- package/src/engine/boot-validator/access-declarations.ts +5 -66
- package/src/engine/extension-names.ts +55 -25
- package/src/engine/extensions/storage-provider.ts +14 -41
- package/src/engine/extensions/tenant-data.ts +4 -0
- package/src/engine/extensions/tenant-resource.ts +40 -0
- package/src/engine/extensions/user-data.ts +8 -7
- package/src/engine/feature-ast/__tests__/handler-header-roundtrip.test.ts +669 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +5 -3
- package/src/engine/feature-ast/__tests__/patch-update.test.ts +718 -0
- package/src/engine/feature-ast/__tests__/pattern-change-schema.test.ts +819 -0
- package/src/engine/feature-ast/entity-field-types.ts +41 -0
- package/src/engine/feature-ast/extractors/handlers.ts +217 -84
- package/src/engine/feature-ast/extractors/hooks.ts +72 -15
- package/src/engine/feature-ast/extractors/round2.ts +21 -0
- package/src/engine/feature-ast/extractors/shared.ts +9 -0
- package/src/engine/feature-ast/index.ts +11 -1
- package/src/engine/feature-ast/patch.ts +338 -5
- package/src/engine/feature-ast/patcher.ts +2 -2
- package/src/engine/feature-ast/pattern-change-schema.ts +1411 -0
- package/src/engine/feature-ast/patterns.ts +22 -15
- package/src/engine/feature-ast/render.ts +1 -0
- package/src/engine/feature-ui-extensions.ts +8 -7
- package/src/engine/index.ts +23 -5
- package/src/engine/personal-data-fields.ts +66 -0
- package/src/engine/registry-validate.ts +15 -0
- package/src/engine/registry.ts +2 -0
- package/src/engine/types/extension-options-map.ts +1 -0
- package/src/engine/types/index.ts +8 -0
- package/src/env/__tests__/dry-run.test.ts +43 -3
- package/src/env/dry-run.ts +28 -15
- package/src/errors/__tests__/write-failures.test.ts +47 -4
- package/src/errors/i18n/de.yaml +12 -0
- package/src/errors/i18n/en.yaml +12 -0
- package/src/errors/reasons.ts +4 -0
- package/src/errors/write-error-info.ts +12 -3
- package/src/jobs/__tests__/job-backoff.integration.test.ts +155 -0
- package/src/jobs/__tests__/job-retention.integration.test.ts +316 -0
- package/src/jobs/__tests__/job-retry-enqueue-paths.integration.test.ts +309 -0
- package/src/jobs/__tests__/jobs.integration.test.ts +38 -3
- package/src/jobs/job-runner.ts +170 -19
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +113 -26
- package/src/pipeline/__tests__/dispatcher.test.ts +5 -0
- package/src/pipeline/__tests__/hook-systemdb-escape-hatch.integration.test.ts +246 -0
- package/src/pipeline/__tests__/idempotency-transient-failure.integration.test.ts +198 -0
- package/src/pipeline/__tests__/public-intake-runtime-gate.integration.test.ts +425 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.test.ts +59 -0
- package/src/pipeline/active-membership.ts +5 -1
- package/src/pipeline/dispatch-batch.ts +59 -13
- package/src/pipeline/dispatch-query.ts +16 -5
- package/src/pipeline/dispatch-shared.ts +12 -5
- package/src/pipeline/dispatch-stream.ts +7 -2
- package/src/pipeline/dispatch-write.ts +22 -5
- package/src/pipeline/dispatcher.ts +9 -2
- package/src/pipeline/idempotency.ts +16 -0
- package/src/pipeline/member-reader.ts +3 -1
- package/src/pipeline/system-identity-switch.ts +22 -4
- package/src/pipeline/write-origin.ts +107 -0
- package/src/rate-limit/__tests__/middleware.integration.test.ts +40 -0
- package/src/rate-limit/middleware.ts +3 -0
- package/src/stack/__tests__/setup-test-stack-metrics.integration.test.ts +79 -0
- package/src/stack/test-stack.ts +5 -0
- package/src/testing/closed-connection-error.ts +62 -0
- package/src/testing/index.ts +1 -0
- package/src/bun-db/__tests__/select-many-retry.test.ts +0 -79
|
@@ -0,0 +1,1411 @@
|
|
|
1
|
+
// Runtime validator for the `unknown → PatternChange[]` boundary. An AI
|
|
2
|
+
// tool (or any external caller) hands us JSON; before it ever reaches
|
|
3
|
+
// `applyChanges` it must pass through here. Every rejection carries an
|
|
4
|
+
// exact dotted/bracketed path so the caller (frequently an LLM) can
|
|
5
|
+
// self-correct without a human in the loop — see kumiko-framework#3137.
|
|
6
|
+
//
|
|
7
|
+
// **Zod v4 constraint driving this file's shape:** every discriminated-
|
|
8
|
+
// union member below stays a plain `z.object(...)` (or `.superRefine`
|
|
9
|
+
// wrapper around one) — never `.transform()` at the object level. A
|
|
10
|
+
// whole-object transform would erase the "which member matched" signal
|
|
11
|
+
// `discriminatedUnion` needs to attribute a nested field error to the
|
|
12
|
+
// right member, and it would blur per-member `.strict()` unrecognized-key
|
|
13
|
+
// detection. Transforms only ever touch a single field (`sourceBody`,
|
|
14
|
+
// below) — never the object they live on.
|
|
15
|
+
//
|
|
16
|
+
// **Deep definitions stay opaque.** EntityDefinition, ScreenDefinition,
|
|
17
|
+
// NavDefinition, WorkspaceDefinition, JobDefinition-options, MetricOptions,
|
|
18
|
+
// etc. are NOT hand-modeled here — that would fork a second, driftable
|
|
19
|
+
// catalogue of their shape. They're accepted via `z.custom` (plain object,
|
|
20
|
+
// or a RawRefSentinel/AiStepOpaqueArgs — the extractor's own "couldn't
|
|
21
|
+
// resolve this statically" markers) and, for EntityDefinition only,
|
|
22
|
+
// additionally checked for field-type membership via the shared
|
|
23
|
+
// `findUnknownEntityFieldTypes` helper (kept in sync with `extractEntity`).
|
|
24
|
+
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { type LifecycleHookType, LifecycleHookTypes } from "../constants";
|
|
27
|
+
import type {
|
|
28
|
+
ConfigKeyDefinition,
|
|
29
|
+
ConfigKeyType,
|
|
30
|
+
JobDefinition,
|
|
31
|
+
ReferenceDataDef,
|
|
32
|
+
RunIn,
|
|
33
|
+
TranslationKeys,
|
|
34
|
+
} from "../types/config";
|
|
35
|
+
import type { MetricOptions, SecretOptions } from "../types/feature";
|
|
36
|
+
import type { EntityDefinition } from "../types/fields";
|
|
37
|
+
import type { AgentRisk, ClaimKeyType, RateLimitPer } from "../types/handlers";
|
|
38
|
+
import type { HookPhase } from "../types/hooks";
|
|
39
|
+
import type { HttpRouteMethod } from "../types/http-route";
|
|
40
|
+
import type { NavDefinition } from "../types/nav";
|
|
41
|
+
import type { RelationDefinition } from "../types/relations";
|
|
42
|
+
import type { ScreenDefinition } from "../types/screen";
|
|
43
|
+
import type { TreeActionDef } from "../types/tree-node";
|
|
44
|
+
import type { WorkspaceDefinition } from "../types/workspace";
|
|
45
|
+
import { describeUnknownFieldType, findUnknownEntityFieldTypes } from "./entity-field-types";
|
|
46
|
+
import { AGENT_RISK_VALUES } from "./extractors/handlers";
|
|
47
|
+
import { isPlainObject, isRawRefSentinel } from "./extractors/shared";
|
|
48
|
+
import type {
|
|
49
|
+
PatternChange,
|
|
50
|
+
PatternId,
|
|
51
|
+
QueryHandlerHeaderKey,
|
|
52
|
+
StreamHandlerHeaderKey,
|
|
53
|
+
WriteHandlerHeaderKey,
|
|
54
|
+
} from "./patch";
|
|
55
|
+
import { SYNTHETIC_LOC } from "./patcher";
|
|
56
|
+
import type { FeaturePatternKind } from "./patterns";
|
|
57
|
+
import type { SourceLocation, SourcePosition } from "./source-location";
|
|
58
|
+
|
|
59
|
+
// Derives an exhaustive literal-value array from a `Record<T, true>` flag
|
|
60
|
+
// map: an added/removed union member fails to compile here instead of
|
|
61
|
+
// silently producing a schema narrower than the runtime type — same
|
|
62
|
+
// rationale as kumiko-types's `NO_WIDGET_FIELD_TYPES`/`FIELD_TYPE_NAMES`.
|
|
63
|
+
function keysOf<T extends string>(flags: Record<T, true>): readonly T[] {
|
|
64
|
+
return Object.keys(flags) as T[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Result types
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
export type PatternChangeIssue = {
|
|
72
|
+
readonly path: string;
|
|
73
|
+
readonly message: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type PatternChangesParseResult =
|
|
77
|
+
| { readonly ok: true; readonly changes: readonly PatternChange[] }
|
|
78
|
+
| { readonly ok: false; readonly issues: readonly PatternChangeIssue[] };
|
|
79
|
+
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// Shared primitives
|
|
82
|
+
// =============================================================================
|
|
83
|
+
|
|
84
|
+
function isPlainObjectOrSentinel(value: unknown): boolean {
|
|
85
|
+
return isRawRefSentinel(value) || isPlainObject(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const sourcePositionSchema: z.ZodType<SourcePosition> = z
|
|
89
|
+
.object({ line: z.number(), column: z.number() })
|
|
90
|
+
.strict();
|
|
91
|
+
|
|
92
|
+
const fullSourceLocationSchema: z.ZodType<SourceLocation> = z
|
|
93
|
+
.object({
|
|
94
|
+
file: z.string(),
|
|
95
|
+
start: sourcePositionSchema,
|
|
96
|
+
end: sourcePositionSchema,
|
|
97
|
+
raw: z.string(),
|
|
98
|
+
})
|
|
99
|
+
.strict();
|
|
100
|
+
|
|
101
|
+
// Partial object form — only `raw` is meaningful, `file`/`start`/`end` (if
|
|
102
|
+
// present) are accepted but discarded: a wire producer sending a
|
|
103
|
+
// half-populated location is telling us "this came from generated code, not
|
|
104
|
+
// a real file span" and `rawLoc` is exactly that contract.
|
|
105
|
+
const partialSourceLocationSchema = z
|
|
106
|
+
.object({
|
|
107
|
+
raw: z.string(),
|
|
108
|
+
file: z.string().optional(),
|
|
109
|
+
start: sourcePositionSchema.optional(),
|
|
110
|
+
end: sourcePositionSchema.optional(),
|
|
111
|
+
})
|
|
112
|
+
.strict()
|
|
113
|
+
.transform((value): SourceLocation => ({ ...SYNTHETIC_LOC, raw: value.raw }));
|
|
114
|
+
|
|
115
|
+
const stringSourceLocationSchema = z
|
|
116
|
+
.string()
|
|
117
|
+
.transform((raw): SourceLocation => ({ ...SYNTHETIC_LOC, raw }));
|
|
118
|
+
|
|
119
|
+
// `sourceBody`: string | {raw, file?, start?, end?} | full SourceLocation.
|
|
120
|
+
// Order matters — a fully-populated object must match `fullSourceLocationSchema`
|
|
121
|
+
// first so it round-trips unchanged; only a partial object falls through to
|
|
122
|
+
// the raw-collapsing variant.
|
|
123
|
+
const sourceBodySchema: z.ZodType<SourceLocation> = z.union([
|
|
124
|
+
fullSourceLocationSchema,
|
|
125
|
+
partialSourceLocationSchema,
|
|
126
|
+
stringSourceLocationSchema,
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
const nonEmptySourceBodySchema = sourceBodySchema.refine((loc) => loc.raw.trim().length > 0, {
|
|
130
|
+
message: "source.raw must be non-empty",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// The pattern-level `source` field: optional on the wire, defaults to the
|
|
134
|
+
// patcher's own synthetic placeholder (mirrors createFeaturePatcher's
|
|
135
|
+
// add{Kind} methods, which never have a real file span either).
|
|
136
|
+
const sourceFieldSchema: z.ZodType<SourceLocation> = sourceBodySchema
|
|
137
|
+
.optional()
|
|
138
|
+
.default(SYNTHETIC_LOC);
|
|
139
|
+
|
|
140
|
+
// `readVarargsOrArrayProp` (extractors/shared.ts) already types a mixed
|
|
141
|
+
// string/sentinel array as `readonly string[]` at its own extraction
|
|
142
|
+
// boundary (requires.featureNames, readsConfig.qualifiedKeys); mirror that
|
|
143
|
+
// same typed lie here instead of re-deriving a wider array type only to
|
|
144
|
+
// cast it back down.
|
|
145
|
+
const stringOrRawRefListSchema: z.ZodType<readonly string[]> = z.custom<readonly string[]>(
|
|
146
|
+
(value) =>
|
|
147
|
+
Array.isArray(value) && value.every((el) => typeof el === "string" || isRawRefSentinel(el)),
|
|
148
|
+
{ message: "must be an array of strings or unresolved references" },
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Runtime source of truth for the lifecycle-hook-type literals: the
|
|
152
|
+
// engine's own `LifecycleHookTypes` const object (engine/constants.ts) —
|
|
153
|
+
// `hook`'s `hookType` is that union plus the AST-only "validation" pseudo-
|
|
154
|
+
// type (r.hook(type: "validation", ...) has no runtime LifecycleHookType
|
|
155
|
+
// counterpart, it maps to a different registrar call).
|
|
156
|
+
const LIFECYCLE_HOOK_TYPE_VALUES = Object.values(
|
|
157
|
+
LifecycleHookTypes,
|
|
158
|
+
) as readonly LifecycleHookType[];
|
|
159
|
+
|
|
160
|
+
const hookTypeSchema = z.enum([...LIFECYCLE_HOOK_TYPE_VALUES, "validation"] as const);
|
|
161
|
+
const hookPhaseSchema = z.enum(keysOf<HookPhase>({ inTransaction: true, afterCommit: true }));
|
|
162
|
+
const httpRouteMethodSchema = z.enum(
|
|
163
|
+
keysOf<HttpRouteMethod>({
|
|
164
|
+
GET: true,
|
|
165
|
+
POST: true,
|
|
166
|
+
PUT: true,
|
|
167
|
+
PATCH: true,
|
|
168
|
+
DELETE: true,
|
|
169
|
+
HEAD: true,
|
|
170
|
+
OPTIONS: true,
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
const runInSchema = z.enum(keysOf<RunIn>({ api: true, worker: true, both: true }));
|
|
174
|
+
const claimKeyTypeSchema = z.enum(
|
|
175
|
+
keysOf<ClaimKeyType>({
|
|
176
|
+
string: true,
|
|
177
|
+
number: true,
|
|
178
|
+
boolean: true,
|
|
179
|
+
"string[]": true,
|
|
180
|
+
object: true,
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
const agentRiskSchema = z.enum(AGENT_RISK_VALUES as [AgentRisk, ...AgentRisk[]]);
|
|
184
|
+
const rateLimitPerSchema = z.enum(
|
|
185
|
+
keysOf<RateLimitPer>({
|
|
186
|
+
user: true,
|
|
187
|
+
tenant: true,
|
|
188
|
+
ip: true,
|
|
189
|
+
"user+handler": true,
|
|
190
|
+
"tenant+handler": true,
|
|
191
|
+
"ip+handler": true,
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
const mspDeliverySchema = z.enum(
|
|
195
|
+
keysOf<"shared" | "per-instance">({ shared: true, "per-instance": true }),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const escapeHatchSchema = z
|
|
199
|
+
.object({
|
|
200
|
+
reason: z.string().refine((s) => s.trim().length > 0, {
|
|
201
|
+
message: "reason must be non-empty",
|
|
202
|
+
}),
|
|
203
|
+
})
|
|
204
|
+
.strict();
|
|
205
|
+
|
|
206
|
+
const rawRefSentinelSchema = z.object({ __raw: z.string() }).strict();
|
|
207
|
+
|
|
208
|
+
// Adds the raw-ref-sentinel branch a handler header field accepts when the
|
|
209
|
+
// extractor couldn't resolve it to a literal (see extractors/hooks.ts).
|
|
210
|
+
function orRawRef<T extends z.ZodTypeAny>(schema: T) {
|
|
211
|
+
return z.union([schema, rawRefSentinelSchema]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const rateLimitDisabledSchema = z
|
|
215
|
+
.object({
|
|
216
|
+
disabled: z.literal(true),
|
|
217
|
+
reason: z.string().refine((s) => s.trim().length > 0, {
|
|
218
|
+
message: "reason must be non-empty",
|
|
219
|
+
}),
|
|
220
|
+
})
|
|
221
|
+
.strict();
|
|
222
|
+
|
|
223
|
+
const rateLimitOptionSchema = z
|
|
224
|
+
.object({
|
|
225
|
+
per: rateLimitPerSchema,
|
|
226
|
+
limit: z.number(),
|
|
227
|
+
windowSeconds: z.number(),
|
|
228
|
+
cost: z.number().optional(),
|
|
229
|
+
})
|
|
230
|
+
.strict();
|
|
231
|
+
|
|
232
|
+
const agentHandlerHintsSchema = z
|
|
233
|
+
.object({
|
|
234
|
+
expose: z.boolean().optional(),
|
|
235
|
+
risk: agentRiskSchema.optional(),
|
|
236
|
+
})
|
|
237
|
+
.strict();
|
|
238
|
+
|
|
239
|
+
// AccessRule — DEFAULT-DENY per its doc in types/handlers.ts; a malformed
|
|
240
|
+
// shape must be rejected, not silently narrowed to "no access" (that would
|
|
241
|
+
// hide the author's mistake instead of reporting it at the boundary).
|
|
242
|
+
const roleAccessRuleSchema = z
|
|
243
|
+
.object({
|
|
244
|
+
roles: z.array(z.string()),
|
|
245
|
+
personalData: z.literal("public-intake").optional(),
|
|
246
|
+
})
|
|
247
|
+
.strict();
|
|
248
|
+
|
|
249
|
+
const openToAllAccessRuleSchema = z
|
|
250
|
+
.object({
|
|
251
|
+
openToAll: z
|
|
252
|
+
.object({
|
|
253
|
+
reason: z.string().refine((s) => s.trim().length > 0, {
|
|
254
|
+
message: "openToAll.reason must be non-empty",
|
|
255
|
+
}),
|
|
256
|
+
personalData: z.literal("tenant-members").optional(),
|
|
257
|
+
})
|
|
258
|
+
.strict(),
|
|
259
|
+
})
|
|
260
|
+
.strict();
|
|
261
|
+
|
|
262
|
+
const accessRuleSchema = z.union([roleAccessRuleSchema, openToAllAccessRuleSchema]);
|
|
263
|
+
|
|
264
|
+
// Header keys per handler kind — drift pin against patch.ts's *HeaderKey
|
|
265
|
+
// types (via `satisfies`), and the source the schemas below derive from.
|
|
266
|
+
const WRITE_HANDLER_HEADER_SHAPE = {
|
|
267
|
+
access: accessRuleSchema,
|
|
268
|
+
description: z.string(),
|
|
269
|
+
agent: agentHandlerHintsSchema,
|
|
270
|
+
rateLimit: rateLimitOptionSchema,
|
|
271
|
+
unsafeSkipTransitionGuard: z.boolean(),
|
|
272
|
+
escapeHatch: escapeHatchSchema,
|
|
273
|
+
} satisfies Record<WriteHandlerHeaderKey, z.ZodTypeAny>;
|
|
274
|
+
const QUERY_HANDLER_HEADER_SHAPE = {
|
|
275
|
+
access: accessRuleSchema,
|
|
276
|
+
description: z.string(),
|
|
277
|
+
agent: agentHandlerHintsSchema,
|
|
278
|
+
rateLimit: rateLimitOptionSchema,
|
|
279
|
+
escapeHatch: escapeHatchSchema,
|
|
280
|
+
} satisfies Record<QueryHandlerHeaderKey, z.ZodTypeAny>;
|
|
281
|
+
const STREAM_HANDLER_HEADER_SHAPE = {
|
|
282
|
+
access: accessRuleSchema,
|
|
283
|
+
rateLimit: rateLimitOptionSchema,
|
|
284
|
+
escapeHatch: escapeHatchSchema,
|
|
285
|
+
} satisfies Record<StreamHandlerHeaderKey, z.ZodTypeAny>;
|
|
286
|
+
|
|
287
|
+
const WRITE_HANDLER_HEADER_KEYS = z.object(WRITE_HANDLER_HEADER_SHAPE).keyof().options;
|
|
288
|
+
const QUERY_HANDLER_HEADER_KEYS = z.object(QUERY_HANDLER_HEADER_SHAPE).keyof().options;
|
|
289
|
+
const STREAM_HANDLER_HEADER_KEYS = z.object(STREAM_HANDLER_HEADER_SHAPE).keyof().options;
|
|
290
|
+
|
|
291
|
+
// `update`'s set-schema stays exactly as strict as the extractor's own
|
|
292
|
+
// output: Designer/AI-authored values must be structured, never a raw
|
|
293
|
+
// reference or a partially-literal shape — those only ever come from
|
|
294
|
+
// statically parsed source, not from a `set` payload.
|
|
295
|
+
const WRITE_HANDLER_UPDATE_SET_SCHEMA = z.object(WRITE_HANDLER_HEADER_SHAPE).partial().strict();
|
|
296
|
+
const QUERY_HANDLER_UPDATE_SET_SCHEMA = z.object(QUERY_HANDLER_HEADER_SHAPE).partial().strict();
|
|
297
|
+
const STREAM_HANDLER_UPDATE_SET_SCHEMA = z.object(STREAM_HANDLER_HEADER_SHAPE).partial().strict();
|
|
298
|
+
|
|
299
|
+
// `unset` may name any header field except `access` (always required).
|
|
300
|
+
const WRITE_HANDLER_UPDATE_UNSET_ENUM = z
|
|
301
|
+
.object(WRITE_HANDLER_HEADER_SHAPE)
|
|
302
|
+
.omit({ access: true })
|
|
303
|
+
.keyof();
|
|
304
|
+
const QUERY_HANDLER_UPDATE_UNSET_ENUM = z
|
|
305
|
+
.object(QUERY_HANDLER_HEADER_SHAPE)
|
|
306
|
+
.omit({ access: true })
|
|
307
|
+
.keyof();
|
|
308
|
+
const STREAM_HANDLER_UPDATE_UNSET_ENUM = z
|
|
309
|
+
.object(STREAM_HANDLER_HEADER_SHAPE)
|
|
310
|
+
.omit({ access: true })
|
|
311
|
+
.keyof();
|
|
312
|
+
|
|
313
|
+
// Full patterns (add/replace) carry whatever the extractor parsed, which
|
|
314
|
+
// may be a RawRefSentinel or a disabled-rate-limit shape it can't resolve
|
|
315
|
+
// to a literal; widen only here, not on UPDATE_SET_SCHEMA above.
|
|
316
|
+
const WRITE_HANDLER_PATTERN_HEADER_SHAPE = {
|
|
317
|
+
...WRITE_HANDLER_HEADER_SHAPE,
|
|
318
|
+
access: orRawRef(accessRuleSchema),
|
|
319
|
+
agent: orRawRef(agentHandlerHintsSchema),
|
|
320
|
+
rateLimit: orRawRef(z.union([rateLimitOptionSchema, rateLimitDisabledSchema])),
|
|
321
|
+
escapeHatch: orRawRef(escapeHatchSchema),
|
|
322
|
+
};
|
|
323
|
+
const QUERY_HANDLER_PATTERN_HEADER_SHAPE = {
|
|
324
|
+
...QUERY_HANDLER_HEADER_SHAPE,
|
|
325
|
+
access: orRawRef(accessRuleSchema),
|
|
326
|
+
agent: orRawRef(agentHandlerHintsSchema),
|
|
327
|
+
rateLimit: orRawRef(z.union([rateLimitOptionSchema, rateLimitDisabledSchema])),
|
|
328
|
+
escapeHatch: orRawRef(escapeHatchSchema),
|
|
329
|
+
};
|
|
330
|
+
const STREAM_HANDLER_PATTERN_HEADER_SHAPE = {
|
|
331
|
+
...STREAM_HANDLER_HEADER_SHAPE,
|
|
332
|
+
access: orRawRef(accessRuleSchema),
|
|
333
|
+
rateLimit: orRawRef(z.union([rateLimitOptionSchema, rateLimitDisabledSchema])),
|
|
334
|
+
escapeHatch: orRawRef(escapeHatchSchema),
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
function requireHandlerBody(
|
|
338
|
+
val: {
|
|
339
|
+
readonly handlerName?: string;
|
|
340
|
+
readonly schemaSource?: SourceLocation;
|
|
341
|
+
readonly handlerBody?: SourceLocation;
|
|
342
|
+
readonly source: SourceLocation;
|
|
343
|
+
readonly [headerKey: string]: unknown;
|
|
344
|
+
},
|
|
345
|
+
ctx: z.RefinementCtx,
|
|
346
|
+
headerKeys: readonly string[],
|
|
347
|
+
): void {
|
|
348
|
+
const hasAny =
|
|
349
|
+
val.handlerName !== undefined ||
|
|
350
|
+
val.schemaSource !== undefined ||
|
|
351
|
+
val.handlerBody !== undefined;
|
|
352
|
+
if (!hasAny) {
|
|
353
|
+
if (val.source.raw.trim().length === 0) {
|
|
354
|
+
ctx.addIssue({
|
|
355
|
+
code: "custom",
|
|
356
|
+
path: ["source"],
|
|
357
|
+
message:
|
|
358
|
+
"an opaque handler reference (no handlerName/schemaSource/handlerBody) requires a non-empty source.raw",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
for (const key of headerKeys) {
|
|
362
|
+
if (val[key] !== undefined) {
|
|
363
|
+
ctx.addIssue({
|
|
364
|
+
code: "custom",
|
|
365
|
+
path: [key],
|
|
366
|
+
message:
|
|
367
|
+
"not rendered for an opaque handler reference; provide handlerName, schemaSource and handlerBody",
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// skip: opaque handler reference carries no name/schema/body to require
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (val.handlerName === undefined || val.handlerName.trim().length === 0) {
|
|
375
|
+
ctx.addIssue({
|
|
376
|
+
code: "custom",
|
|
377
|
+
path: ["handlerName"],
|
|
378
|
+
message: "handlerName is required and must be non-empty",
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (val.schemaSource === undefined) {
|
|
382
|
+
ctx.addIssue({ code: "custom", path: ["schemaSource"], message: "schemaSource is required" });
|
|
383
|
+
}
|
|
384
|
+
if (val.handlerBody === undefined) {
|
|
385
|
+
ctx.addIssue({ code: "custom", path: ["handlerBody"], message: "handlerBody is required" });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// =============================================================================
|
|
390
|
+
// Static patterns
|
|
391
|
+
// =============================================================================
|
|
392
|
+
|
|
393
|
+
// Deep-definition passthrough: accepts a plain object OR the extractor's
|
|
394
|
+
// own "couldn't resolve this statically" sentinel (RawRefSentinel), typed
|
|
395
|
+
// as `T` at the boundary — the same "typed lie" the extractors already
|
|
396
|
+
// make (e.g. round2.ts's `definition as EntityDefinition`), because a
|
|
397
|
+
// sentinel-bearing value is never actually shaped like `T` at runtime, but
|
|
398
|
+
// round-trips through `applyChanges`/the renderer unexamined either way.
|
|
399
|
+
function passthroughSchema<T>(): z.ZodType<T> {
|
|
400
|
+
return z.custom<T>(isPlainObjectOrSentinel, {
|
|
401
|
+
message: "must be a plain object or unresolved reference",
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const entityDefinitionSchema = passthroughSchema<EntityDefinition>().superRefine((value, ctx) => {
|
|
406
|
+
for (const entry of findUnknownEntityFieldTypes(value)) {
|
|
407
|
+
ctx.addIssue({
|
|
408
|
+
code: "custom",
|
|
409
|
+
path: ["fields", entry.fieldName, "type"],
|
|
410
|
+
message: describeUnknownFieldType(entry),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const entitySchema = z
|
|
416
|
+
.object({
|
|
417
|
+
kind: z.literal("entity"),
|
|
418
|
+
source: sourceFieldSchema,
|
|
419
|
+
entityName: z.string(),
|
|
420
|
+
definition: entityDefinitionSchema,
|
|
421
|
+
})
|
|
422
|
+
.strict();
|
|
423
|
+
|
|
424
|
+
const relationSchema = z
|
|
425
|
+
.object({
|
|
426
|
+
kind: z.literal("relation"),
|
|
427
|
+
source: sourceFieldSchema,
|
|
428
|
+
entityName: z.string(),
|
|
429
|
+
relationName: z.string(),
|
|
430
|
+
definition: passthroughSchema<RelationDefinition>(),
|
|
431
|
+
})
|
|
432
|
+
.strict();
|
|
433
|
+
|
|
434
|
+
const navSchema = z
|
|
435
|
+
.object({
|
|
436
|
+
kind: z.literal("nav"),
|
|
437
|
+
source: sourceFieldSchema,
|
|
438
|
+
definition: passthroughSchema<NavDefinition>(),
|
|
439
|
+
})
|
|
440
|
+
.strict();
|
|
441
|
+
|
|
442
|
+
const workspaceSchema = z
|
|
443
|
+
.object({
|
|
444
|
+
kind: z.literal("workspace"),
|
|
445
|
+
source: sourceFieldSchema,
|
|
446
|
+
definition: passthroughSchema<WorkspaceDefinition>(),
|
|
447
|
+
})
|
|
448
|
+
.strict();
|
|
449
|
+
|
|
450
|
+
const configSchema = z
|
|
451
|
+
.object({
|
|
452
|
+
kind: z.literal("config"),
|
|
453
|
+
source: sourceFieldSchema,
|
|
454
|
+
keys: passthroughSchema<Readonly<Record<string, ConfigKeyDefinition<ConfigKeyType>>>>(),
|
|
455
|
+
})
|
|
456
|
+
.strict();
|
|
457
|
+
|
|
458
|
+
const translationsSchema = z
|
|
459
|
+
.object({
|
|
460
|
+
kind: z.literal("translations"),
|
|
461
|
+
source: sourceFieldSchema,
|
|
462
|
+
keys: passthroughSchema<TranslationKeys>(),
|
|
463
|
+
})
|
|
464
|
+
.strict();
|
|
465
|
+
|
|
466
|
+
const requiresSchema = z
|
|
467
|
+
.object({
|
|
468
|
+
kind: z.literal("requires"),
|
|
469
|
+
source: sourceFieldSchema,
|
|
470
|
+
featureNames: stringOrRawRefListSchema,
|
|
471
|
+
})
|
|
472
|
+
.strict();
|
|
473
|
+
|
|
474
|
+
const optionalRequiresSchema = z
|
|
475
|
+
.object({
|
|
476
|
+
kind: z.literal("optionalRequires"),
|
|
477
|
+
source: sourceFieldSchema,
|
|
478
|
+
featureNames: stringOrRawRefListSchema,
|
|
479
|
+
})
|
|
480
|
+
.strict();
|
|
481
|
+
|
|
482
|
+
const systemScopeSchema = z
|
|
483
|
+
.object({
|
|
484
|
+
kind: z.literal("systemScope"),
|
|
485
|
+
source: sourceFieldSchema,
|
|
486
|
+
})
|
|
487
|
+
.strict();
|
|
488
|
+
|
|
489
|
+
const toggleableSchema = z
|
|
490
|
+
.object({
|
|
491
|
+
kind: z.literal("toggleable"),
|
|
492
|
+
source: sourceFieldSchema,
|
|
493
|
+
default: z.boolean(),
|
|
494
|
+
})
|
|
495
|
+
.strict();
|
|
496
|
+
|
|
497
|
+
const describeSchema = z
|
|
498
|
+
.object({
|
|
499
|
+
kind: z.literal("describe"),
|
|
500
|
+
source: sourceFieldSchema,
|
|
501
|
+
text: z.string().refine((s) => s.trim().length > 0, { message: "text must be non-empty" }),
|
|
502
|
+
})
|
|
503
|
+
.strict();
|
|
504
|
+
|
|
505
|
+
const uiHintsSchema = z
|
|
506
|
+
.object({
|
|
507
|
+
kind: z.literal("uiHints"),
|
|
508
|
+
source: nonEmptySourceBodySchema,
|
|
509
|
+
})
|
|
510
|
+
.strict();
|
|
511
|
+
|
|
512
|
+
const metricSchema = z
|
|
513
|
+
.object({
|
|
514
|
+
kind: z.literal("metric"),
|
|
515
|
+
source: sourceFieldSchema,
|
|
516
|
+
shortName: z.string(),
|
|
517
|
+
options: passthroughSchema<MetricOptions>(),
|
|
518
|
+
})
|
|
519
|
+
.strict();
|
|
520
|
+
|
|
521
|
+
const secretSchema = z
|
|
522
|
+
.object({
|
|
523
|
+
kind: z.literal("secret"),
|
|
524
|
+
source: sourceFieldSchema,
|
|
525
|
+
shortName: z.string(),
|
|
526
|
+
options: passthroughSchema<SecretOptions>(),
|
|
527
|
+
})
|
|
528
|
+
.strict();
|
|
529
|
+
|
|
530
|
+
const claimKeySchema = z
|
|
531
|
+
.object({
|
|
532
|
+
kind: z.literal("claimKey"),
|
|
533
|
+
source: sourceFieldSchema,
|
|
534
|
+
shortName: z.string(),
|
|
535
|
+
claimType: claimKeyTypeSchema,
|
|
536
|
+
})
|
|
537
|
+
.strict();
|
|
538
|
+
|
|
539
|
+
const referenceDataSchema = z
|
|
540
|
+
.object({
|
|
541
|
+
kind: z.literal("referenceData"),
|
|
542
|
+
source: sourceFieldSchema,
|
|
543
|
+
entityName: z.string(),
|
|
544
|
+
data: z.custom<ReferenceDataDef["data"]>(
|
|
545
|
+
(value) => Array.isArray(value) || isRawRefSentinel(value),
|
|
546
|
+
{ message: "data must be an array or unresolved reference" },
|
|
547
|
+
),
|
|
548
|
+
upsertKey: z.string().optional(),
|
|
549
|
+
})
|
|
550
|
+
.strict();
|
|
551
|
+
|
|
552
|
+
const readsConfigSchema = z
|
|
553
|
+
.object({
|
|
554
|
+
kind: z.literal("readsConfig"),
|
|
555
|
+
source: sourceFieldSchema,
|
|
556
|
+
qualifiedKeys: stringOrRawRefListSchema,
|
|
557
|
+
})
|
|
558
|
+
.strict();
|
|
559
|
+
|
|
560
|
+
const useExtensionSchema = z
|
|
561
|
+
.object({
|
|
562
|
+
kind: z.literal("useExtension"),
|
|
563
|
+
source: sourceFieldSchema,
|
|
564
|
+
extensionName: z.string(),
|
|
565
|
+
extensionNameRaw: z.string().optional(),
|
|
566
|
+
entityName: z.string(),
|
|
567
|
+
options: passthroughSchema<Readonly<Record<string, unknown>>>().optional(),
|
|
568
|
+
})
|
|
569
|
+
.strict();
|
|
570
|
+
|
|
571
|
+
const usesApiSchema = z
|
|
572
|
+
.object({
|
|
573
|
+
kind: z.literal("usesApi"),
|
|
574
|
+
source: sourceFieldSchema,
|
|
575
|
+
apiName: z.string(),
|
|
576
|
+
})
|
|
577
|
+
.strict();
|
|
578
|
+
|
|
579
|
+
const exposesApiSchema = z
|
|
580
|
+
.object({
|
|
581
|
+
kind: z.literal("exposesApi"),
|
|
582
|
+
source: sourceFieldSchema,
|
|
583
|
+
apiName: z.string(),
|
|
584
|
+
})
|
|
585
|
+
.strict();
|
|
586
|
+
|
|
587
|
+
const treeActionsSchema = z
|
|
588
|
+
.object({
|
|
589
|
+
kind: z.literal("treeActions"),
|
|
590
|
+
source: sourceFieldSchema,
|
|
591
|
+
definitions: passthroughSchema<Readonly<Record<string, TreeActionDef>>>(),
|
|
592
|
+
})
|
|
593
|
+
.strict();
|
|
594
|
+
|
|
595
|
+
// =============================================================================
|
|
596
|
+
// Mixed patterns
|
|
597
|
+
// =============================================================================
|
|
598
|
+
|
|
599
|
+
const screenSchema = z
|
|
600
|
+
.object({
|
|
601
|
+
kind: z.literal("screen"),
|
|
602
|
+
source: sourceFieldSchema,
|
|
603
|
+
definition: passthroughSchema<ScreenDefinition>(),
|
|
604
|
+
opaqueProps: z.record(z.string(), sourceBodySchema).optional().default({}),
|
|
605
|
+
})
|
|
606
|
+
.strict();
|
|
607
|
+
|
|
608
|
+
const writeHandlerSchema = z
|
|
609
|
+
.object({
|
|
610
|
+
kind: z.literal("writeHandler"),
|
|
611
|
+
source: sourceFieldSchema,
|
|
612
|
+
handlerName: z.string().optional(),
|
|
613
|
+
schemaSource: sourceBodySchema.optional(),
|
|
614
|
+
handlerBody: sourceBodySchema.optional(),
|
|
615
|
+
...z.object(WRITE_HANDLER_PATTERN_HEADER_SHAPE).partial().shape,
|
|
616
|
+
})
|
|
617
|
+
.strict()
|
|
618
|
+
.superRefine((val, ctx) => requireHandlerBody(val, ctx, WRITE_HANDLER_HEADER_KEYS));
|
|
619
|
+
|
|
620
|
+
const queryHandlerSchema = z
|
|
621
|
+
.object({
|
|
622
|
+
kind: z.literal("queryHandler"),
|
|
623
|
+
source: sourceFieldSchema,
|
|
624
|
+
handlerName: z.string().optional(),
|
|
625
|
+
schemaSource: sourceBodySchema.optional(),
|
|
626
|
+
handlerBody: sourceBodySchema.optional(),
|
|
627
|
+
...z.object(QUERY_HANDLER_PATTERN_HEADER_SHAPE).partial().shape,
|
|
628
|
+
})
|
|
629
|
+
.strict()
|
|
630
|
+
.superRefine((val, ctx) => requireHandlerBody(val, ctx, QUERY_HANDLER_HEADER_KEYS));
|
|
631
|
+
|
|
632
|
+
const streamHandlerSchema = z
|
|
633
|
+
.object({
|
|
634
|
+
kind: z.literal("streamHandler"),
|
|
635
|
+
source: sourceFieldSchema,
|
|
636
|
+
handlerName: z.string().optional(),
|
|
637
|
+
schemaSource: sourceBodySchema.optional(),
|
|
638
|
+
handlerBody: sourceBodySchema.optional(),
|
|
639
|
+
...z.object(STREAM_HANDLER_PATTERN_HEADER_SHAPE).partial().shape,
|
|
640
|
+
})
|
|
641
|
+
.strict()
|
|
642
|
+
.superRefine((val, ctx) => requireHandlerBody(val, ctx, STREAM_HANDLER_HEADER_KEYS));
|
|
643
|
+
|
|
644
|
+
const hookTargetSchema = z.union([
|
|
645
|
+
z.string(),
|
|
646
|
+
z.array(z.string()),
|
|
647
|
+
z.object({ allOf: z.string() }).strict(),
|
|
648
|
+
]);
|
|
649
|
+
|
|
650
|
+
const hookSchema = z
|
|
651
|
+
.object({
|
|
652
|
+
kind: z.literal("hook"),
|
|
653
|
+
source: sourceFieldSchema,
|
|
654
|
+
hookType: hookTypeSchema,
|
|
655
|
+
target: hookTargetSchema,
|
|
656
|
+
fnBody: sourceBodySchema,
|
|
657
|
+
phase: hookPhaseSchema.optional(),
|
|
658
|
+
escapeHatch: orRawRef(escapeHatchSchema).optional(),
|
|
659
|
+
})
|
|
660
|
+
.strict();
|
|
661
|
+
|
|
662
|
+
const jobSchema = z
|
|
663
|
+
.object({
|
|
664
|
+
kind: z.literal("job"),
|
|
665
|
+
source: sourceFieldSchema,
|
|
666
|
+
jobName: z.string(),
|
|
667
|
+
options: passthroughSchema<Omit<JobDefinition, "name" | "handler">>(),
|
|
668
|
+
handlerBody: sourceBodySchema,
|
|
669
|
+
})
|
|
670
|
+
.strict();
|
|
671
|
+
|
|
672
|
+
const notificationSchema = z
|
|
673
|
+
.object({
|
|
674
|
+
kind: z.literal("notification"),
|
|
675
|
+
source: sourceFieldSchema,
|
|
676
|
+
notificationName: z.string(),
|
|
677
|
+
trigger: z.object({ on: z.string() }).strict(),
|
|
678
|
+
recipientBody: sourceBodySchema,
|
|
679
|
+
dataBody: sourceBodySchema,
|
|
680
|
+
templates: z.record(z.string(), sourceBodySchema).optional(),
|
|
681
|
+
})
|
|
682
|
+
.strict();
|
|
683
|
+
|
|
684
|
+
const authClaimsSchema = z
|
|
685
|
+
.object({
|
|
686
|
+
kind: z.literal("authClaims"),
|
|
687
|
+
source: sourceFieldSchema,
|
|
688
|
+
fnBody: sourceBodySchema,
|
|
689
|
+
})
|
|
690
|
+
.strict();
|
|
691
|
+
|
|
692
|
+
const httpRouteSchema = z
|
|
693
|
+
.object({
|
|
694
|
+
kind: z.literal("httpRoute"),
|
|
695
|
+
source: sourceFieldSchema,
|
|
696
|
+
method: httpRouteMethodSchema,
|
|
697
|
+
path: z.string(),
|
|
698
|
+
anonymous: z.boolean(),
|
|
699
|
+
handlerBody: sourceBodySchema,
|
|
700
|
+
})
|
|
701
|
+
.strict();
|
|
702
|
+
|
|
703
|
+
const projectionSchema = z
|
|
704
|
+
.object({
|
|
705
|
+
kind: z.literal("projection"),
|
|
706
|
+
source: sourceFieldSchema,
|
|
707
|
+
name: z.string(),
|
|
708
|
+
sourceEntity: z.union([z.string(), z.array(z.string())]),
|
|
709
|
+
applyBodies: z.record(z.string(), sourceBodySchema),
|
|
710
|
+
})
|
|
711
|
+
.strict();
|
|
712
|
+
|
|
713
|
+
const mspErrorPolicySchema = z.object({ skipApplyErrors: z.boolean().optional() }).strict();
|
|
714
|
+
const mspErrorModeSchema = z
|
|
715
|
+
.object({
|
|
716
|
+
continuous: mspErrorPolicySchema.optional(),
|
|
717
|
+
rebuild: mspErrorPolicySchema.optional(),
|
|
718
|
+
})
|
|
719
|
+
.strict();
|
|
720
|
+
|
|
721
|
+
const multiStreamProjectionSchema = z
|
|
722
|
+
.object({
|
|
723
|
+
kind: z.literal("multiStreamProjection"),
|
|
724
|
+
source: sourceFieldSchema,
|
|
725
|
+
name: z.string(),
|
|
726
|
+
applyBodies: z.record(z.string(), sourceBodySchema),
|
|
727
|
+
errorMode: mspErrorModeSchema.optional(),
|
|
728
|
+
runIn: runInSchema.optional(),
|
|
729
|
+
delivery: mspDeliverySchema.optional(),
|
|
730
|
+
})
|
|
731
|
+
.strict();
|
|
732
|
+
|
|
733
|
+
const defineEventSchema = z
|
|
734
|
+
.object({
|
|
735
|
+
kind: z.literal("defineEvent"),
|
|
736
|
+
source: sourceFieldSchema,
|
|
737
|
+
eventName: z.string(),
|
|
738
|
+
eventNameRaw: z.string().optional(),
|
|
739
|
+
schemaSource: sourceBodySchema,
|
|
740
|
+
version: z.number().optional(),
|
|
741
|
+
piiFields: sourceBodySchema,
|
|
742
|
+
migrations: z.record(z.string(), sourceBodySchema).optional(),
|
|
743
|
+
})
|
|
744
|
+
.strict();
|
|
745
|
+
|
|
746
|
+
const extendsRegistrarSchema = z
|
|
747
|
+
.object({
|
|
748
|
+
kind: z.literal("extendsRegistrar"),
|
|
749
|
+
source: sourceFieldSchema,
|
|
750
|
+
extensionName: z.string(),
|
|
751
|
+
extensionNameRaw: z.string().optional(),
|
|
752
|
+
defBody: sourceBodySchema,
|
|
753
|
+
})
|
|
754
|
+
.strict();
|
|
755
|
+
|
|
756
|
+
const envSchemaSchema = z
|
|
757
|
+
.object({
|
|
758
|
+
kind: z.literal("envSchema"),
|
|
759
|
+
source: sourceFieldSchema,
|
|
760
|
+
schemaBody: sourceBodySchema,
|
|
761
|
+
})
|
|
762
|
+
.strict();
|
|
763
|
+
|
|
764
|
+
const aiStepOpaqueArgsSchema = z.custom<{ readonly __raw: string }>(isRawRefSentinel, {
|
|
765
|
+
message: "must be an unresolved reference ({ __raw })",
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
const aiStepPolicySchema = z
|
|
769
|
+
.object({
|
|
770
|
+
enabled: z.boolean(),
|
|
771
|
+
providerId: z.string().optional(),
|
|
772
|
+
model: z.string().optional(),
|
|
773
|
+
params: z.custom<Record<string, unknown>>(isPlainObject, {
|
|
774
|
+
message: "params must be a plain object",
|
|
775
|
+
}),
|
|
776
|
+
})
|
|
777
|
+
.strict();
|
|
778
|
+
|
|
779
|
+
const stringOrAiOpaqueSchema = z.union([z.string(), aiStepOpaqueArgsSchema]);
|
|
780
|
+
const aiStepPolicyOrOpaqueSchema = z.union([aiStepPolicySchema, aiStepOpaqueArgsSchema]);
|
|
781
|
+
|
|
782
|
+
const aiGenerateSchema = z
|
|
783
|
+
.object({
|
|
784
|
+
kind: z.literal("ai.generate"),
|
|
785
|
+
source: sourceFieldSchema,
|
|
786
|
+
argsSource: aiStepOpaqueArgsSchema.optional(),
|
|
787
|
+
stepKey: stringOrAiOpaqueSchema.optional(),
|
|
788
|
+
promptKey: stringOrAiOpaqueSchema.optional(),
|
|
789
|
+
promptFallback: stringOrAiOpaqueSchema.optional(),
|
|
790
|
+
defaults: aiStepPolicyOrOpaqueSchema.optional(),
|
|
791
|
+
paramsSchemaSource: sourceBodySchema.optional(),
|
|
792
|
+
inputBody: sourceBodySchema.optional(),
|
|
793
|
+
})
|
|
794
|
+
.strict();
|
|
795
|
+
|
|
796
|
+
const aiExtractSchema = z
|
|
797
|
+
.object({
|
|
798
|
+
kind: z.literal("ai.extract"),
|
|
799
|
+
source: sourceFieldSchema,
|
|
800
|
+
argsSource: aiStepOpaqueArgsSchema.optional(),
|
|
801
|
+
stepKey: stringOrAiOpaqueSchema.optional(),
|
|
802
|
+
promptKey: stringOrAiOpaqueSchema.optional(),
|
|
803
|
+
promptFallback: stringOrAiOpaqueSchema.optional(),
|
|
804
|
+
defaults: aiStepPolicyOrOpaqueSchema.optional(),
|
|
805
|
+
paramsSchemaSource: sourceBodySchema.optional(),
|
|
806
|
+
outputSchemaSource: sourceBodySchema.optional(),
|
|
807
|
+
instructionsBody: sourceBodySchema.optional(),
|
|
808
|
+
documentBody: sourceBodySchema.optional(),
|
|
809
|
+
})
|
|
810
|
+
.strict();
|
|
811
|
+
|
|
812
|
+
const aiClassifySchema = z
|
|
813
|
+
.object({
|
|
814
|
+
kind: z.literal("ai.classify"),
|
|
815
|
+
source: sourceFieldSchema,
|
|
816
|
+
argsSource: aiStepOpaqueArgsSchema.optional(),
|
|
817
|
+
stepKey: stringOrAiOpaqueSchema.optional(),
|
|
818
|
+
promptKey: stringOrAiOpaqueSchema.optional(),
|
|
819
|
+
promptFallback: stringOrAiOpaqueSchema.optional(),
|
|
820
|
+
defaults: aiStepPolicyOrOpaqueSchema.optional(),
|
|
821
|
+
paramsSchemaSource: sourceBodySchema.optional(),
|
|
822
|
+
actions: z.array(z.object({ type: z.string(), description: z.string() }).strict()).optional(),
|
|
823
|
+
inputBody: sourceBodySchema.optional(),
|
|
824
|
+
})
|
|
825
|
+
.strict();
|
|
826
|
+
|
|
827
|
+
// =============================================================================
|
|
828
|
+
// Catch-all
|
|
829
|
+
// =============================================================================
|
|
830
|
+
|
|
831
|
+
const unknownPatternSchema = z
|
|
832
|
+
.object({
|
|
833
|
+
kind: z.literal("unknown"),
|
|
834
|
+
source: nonEmptySourceBodySchema,
|
|
835
|
+
methodName: z.string(),
|
|
836
|
+
})
|
|
837
|
+
.strict();
|
|
838
|
+
|
|
839
|
+
// =============================================================================
|
|
840
|
+
// Pattern union — Record keyed by kind doubles as the exhaustiveness pin
|
|
841
|
+
// consumed by the compile-time typtest (a missing/renamed kind fails to
|
|
842
|
+
// compile here, same contract as patterns.ts's own switch statements).
|
|
843
|
+
// =============================================================================
|
|
844
|
+
|
|
845
|
+
export const PATTERN_SCHEMAS_BY_KIND = {
|
|
846
|
+
entity: entitySchema,
|
|
847
|
+
relation: relationSchema,
|
|
848
|
+
nav: navSchema,
|
|
849
|
+
workspace: workspaceSchema,
|
|
850
|
+
config: configSchema,
|
|
851
|
+
translations: translationsSchema,
|
|
852
|
+
requires: requiresSchema,
|
|
853
|
+
optionalRequires: optionalRequiresSchema,
|
|
854
|
+
systemScope: systemScopeSchema,
|
|
855
|
+
toggleable: toggleableSchema,
|
|
856
|
+
describe: describeSchema,
|
|
857
|
+
uiHints: uiHintsSchema,
|
|
858
|
+
metric: metricSchema,
|
|
859
|
+
secret: secretSchema,
|
|
860
|
+
claimKey: claimKeySchema,
|
|
861
|
+
referenceData: referenceDataSchema,
|
|
862
|
+
readsConfig: readsConfigSchema,
|
|
863
|
+
useExtension: useExtensionSchema,
|
|
864
|
+
usesApi: usesApiSchema,
|
|
865
|
+
exposesApi: exposesApiSchema,
|
|
866
|
+
treeActions: treeActionsSchema,
|
|
867
|
+
screen: screenSchema,
|
|
868
|
+
writeHandler: writeHandlerSchema,
|
|
869
|
+
queryHandler: queryHandlerSchema,
|
|
870
|
+
streamHandler: streamHandlerSchema,
|
|
871
|
+
hook: hookSchema,
|
|
872
|
+
job: jobSchema,
|
|
873
|
+
notification: notificationSchema,
|
|
874
|
+
authClaims: authClaimsSchema,
|
|
875
|
+
httpRoute: httpRouteSchema,
|
|
876
|
+
projection: projectionSchema,
|
|
877
|
+
multiStreamProjection: multiStreamProjectionSchema,
|
|
878
|
+
defineEvent: defineEventSchema,
|
|
879
|
+
extendsRegistrar: extendsRegistrarSchema,
|
|
880
|
+
envSchema: envSchemaSchema,
|
|
881
|
+
"ai.generate": aiGenerateSchema,
|
|
882
|
+
"ai.extract": aiExtractSchema,
|
|
883
|
+
"ai.classify": aiClassifySchema,
|
|
884
|
+
unknown: unknownPatternSchema,
|
|
885
|
+
} satisfies Record<FeaturePatternKind, z.ZodTypeAny>;
|
|
886
|
+
|
|
887
|
+
const patternSchema = z.discriminatedUnion("kind", [
|
|
888
|
+
entitySchema,
|
|
889
|
+
relationSchema,
|
|
890
|
+
navSchema,
|
|
891
|
+
workspaceSchema,
|
|
892
|
+
configSchema,
|
|
893
|
+
translationsSchema,
|
|
894
|
+
requiresSchema,
|
|
895
|
+
optionalRequiresSchema,
|
|
896
|
+
systemScopeSchema,
|
|
897
|
+
toggleableSchema,
|
|
898
|
+
describeSchema,
|
|
899
|
+
uiHintsSchema,
|
|
900
|
+
metricSchema,
|
|
901
|
+
secretSchema,
|
|
902
|
+
claimKeySchema,
|
|
903
|
+
referenceDataSchema,
|
|
904
|
+
readsConfigSchema,
|
|
905
|
+
useExtensionSchema,
|
|
906
|
+
usesApiSchema,
|
|
907
|
+
exposesApiSchema,
|
|
908
|
+
treeActionsSchema,
|
|
909
|
+
screenSchema,
|
|
910
|
+
writeHandlerSchema,
|
|
911
|
+
queryHandlerSchema,
|
|
912
|
+
streamHandlerSchema,
|
|
913
|
+
hookSchema,
|
|
914
|
+
jobSchema,
|
|
915
|
+
notificationSchema,
|
|
916
|
+
authClaimsSchema,
|
|
917
|
+
httpRouteSchema,
|
|
918
|
+
projectionSchema,
|
|
919
|
+
multiStreamProjectionSchema,
|
|
920
|
+
defineEventSchema,
|
|
921
|
+
extendsRegistrarSchema,
|
|
922
|
+
envSchemaSchema,
|
|
923
|
+
aiGenerateSchema,
|
|
924
|
+
aiExtractSchema,
|
|
925
|
+
aiClassifySchema,
|
|
926
|
+
unknownPatternSchema,
|
|
927
|
+
]);
|
|
928
|
+
|
|
929
|
+
// =============================================================================
|
|
930
|
+
// PatternId union
|
|
931
|
+
// =============================================================================
|
|
932
|
+
|
|
933
|
+
const patternIdEntitySchema = z
|
|
934
|
+
.object({ kind: z.literal("entity"), entityName: z.string() })
|
|
935
|
+
.strict();
|
|
936
|
+
const patternIdRelationSchema = z
|
|
937
|
+
.object({ kind: z.literal("relation"), entityName: z.string(), relationName: z.string() })
|
|
938
|
+
.strict();
|
|
939
|
+
const patternIdNavSchema = z.object({ kind: z.literal("nav"), id: z.string() }).strict();
|
|
940
|
+
const patternIdWorkspaceSchema = z
|
|
941
|
+
.object({ kind: z.literal("workspace"), id: z.string() })
|
|
942
|
+
.strict();
|
|
943
|
+
const patternIdScreenSchema = z.object({ kind: z.literal("screen"), id: z.string() }).strict();
|
|
944
|
+
const patternIdWriteHandlerSchema = z
|
|
945
|
+
.object({ kind: z.literal("writeHandler"), handlerName: z.string() })
|
|
946
|
+
.strict();
|
|
947
|
+
const patternIdQueryHandlerSchema = z
|
|
948
|
+
.object({ kind: z.literal("queryHandler"), handlerName: z.string() })
|
|
949
|
+
.strict();
|
|
950
|
+
const patternIdStreamHandlerSchema = z
|
|
951
|
+
.object({ kind: z.literal("streamHandler"), handlerName: z.string() })
|
|
952
|
+
.strict();
|
|
953
|
+
const patternIdHookSchema = z
|
|
954
|
+
.object({
|
|
955
|
+
kind: z.literal("hook"),
|
|
956
|
+
hookType: z.string(),
|
|
957
|
+
target: z.union([z.string(), z.object({ allOf: z.string() }).strict()]),
|
|
958
|
+
})
|
|
959
|
+
.strict();
|
|
960
|
+
const patternIdMetricSchema = z
|
|
961
|
+
.object({ kind: z.literal("metric"), shortName: z.string() })
|
|
962
|
+
.strict();
|
|
963
|
+
const patternIdSecretSchema = z
|
|
964
|
+
.object({ kind: z.literal("secret"), shortName: z.string() })
|
|
965
|
+
.strict();
|
|
966
|
+
const patternIdClaimKeySchema = z
|
|
967
|
+
.object({ kind: z.literal("claimKey"), shortName: z.string() })
|
|
968
|
+
.strict();
|
|
969
|
+
const patternIdReferenceDataSchema = z
|
|
970
|
+
.object({ kind: z.literal("referenceData"), entityName: z.string() })
|
|
971
|
+
.strict();
|
|
972
|
+
const patternIdUseExtensionSchema = z
|
|
973
|
+
.object({ kind: z.literal("useExtension"), extensionName: z.string(), entityName: z.string() })
|
|
974
|
+
.strict();
|
|
975
|
+
const patternIdJobSchema = z.object({ kind: z.literal("job"), jobName: z.string() }).strict();
|
|
976
|
+
const patternIdNotificationSchema = z
|
|
977
|
+
.object({ kind: z.literal("notification"), notificationName: z.string() })
|
|
978
|
+
.strict();
|
|
979
|
+
const patternIdHttpRouteSchema = z
|
|
980
|
+
.object({ kind: z.literal("httpRoute"), method: z.string(), path: z.string() })
|
|
981
|
+
.strict();
|
|
982
|
+
const patternIdProjectionSchema = z
|
|
983
|
+
.object({ kind: z.literal("projection"), name: z.string() })
|
|
984
|
+
.strict();
|
|
985
|
+
const patternIdMultiStreamProjectionSchema = z
|
|
986
|
+
.object({ kind: z.literal("multiStreamProjection"), name: z.string() })
|
|
987
|
+
.strict();
|
|
988
|
+
const patternIdDefineEventSchema = z
|
|
989
|
+
.object({ kind: z.literal("defineEvent"), eventName: z.string() })
|
|
990
|
+
.strict();
|
|
991
|
+
const patternIdExtendsRegistrarSchema = z
|
|
992
|
+
.object({ kind: z.literal("extendsRegistrar"), extensionName: z.string() })
|
|
993
|
+
.strict();
|
|
994
|
+
const patternIdAiGenerateSchema = z
|
|
995
|
+
.object({ kind: z.literal("ai.generate"), stepKey: z.string() })
|
|
996
|
+
.strict();
|
|
997
|
+
const patternIdAiExtractSchema = z
|
|
998
|
+
.object({ kind: z.literal("ai.extract"), stepKey: z.string() })
|
|
999
|
+
.strict();
|
|
1000
|
+
const patternIdAiClassifySchema = z
|
|
1001
|
+
.object({ kind: z.literal("ai.classify"), stepKey: z.string() })
|
|
1002
|
+
.strict();
|
|
1003
|
+
const patternIdRequiresSchema = z.object({ kind: z.literal("requires") }).strict();
|
|
1004
|
+
const patternIdOptionalRequiresSchema = z.object({ kind: z.literal("optionalRequires") }).strict();
|
|
1005
|
+
const patternIdReadsConfigSchema = z.object({ kind: z.literal("readsConfig") }).strict();
|
|
1006
|
+
const patternIdSystemScopeSchema = z.object({ kind: z.literal("systemScope") }).strict();
|
|
1007
|
+
const patternIdToggleableSchema = z.object({ kind: z.literal("toggleable") }).strict();
|
|
1008
|
+
const patternIdDescribeSchema = z.object({ kind: z.literal("describe") }).strict();
|
|
1009
|
+
const patternIdUiHintsSchema = z.object({ kind: z.literal("uiHints") }).strict();
|
|
1010
|
+
const patternIdConfigSchema = z.object({ kind: z.literal("config") }).strict();
|
|
1011
|
+
const patternIdTranslationsSchema = z.object({ kind: z.literal("translations") }).strict();
|
|
1012
|
+
const patternIdAuthClaimsSchema = z.object({ kind: z.literal("authClaims") }).strict();
|
|
1013
|
+
const patternIdTreeActionsSchema = z.object({ kind: z.literal("treeActions") }).strict();
|
|
1014
|
+
|
|
1015
|
+
export const PATTERN_ID_SCHEMAS_BY_KIND = {
|
|
1016
|
+
entity: patternIdEntitySchema,
|
|
1017
|
+
relation: patternIdRelationSchema,
|
|
1018
|
+
nav: patternIdNavSchema,
|
|
1019
|
+
workspace: patternIdWorkspaceSchema,
|
|
1020
|
+
screen: patternIdScreenSchema,
|
|
1021
|
+
writeHandler: patternIdWriteHandlerSchema,
|
|
1022
|
+
queryHandler: patternIdQueryHandlerSchema,
|
|
1023
|
+
streamHandler: patternIdStreamHandlerSchema,
|
|
1024
|
+
hook: patternIdHookSchema,
|
|
1025
|
+
metric: patternIdMetricSchema,
|
|
1026
|
+
secret: patternIdSecretSchema,
|
|
1027
|
+
claimKey: patternIdClaimKeySchema,
|
|
1028
|
+
referenceData: patternIdReferenceDataSchema,
|
|
1029
|
+
useExtension: patternIdUseExtensionSchema,
|
|
1030
|
+
job: patternIdJobSchema,
|
|
1031
|
+
notification: patternIdNotificationSchema,
|
|
1032
|
+
httpRoute: patternIdHttpRouteSchema,
|
|
1033
|
+
projection: patternIdProjectionSchema,
|
|
1034
|
+
multiStreamProjection: patternIdMultiStreamProjectionSchema,
|
|
1035
|
+
defineEvent: patternIdDefineEventSchema,
|
|
1036
|
+
extendsRegistrar: patternIdExtendsRegistrarSchema,
|
|
1037
|
+
"ai.generate": patternIdAiGenerateSchema,
|
|
1038
|
+
"ai.extract": patternIdAiExtractSchema,
|
|
1039
|
+
"ai.classify": patternIdAiClassifySchema,
|
|
1040
|
+
requires: patternIdRequiresSchema,
|
|
1041
|
+
optionalRequires: patternIdOptionalRequiresSchema,
|
|
1042
|
+
readsConfig: patternIdReadsConfigSchema,
|
|
1043
|
+
systemScope: patternIdSystemScopeSchema,
|
|
1044
|
+
toggleable: patternIdToggleableSchema,
|
|
1045
|
+
describe: patternIdDescribeSchema,
|
|
1046
|
+
uiHints: patternIdUiHintsSchema,
|
|
1047
|
+
config: patternIdConfigSchema,
|
|
1048
|
+
translations: patternIdTranslationsSchema,
|
|
1049
|
+
authClaims: patternIdAuthClaimsSchema,
|
|
1050
|
+
treeActions: patternIdTreeActionsSchema,
|
|
1051
|
+
} satisfies Record<PatternId["kind"], z.ZodTypeAny>;
|
|
1052
|
+
|
|
1053
|
+
const patternIdSchema = z.discriminatedUnion("kind", [
|
|
1054
|
+
patternIdEntitySchema,
|
|
1055
|
+
patternIdRelationSchema,
|
|
1056
|
+
patternIdNavSchema,
|
|
1057
|
+
patternIdWorkspaceSchema,
|
|
1058
|
+
patternIdScreenSchema,
|
|
1059
|
+
patternIdWriteHandlerSchema,
|
|
1060
|
+
patternIdQueryHandlerSchema,
|
|
1061
|
+
patternIdStreamHandlerSchema,
|
|
1062
|
+
patternIdHookSchema,
|
|
1063
|
+
patternIdMetricSchema,
|
|
1064
|
+
patternIdSecretSchema,
|
|
1065
|
+
patternIdClaimKeySchema,
|
|
1066
|
+
patternIdReferenceDataSchema,
|
|
1067
|
+
patternIdUseExtensionSchema,
|
|
1068
|
+
patternIdJobSchema,
|
|
1069
|
+
patternIdNotificationSchema,
|
|
1070
|
+
patternIdHttpRouteSchema,
|
|
1071
|
+
patternIdProjectionSchema,
|
|
1072
|
+
patternIdMultiStreamProjectionSchema,
|
|
1073
|
+
patternIdDefineEventSchema,
|
|
1074
|
+
patternIdExtendsRegistrarSchema,
|
|
1075
|
+
patternIdAiGenerateSchema,
|
|
1076
|
+
patternIdAiExtractSchema,
|
|
1077
|
+
patternIdAiClassifySchema,
|
|
1078
|
+
patternIdRequiresSchema,
|
|
1079
|
+
patternIdOptionalRequiresSchema,
|
|
1080
|
+
patternIdReadsConfigSchema,
|
|
1081
|
+
patternIdSystemScopeSchema,
|
|
1082
|
+
patternIdToggleableSchema,
|
|
1083
|
+
patternIdDescribeSchema,
|
|
1084
|
+
patternIdUiHintsSchema,
|
|
1085
|
+
patternIdConfigSchema,
|
|
1086
|
+
patternIdTranslationsSchema,
|
|
1087
|
+
patternIdAuthClaimsSchema,
|
|
1088
|
+
patternIdTreeActionsSchema,
|
|
1089
|
+
]);
|
|
1090
|
+
|
|
1091
|
+
// =============================================================================
|
|
1092
|
+
// Change union — `rationale` is accepted (optional, on every member) and
|
|
1093
|
+
// dropped when the final PatternChange is assembled below (plain field
|
|
1094
|
+
// selection, not a schema-level transform — see file-header rationale).
|
|
1095
|
+
// =============================================================================
|
|
1096
|
+
|
|
1097
|
+
const addChangeSchema = z
|
|
1098
|
+
.object({
|
|
1099
|
+
op: z.literal("add"),
|
|
1100
|
+
pattern: patternSchema,
|
|
1101
|
+
rationale: z.string().optional(),
|
|
1102
|
+
})
|
|
1103
|
+
.strict();
|
|
1104
|
+
|
|
1105
|
+
const replaceChangeSchema = z
|
|
1106
|
+
.object({
|
|
1107
|
+
op: z.literal("replace"),
|
|
1108
|
+
id: patternIdSchema,
|
|
1109
|
+
pattern: patternSchema,
|
|
1110
|
+
rationale: z.string().optional(),
|
|
1111
|
+
})
|
|
1112
|
+
.strict();
|
|
1113
|
+
|
|
1114
|
+
const removeChangeSchema = z
|
|
1115
|
+
.object({
|
|
1116
|
+
op: z.literal("remove"),
|
|
1117
|
+
id: patternIdSchema,
|
|
1118
|
+
rationale: z.string().optional(),
|
|
1119
|
+
})
|
|
1120
|
+
.strict();
|
|
1121
|
+
|
|
1122
|
+
// `set`/`unset` are validated loosely here; the real per-kind check happens
|
|
1123
|
+
// in a second pass once `id.kind` is known (see parseUpdateChange).
|
|
1124
|
+
const updateChangeSchema = z
|
|
1125
|
+
.object({
|
|
1126
|
+
op: z.literal("update"),
|
|
1127
|
+
id: patternIdSchema,
|
|
1128
|
+
set: z.record(z.string(), z.unknown()).optional(),
|
|
1129
|
+
unset: z.array(z.string()).optional(),
|
|
1130
|
+
rationale: z.string().optional(),
|
|
1131
|
+
})
|
|
1132
|
+
.strict();
|
|
1133
|
+
|
|
1134
|
+
const changeSchema = z.discriminatedUnion("op", [
|
|
1135
|
+
addChangeSchema,
|
|
1136
|
+
replaceChangeSchema,
|
|
1137
|
+
removeChangeSchema,
|
|
1138
|
+
updateChangeSchema,
|
|
1139
|
+
]);
|
|
1140
|
+
|
|
1141
|
+
function prefixIssuePath(
|
|
1142
|
+
issues: readonly z.core.$ZodIssue[],
|
|
1143
|
+
prefix: PropertyKey,
|
|
1144
|
+
): z.core.$ZodIssue[] {
|
|
1145
|
+
return issues.map((issue) => ({ ...issue, path: [prefix, ...issue.path] }));
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Shared per-`unset[j]` checks ("access" is fixed-required, overlap with
|
|
1149
|
+
// `set`) ahead of the kind-specific enum parse in parseHeaderSetAndUnset.
|
|
1150
|
+
function unsetElementIssue(
|
|
1151
|
+
key: string,
|
|
1152
|
+
j: number,
|
|
1153
|
+
index: number,
|
|
1154
|
+
setKeys: ReadonlySet<string>,
|
|
1155
|
+
): PatternChangeIssue | undefined {
|
|
1156
|
+
if (key === "access") {
|
|
1157
|
+
return {
|
|
1158
|
+
path: `changes[${index}].unset[${j}]`,
|
|
1159
|
+
message: "access is required and cannot be unset",
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
if (setKeys.has(key)) {
|
|
1163
|
+
return { path: `changes[${index}].unset[${j}]`, message: "key is also in set" };
|
|
1164
|
+
}
|
|
1165
|
+
return undefined;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Shared set/unset validation for one handler kind's Set-schema/Unset-enum —
|
|
1169
|
+
// the only thing that differs per kind (see the switch below).
|
|
1170
|
+
function parseHeaderSetAndUnset<
|
|
1171
|
+
SetSchema extends z.ZodType<Record<string, unknown>>,
|
|
1172
|
+
UnsetKey extends string,
|
|
1173
|
+
>(
|
|
1174
|
+
setSchema: SetSchema,
|
|
1175
|
+
unsetEnum: z.ZodType<UnsetKey>,
|
|
1176
|
+
kindLabel: string,
|
|
1177
|
+
value: z.output<typeof updateChangeSchema>,
|
|
1178
|
+
index: number,
|
|
1179
|
+
rawItem: unknown,
|
|
1180
|
+
):
|
|
1181
|
+
| { readonly ok: true; readonly set: z.output<SetSchema>; readonly unset: readonly UnsetKey[] }
|
|
1182
|
+
| { readonly ok: false; readonly issues: readonly PatternChangeIssue[] } {
|
|
1183
|
+
const issues: PatternChangeIssue[] = [];
|
|
1184
|
+
const setResult = setSchema.safeParse(value.set ?? {});
|
|
1185
|
+
if (!setResult.success) {
|
|
1186
|
+
issues.push(...formatZodIssues(prefixIssuePath(setResult.error.issues, "set"), index, rawItem));
|
|
1187
|
+
}
|
|
1188
|
+
const setKeys = new Set(Object.keys(value.set ?? {}));
|
|
1189
|
+
const unsetInput = value.unset ?? [];
|
|
1190
|
+
const validatedUnset: UnsetKey[] = [];
|
|
1191
|
+
unsetInput.forEach((key, j) => {
|
|
1192
|
+
const preIssue = unsetElementIssue(key, j, index, setKeys);
|
|
1193
|
+
if (preIssue) {
|
|
1194
|
+
issues.push(preIssue);
|
|
1195
|
+
// skip: already reported, no further checks apply to this element
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const parsed = unsetEnum.safeParse(key);
|
|
1199
|
+
if (!parsed.success) {
|
|
1200
|
+
issues.push({
|
|
1201
|
+
path: `changes[${index}].unset[${j}]`,
|
|
1202
|
+
message: `"${key}" is not a header field of ${kindLabel}`,
|
|
1203
|
+
});
|
|
1204
|
+
// skip: already reported, no further checks apply to this element
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
validatedUnset.push(parsed.data);
|
|
1208
|
+
});
|
|
1209
|
+
const hasSet = value.set !== undefined && Object.keys(value.set).length > 0;
|
|
1210
|
+
if (!hasSet && unsetInput.length === 0) {
|
|
1211
|
+
issues.push({
|
|
1212
|
+
path: `changes[${index}].set`,
|
|
1213
|
+
message: "update needs at least one key in set or unset",
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
if (!setResult.success || issues.length > 0) return { ok: false, issues };
|
|
1217
|
+
return { ok: true, set: setResult.data, unset: validatedUnset };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Per-kind switch so `value.id` narrows and `change` needs no cast.
|
|
1221
|
+
function parseUpdateChange(
|
|
1222
|
+
value: z.output<typeof updateChangeSchema>,
|
|
1223
|
+
index: number,
|
|
1224
|
+
rawItem: unknown,
|
|
1225
|
+
): { readonly change?: PatternChange; readonly issues: readonly PatternChangeIssue[] } {
|
|
1226
|
+
switch (value.id.kind) {
|
|
1227
|
+
case "writeHandler": {
|
|
1228
|
+
const result = parseHeaderSetAndUnset(
|
|
1229
|
+
WRITE_HANDLER_UPDATE_SET_SCHEMA,
|
|
1230
|
+
WRITE_HANDLER_UPDATE_UNSET_ENUM,
|
|
1231
|
+
"writeHandler",
|
|
1232
|
+
value,
|
|
1233
|
+
index,
|
|
1234
|
+
rawItem,
|
|
1235
|
+
);
|
|
1236
|
+
if (!result.ok) return { issues: result.issues };
|
|
1237
|
+
const change: PatternChange = {
|
|
1238
|
+
op: "update",
|
|
1239
|
+
id: value.id,
|
|
1240
|
+
set: result.set,
|
|
1241
|
+
...(result.unset.length > 0 ? { unset: result.unset } : {}),
|
|
1242
|
+
};
|
|
1243
|
+
return { change, issues: [] };
|
|
1244
|
+
}
|
|
1245
|
+
case "queryHandler": {
|
|
1246
|
+
const result = parseHeaderSetAndUnset(
|
|
1247
|
+
QUERY_HANDLER_UPDATE_SET_SCHEMA,
|
|
1248
|
+
QUERY_HANDLER_UPDATE_UNSET_ENUM,
|
|
1249
|
+
"queryHandler",
|
|
1250
|
+
value,
|
|
1251
|
+
index,
|
|
1252
|
+
rawItem,
|
|
1253
|
+
);
|
|
1254
|
+
if (!result.ok) return { issues: result.issues };
|
|
1255
|
+
const change: PatternChange = {
|
|
1256
|
+
op: "update",
|
|
1257
|
+
id: value.id,
|
|
1258
|
+
set: result.set,
|
|
1259
|
+
...(result.unset.length > 0 ? { unset: result.unset } : {}),
|
|
1260
|
+
};
|
|
1261
|
+
return { change, issues: [] };
|
|
1262
|
+
}
|
|
1263
|
+
case "streamHandler": {
|
|
1264
|
+
const result = parseHeaderSetAndUnset(
|
|
1265
|
+
STREAM_HANDLER_UPDATE_SET_SCHEMA,
|
|
1266
|
+
STREAM_HANDLER_UPDATE_UNSET_ENUM,
|
|
1267
|
+
"streamHandler",
|
|
1268
|
+
value,
|
|
1269
|
+
index,
|
|
1270
|
+
rawItem,
|
|
1271
|
+
);
|
|
1272
|
+
if (!result.ok) return { issues: result.issues };
|
|
1273
|
+
const change: PatternChange = {
|
|
1274
|
+
op: "update",
|
|
1275
|
+
id: value.id,
|
|
1276
|
+
set: result.set,
|
|
1277
|
+
...(result.unset.length > 0 ? { unset: result.unset } : {}),
|
|
1278
|
+
};
|
|
1279
|
+
return { change, issues: [] };
|
|
1280
|
+
}
|
|
1281
|
+
default:
|
|
1282
|
+
return {
|
|
1283
|
+
issues: [
|
|
1284
|
+
{
|
|
1285
|
+
path: `changes[${index}].id.kind`,
|
|
1286
|
+
message: `update is not supported for kind "${value.id.kind}"; use replace`,
|
|
1287
|
+
},
|
|
1288
|
+
],
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// =============================================================================
|
|
1294
|
+
// Issue formatting
|
|
1295
|
+
// =============================================================================
|
|
1296
|
+
|
|
1297
|
+
function formatPath(index: number, path: readonly PropertyKey[]): string {
|
|
1298
|
+
let out = `changes[${index}]`;
|
|
1299
|
+
for (const segment of path) {
|
|
1300
|
+
out += typeof segment === "number" ? `[${segment}]` : `.${String(segment)}`;
|
|
1301
|
+
}
|
|
1302
|
+
return out;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function getAtPath(root: unknown, path: readonly PropertyKey[]): unknown {
|
|
1306
|
+
let current: unknown = root;
|
|
1307
|
+
for (const segment of path) {
|
|
1308
|
+
if (isPlainObject(current)) {
|
|
1309
|
+
current = current[String(segment)];
|
|
1310
|
+
} else if (Array.isArray(current) && typeof segment === "number") {
|
|
1311
|
+
current = current[segment];
|
|
1312
|
+
} else {
|
|
1313
|
+
return undefined;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return current;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// zod v4 reports `.strict()` violations as a single `unrecognized_keys`
|
|
1320
|
+
// issue per offending object, carrying all extra key names at once. We
|
|
1321
|
+
// split it into one PatternChangeIssue per key so each unexpected key gets
|
|
1322
|
+
// its own actionable path — except `definition` on a handler-kind pattern
|
|
1323
|
+
// nesting an `access` sub-key, which gets the more specific message the
|
|
1324
|
+
// spec calls for (a caller who nests `access` under a stray `definition`
|
|
1325
|
+
// key is making one specific, common mistake, not an arbitrary unknown key).
|
|
1326
|
+
// Only these kinds render a top-level `access`; on any other kind the hint
|
|
1327
|
+
// would send the caller into a second rejection.
|
|
1328
|
+
function isKindWithTopLevelAccess(kind: unknown): boolean {
|
|
1329
|
+
return kind === "writeHandler" || kind === "queryHandler" || kind === "streamHandler";
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function formatZodIssues(
|
|
1333
|
+
issues: readonly z.core.$ZodIssue[],
|
|
1334
|
+
index: number,
|
|
1335
|
+
rawItem: unknown,
|
|
1336
|
+
): PatternChangeIssue[] {
|
|
1337
|
+
const out: PatternChangeIssue[] = [];
|
|
1338
|
+
for (const issue of issues) {
|
|
1339
|
+
if (issue.code === "unrecognized_keys") {
|
|
1340
|
+
const parentValue = getAtPath(rawItem, issue.path);
|
|
1341
|
+
for (const key of issue.keys) {
|
|
1342
|
+
const keyValue = isPlainObject(parentValue) ? parentValue[key] : undefined;
|
|
1343
|
+
if (
|
|
1344
|
+
key === "definition" &&
|
|
1345
|
+
isPlainObject(parentValue) &&
|
|
1346
|
+
isKindWithTopLevelAccess(parentValue["kind"]) &&
|
|
1347
|
+
isPlainObject(keyValue) &&
|
|
1348
|
+
"access" in keyValue
|
|
1349
|
+
) {
|
|
1350
|
+
out.push({
|
|
1351
|
+
path: `${formatPath(index, issue.path)}.definition.access`,
|
|
1352
|
+
message: "unexpected key; access belongs at top level of the pattern",
|
|
1353
|
+
});
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
out.push({
|
|
1357
|
+
path: `${formatPath(index, issue.path)}.${key}`,
|
|
1358
|
+
message: "unexpected key",
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
out.push({ path: formatPath(index, issue.path), message: issue.message });
|
|
1364
|
+
}
|
|
1365
|
+
return out;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// =============================================================================
|
|
1369
|
+
// Public API
|
|
1370
|
+
// =============================================================================
|
|
1371
|
+
|
|
1372
|
+
export function parsePatternChanges(input: unknown): PatternChangesParseResult {
|
|
1373
|
+
if (!Array.isArray(input)) {
|
|
1374
|
+
return { ok: false, issues: [{ path: "changes", message: "must be an array" }] };
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const issues: PatternChangeIssue[] = [];
|
|
1378
|
+
const changes: PatternChange[] = [];
|
|
1379
|
+
|
|
1380
|
+
input.forEach((item, index) => {
|
|
1381
|
+
const result = changeSchema.safeParse(item);
|
|
1382
|
+
if (!result.success) {
|
|
1383
|
+
issues.push(...formatZodIssues(result.error.issues, index, item));
|
|
1384
|
+
// skip: schema violations already reported as issues
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const value = result.data;
|
|
1388
|
+
if (value.op === "replace" && value.id.kind !== value.pattern.kind) {
|
|
1389
|
+
issues.push({
|
|
1390
|
+
path: `changes[${index}].pattern.kind`,
|
|
1391
|
+
message: `pattern.kind "${value.pattern.kind}" does not match id.kind "${value.id.kind}"`,
|
|
1392
|
+
});
|
|
1393
|
+
// skip: id/pattern kind mismatch already reported as an issue
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
if (value.op === "add") {
|
|
1397
|
+
changes.push({ op: "add", pattern: value.pattern });
|
|
1398
|
+
} else if (value.op === "replace") {
|
|
1399
|
+
changes.push({ op: "replace", id: value.id, pattern: value.pattern });
|
|
1400
|
+
} else if (value.op === "remove") {
|
|
1401
|
+
changes.push({ op: "remove", id: value.id });
|
|
1402
|
+
} else {
|
|
1403
|
+
const outcome = parseUpdateChange(value, index, item);
|
|
1404
|
+
issues.push(...outcome.issues);
|
|
1405
|
+
if (outcome.change) changes.push(outcome.change);
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
if (issues.length > 0) return { ok: false, issues };
|
|
1410
|
+
return { ok: true, changes };
|
|
1411
|
+
}
|