@telorun/analyzer 0.48.0 → 0.49.1
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/dist/analysis-registry.d.ts +22 -11
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +36 -39
- package/dist/analyzer.d.ts +38 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +115 -83
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +72 -1
- package/dist/extends-resolution.d.ts +41 -0
- package/dist/extends-resolution.d.ts.map +1 -1
- package/dist/extends-resolution.js +68 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/invocation-contract.d.ts +100 -0
- package/dist/invocation-contract.d.ts.map +1 -0
- package/dist/invocation-contract.js +208 -0
- package/dist/manifest-loader.d.ts +11 -0
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +20 -0
- package/dist/schema-compat.d.ts +12 -4
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +185 -9
- package/dist/validate-base-mapping.js +11 -1
- package/dist/validate-cel-context.d.ts +0 -6
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +51 -4
- package/dist/validate-invocation-contract.d.ts +30 -0
- package/dist/validate-invocation-contract.d.ts.map +1 -0
- package/dist/validate-invocation-contract.js +394 -0
- package/dist/validate-step-inputs.d.ts +24 -0
- package/dist/validate-step-inputs.d.ts.map +1 -0
- package/dist/validate-step-inputs.js +87 -0
- package/dist/validate-throws-coverage.d.ts +1 -1
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +9 -1
- package/package.json +3 -3
- package/src/analysis-registry.ts +44 -34
- package/src/analyzer.ts +171 -100
- package/src/builtins.ts +74 -1
- package/src/extends-resolution.ts +86 -0
- package/src/index.ts +13 -1
- package/src/invocation-contract.ts +275 -0
- package/src/manifest-loader.ts +20 -0
- package/src/schema-compat.ts +191 -8
- package/src/validate-base-mapping.ts +14 -1
- package/src/validate-cel-context.ts +49 -4
- package/src/validate-invocation-contract.ts +450 -0
- package/src/validate-step-inputs.ts +117 -0
- package/src/validate-throws-coverage.ts +12 -2
package/src/schema-compat.ts
CHANGED
|
@@ -306,7 +306,71 @@ export function celTypeSatisfiesJsonSchema(celType: string, schema: Record<strin
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
/** Return a literal placeholder value of the correct schema type for AJV. */
|
|
309
|
-
|
|
309
|
+
/** A number inside the schema's declared bounds. The placeholder stands in for a
|
|
310
|
+
* value only known at runtime, so its single job is to be ACCEPTABLE — a bare 0
|
|
311
|
+
* into an `exclusiveMinimum: 0` field (a scale, a positive dimension) would
|
|
312
|
+
* report a violation against a value the author never wrote. Bounds are read in
|
|
313
|
+
* the order that pins the value: an inclusive minimum is usable as-is, an
|
|
314
|
+
* exclusive one needs a step past it, and a wholly-negative range needs the
|
|
315
|
+
* maximum end instead. */
|
|
316
|
+
function numericPlaceholder(schema: Record<string, any>): number {
|
|
317
|
+
const isInteger = schema.type === "integer";
|
|
318
|
+
// One step past an exclusive bound. Integral for both `integer` and `number`:
|
|
319
|
+
// any value inside the band will do, and a whole number is inside it whenever
|
|
320
|
+
// a fractional one is (the narrow-band case below handles when it is not).
|
|
321
|
+
const step = 1;
|
|
322
|
+
if (typeof schema.minimum === "number") return schema.minimum;
|
|
323
|
+
if (typeof schema.exclusiveMinimum === "number") {
|
|
324
|
+
const candidate = schema.exclusiveMinimum + step;
|
|
325
|
+
if (typeof schema.maximum === "number" && candidate > schema.maximum) {
|
|
326
|
+
// A narrow band (0 < x <= 0.5) has no integral step; take the midpoint,
|
|
327
|
+
// which the band's own definition guarantees is inside it.
|
|
328
|
+
return isInteger ? schema.maximum : (schema.exclusiveMinimum + schema.maximum) / 2;
|
|
329
|
+
}
|
|
330
|
+
return candidate;
|
|
331
|
+
}
|
|
332
|
+
if (typeof schema.maximum === "number" && schema.maximum < 0) return schema.maximum;
|
|
333
|
+
if (typeof schema.exclusiveMaximum === "number" && schema.exclusiveMaximum <= 0) {
|
|
334
|
+
return schema.exclusiveMaximum - step;
|
|
335
|
+
}
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The constraints a placeholder must satisfy, folded across `allOf` branches.
|
|
340
|
+
* Inheritance between types is expressed by intersecting `allOf`, so a bound a
|
|
341
|
+
* parent declared lives in a branch rather than on the property itself — a
|
|
342
|
+
* placeholder that reads only the top level would violate it and report against
|
|
343
|
+
* a value the author never wrote. The tightest bound wins, which is what the
|
|
344
|
+
* intersection means. */
|
|
345
|
+
function foldedConstraints(schema: Record<string, any>): Record<string, any> {
|
|
346
|
+
const branches = Array.isArray(schema.allOf) ? (schema.allOf as Record<string, any>[]) : [];
|
|
347
|
+
if (branches.length === 0) return schema;
|
|
348
|
+
const out: Record<string, any> = { ...schema };
|
|
349
|
+
for (const branch of branches) {
|
|
350
|
+
const folded = foldedConstraints(branch);
|
|
351
|
+
for (const key of ["minimum", "exclusiveMinimum", "minLength", "minItems"] as const) {
|
|
352
|
+
if (typeof folded[key] === "number" && (typeof out[key] !== "number" || folded[key] > out[key])) {
|
|
353
|
+
out[key] = folded[key];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (const key of ["maximum", "exclusiveMaximum"] as const) {
|
|
357
|
+
if (typeof folded[key] === "number" && (typeof out[key] !== "number" || folded[key] < out[key])) {
|
|
358
|
+
out[key] = folded[key];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (out.type === undefined && folded.type !== undefined) out.type = folded.type;
|
|
362
|
+
if (out.enum === undefined && folded.enum !== undefined) out.enum = folded.enum;
|
|
363
|
+
if (out.default === undefined && folded.default !== undefined) out.default = folded.default;
|
|
364
|
+
if (folded.required) {
|
|
365
|
+
out.required = [...new Set([...(out.required ?? []), ...folded.required])];
|
|
366
|
+
}
|
|
367
|
+
if (folded.properties) out.properties = { ...folded.properties, ...(out.properties ?? {}) };
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function celPlaceholderForSchema(rawSchema: Record<string, any>): unknown {
|
|
373
|
+
const schema = foldedConstraints(rawSchema);
|
|
310
374
|
if (schema.default !== undefined) return schema.default;
|
|
311
375
|
// An enum-constrained field needs a placeholder drawn from the enum: the
|
|
312
376
|
// type-based fallbacks below ("" for a string, 0 for a number) satisfy `type`
|
|
@@ -318,20 +382,49 @@ export function celPlaceholderForSchema(schema: Record<string, any>): unknown {
|
|
|
318
382
|
switch (schema.type) {
|
|
319
383
|
case "integer":
|
|
320
384
|
case "number":
|
|
321
|
-
return schema
|
|
385
|
+
return numericPlaceholder(schema);
|
|
322
386
|
case "string":
|
|
323
|
-
|
|
387
|
+
// `minLength` is the string analogue of `minimum`: a bare "" into a
|
|
388
|
+
// `minLength: 1` field would report a violation against a value the author
|
|
389
|
+
// never wrote. Any string of the right length will do.
|
|
390
|
+
return typeof schema.minLength === "number" && schema.minLength > 0
|
|
391
|
+
? "x".repeat(schema.minLength)
|
|
392
|
+
: "";
|
|
324
393
|
case "boolean":
|
|
325
394
|
return false;
|
|
326
395
|
case "array":
|
|
327
|
-
|
|
396
|
+
// `minItems` is the array analogue of `minimum` / `minLength`: an empty
|
|
397
|
+
// array into a `minItems: 1` field would report a violation against a
|
|
398
|
+
// value the author never wrote.
|
|
399
|
+
return typeof schema.minItems === "number" && schema.minItems > 0
|
|
400
|
+
? Array.from({ length: schema.minItems }, () =>
|
|
401
|
+
celPlaceholderForSchema((schema.items ?? {}) as Record<string, any>),
|
|
402
|
+
)
|
|
403
|
+
: [];
|
|
328
404
|
case "object":
|
|
329
|
-
return
|
|
405
|
+
return objectPlaceholder(schema);
|
|
330
406
|
default:
|
|
331
407
|
return null;
|
|
332
408
|
}
|
|
333
409
|
}
|
|
334
410
|
|
|
411
|
+
/** An object satisfying the schema's `required` list. A bare `{}` would report
|
|
412
|
+
* every required property as missing against a value the author never wrote —
|
|
413
|
+
* the case where a whole map is produced by one expression (`inputs: !cel
|
|
414
|
+
* "buildRequest(...)"`), which is exactly when the analyzer knows least and
|
|
415
|
+
* should say least. Members are filled recursively by the same rule, so a
|
|
416
|
+
* required nested object is satisfied too. */
|
|
417
|
+
function objectPlaceholder(schema: Record<string, any>): Record<string, unknown> {
|
|
418
|
+
const required = Array.isArray(schema.required) ? (schema.required as string[]) : [];
|
|
419
|
+
if (required.length === 0) return {};
|
|
420
|
+
const properties = (schema.properties ?? {}) as Record<string, Record<string, any>>;
|
|
421
|
+
const out: Record<string, unknown> = {};
|
|
422
|
+
for (const key of required) {
|
|
423
|
+
out[key] = celPlaceholderForSchema(properties[key] ?? {});
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
|
|
335
428
|
const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
|
|
336
429
|
|
|
337
430
|
/** Resolve a `$ref` (only `#/$defs/...` form) against the root schema. */
|
|
@@ -345,8 +438,76 @@ export function resolveRef(schema: Record<string, any>, root: Record<string, any
|
|
|
345
438
|
}
|
|
346
439
|
|
|
347
440
|
/** Collect property schemas from top-level `properties` and all `oneOf`/`anyOf` sub-schemas. */
|
|
441
|
+
/**
|
|
442
|
+
* The `oneOf` / `anyOf` branch a value is written against, when exactly one fits.
|
|
443
|
+
*
|
|
444
|
+
* A union carries no `type` / `properties` / `items` of its own, so a walker that
|
|
445
|
+
* ignores it descends with an empty schema and hands every CEL leaf underneath a
|
|
446
|
+
* `null` placeholder — which then fails every branch and reports a pile of
|
|
447
|
+
* violations against a value that is perfectly valid. Picking the branch first
|
|
448
|
+
* is what lets the leaves be typed.
|
|
449
|
+
*
|
|
450
|
+
* Selection is structural and conservative: a branch must agree with the data's
|
|
451
|
+
* kind, and for an object every `required` key must be present (which is what
|
|
452
|
+
* separates a `{type, text}` part from a `{type, data, mediaType}` one). If that
|
|
453
|
+
* leaves anything other than exactly one branch, the union is returned unchanged
|
|
454
|
+
* — an ambiguous union is one the analyzer should not resolve on the author's
|
|
455
|
+
* behalf.
|
|
456
|
+
*/
|
|
457
|
+
function selectUnionBranch(
|
|
458
|
+
schema: Record<string, any>,
|
|
459
|
+
data: unknown,
|
|
460
|
+
root: Record<string, any>,
|
|
461
|
+
): Record<string, any> {
|
|
462
|
+
const branches = (schema.oneOf ?? schema.anyOf) as Record<string, any>[] | undefined;
|
|
463
|
+
if (!Array.isArray(branches) || branches.length === 0) return schema;
|
|
464
|
+
if (schema.type !== undefined || schema.properties !== undefined) return schema;
|
|
465
|
+
|
|
466
|
+
const kind = Array.isArray(data)
|
|
467
|
+
? "array"
|
|
468
|
+
: data === null
|
|
469
|
+
? "null"
|
|
470
|
+
: typeof data === "object"
|
|
471
|
+
? "object"
|
|
472
|
+
: typeof data === "string"
|
|
473
|
+
? "string"
|
|
474
|
+
: typeof data === "number"
|
|
475
|
+
? "number"
|
|
476
|
+
: typeof data === "boolean"
|
|
477
|
+
? "boolean"
|
|
478
|
+
: undefined;
|
|
479
|
+
if (!kind) return schema;
|
|
480
|
+
|
|
481
|
+
const fits = branches
|
|
482
|
+
.map((b) => resolveRef(b, root))
|
|
483
|
+
.filter((b) => {
|
|
484
|
+
const types = Array.isArray(b.type) ? b.type : b.type ? [b.type] : [];
|
|
485
|
+
if (types.length > 0 && !types.includes(kind)) return false;
|
|
486
|
+
if (kind === "object" && Array.isArray(b.required)) {
|
|
487
|
+
const keys = Object.keys(data as Record<string, unknown>);
|
|
488
|
+
if (!(b.required as string[]).every((r) => keys.includes(r))) return false;
|
|
489
|
+
}
|
|
490
|
+
return true;
|
|
491
|
+
});
|
|
492
|
+
return fits.length === 1 ? fits[0]! : schema;
|
|
493
|
+
}
|
|
494
|
+
|
|
348
495
|
export function collectProperties(schema: Record<string, any>): Record<string, any> {
|
|
349
496
|
const props: Record<string, any> = { ...(schema.properties ?? {}) };
|
|
497
|
+
// `allOf` INTERSECTS, so a branch constraining a property constrains the
|
|
498
|
+
// property itself — type inheritance expresses an inherited bound exactly this
|
|
499
|
+
// way (`allOf: [{ properties: { score: { minimum: 10 } } }]`). Merging the
|
|
500
|
+
// branch's constraints into the property is what lets a placeholder for that
|
|
501
|
+
// property be built from the bound the value must actually satisfy; reading
|
|
502
|
+
// only the top level would produce one that violates it.
|
|
503
|
+
for (const sub of (schema.allOf ?? []) as Record<string, any>[]) {
|
|
504
|
+
if (!sub || typeof sub !== "object" || !sub.properties) continue;
|
|
505
|
+
for (const [k, v] of Object.entries(sub.properties as Record<string, any>)) {
|
|
506
|
+
props[k] = k in props ? { ...(props[k] as object), ...(v as object) } : v;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// `oneOf` / `anyOf` are alternatives, not constraints: a property seen in one
|
|
510
|
+
// branch is contributed only when no branch already declared it.
|
|
350
511
|
for (const sub of schema.oneOf ?? schema.anyOf ?? []) {
|
|
351
512
|
if (sub && typeof sub === "object" && sub.properties) {
|
|
352
513
|
for (const [k, v] of Object.entries(sub.properties as Record<string, any>)) {
|
|
@@ -363,11 +524,24 @@ export function substituteCelFields(
|
|
|
363
524
|
data: unknown,
|
|
364
525
|
schema: Record<string, any>,
|
|
365
526
|
rootSchema?: Record<string, any>,
|
|
527
|
+
/** Called with the dotted path of every value replaced by a placeholder.
|
|
528
|
+
*
|
|
529
|
+
* A placeholder is a stand-in for something only known at runtime, so its
|
|
530
|
+
* VALUE says nothing: a caller that judges constraints at these paths reports
|
|
531
|
+
* against a value no author wrote. Some constraints cannot be satisfied by
|
|
532
|
+
* construction at all (`pattern`, `format`, a `oneOf` of unrelated shapes),
|
|
533
|
+
* so making every placeholder acceptable is not achievable in general —
|
|
534
|
+
* knowing where not to look is. Structural findings survive because they are
|
|
535
|
+
* located at the CONTAINER, not at the substituted leaf. */
|
|
536
|
+
onSubstitute?: (path: string) => void,
|
|
537
|
+
path = "",
|
|
366
538
|
): unknown {
|
|
367
539
|
const root = rootSchema ?? schema;
|
|
368
|
-
const resolved = resolveRef(schema, root);
|
|
540
|
+
const resolved = selectUnionBranch(resolveRef(schema, root), data, root);
|
|
541
|
+
const mark = () => onSubstitute?.(path);
|
|
369
542
|
|
|
370
543
|
if (typeof data === "string" && CEL_PURE_RE.test(data)) {
|
|
544
|
+
mark();
|
|
371
545
|
return celPlaceholderForSchema(resolved);
|
|
372
546
|
}
|
|
373
547
|
// `!ref <name>` sentinels are identity markers, not runtime values —
|
|
@@ -381,11 +555,14 @@ export function substituteCelFields(
|
|
|
381
555
|
return data;
|
|
382
556
|
}
|
|
383
557
|
if (isTaggedSentinel(data)) {
|
|
558
|
+
mark();
|
|
384
559
|
return celPlaceholderForSchema(resolved);
|
|
385
560
|
}
|
|
386
561
|
if (Array.isArray(data)) {
|
|
387
562
|
const itemSchema = resolveRef((resolved.items ?? {}) as Record<string, any>, root);
|
|
388
|
-
return data.map((item) =>
|
|
563
|
+
return data.map((item, i) =>
|
|
564
|
+
substituteCelFields(item, itemSchema, root, onSubstitute, `${path}[${i}]`),
|
|
565
|
+
);
|
|
389
566
|
}
|
|
390
567
|
if (data !== null && typeof data === "object") {
|
|
391
568
|
const props = collectProperties(resolved);
|
|
@@ -395,7 +572,13 @@ export function substituteCelFields(
|
|
|
395
572
|
: undefined;
|
|
396
573
|
const result: Record<string, unknown> = {};
|
|
397
574
|
for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
|
|
398
|
-
result[k] = substituteCelFields(
|
|
575
|
+
result[k] = substituteCelFields(
|
|
576
|
+
v,
|
|
577
|
+
(props[k] ?? addlProps ?? {}) as Record<string, any>,
|
|
578
|
+
root,
|
|
579
|
+
onSubstitute,
|
|
580
|
+
path ? `${path}.${k}` : k,
|
|
581
|
+
);
|
|
399
582
|
}
|
|
400
583
|
return result;
|
|
401
584
|
}
|
|
@@ -92,6 +92,16 @@ interface CheckCtx {
|
|
|
92
92
|
filePath: string | undefined;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/** Keys the inherited controller supplies itself when it builds the parent
|
|
96
|
+
* manifest (`{ kind, metadata, ...base }`), so `base:` neither has to set them
|
|
97
|
+
* nor may. Without this exclusion a parent whose schema lists `metadata` in
|
|
98
|
+
* `required` — `JS.Script` does, the most natural parent for a remapping child —
|
|
99
|
+
* is unusable: omitting it is BASE_MISSING_REQUIRED and setting it is
|
|
100
|
+
* BASE_UNKNOWN_FIELD, since `metadata` is required but never a declared
|
|
101
|
+
* property. Only at the top level; a nested `metadata` field is the parent's
|
|
102
|
+
* own and is checked normally. */
|
|
103
|
+
const CONTROLLER_SUPPLIED_KEYS = new Set(["kind", "metadata"]);
|
|
104
|
+
|
|
95
105
|
function checkObject(
|
|
96
106
|
value: Record<string, unknown>,
|
|
97
107
|
schema: Record<string, any>,
|
|
@@ -99,7 +109,10 @@ function checkObject(
|
|
|
99
109
|
ctx: CheckCtx,
|
|
100
110
|
): void {
|
|
101
111
|
const properties = (schema.properties ?? {}) as Record<string, Record<string, any>>;
|
|
102
|
-
const
|
|
112
|
+
const isRoot = path === "base";
|
|
113
|
+
const required = (Array.isArray(schema.required) ? (schema.required as string[]) : []).filter(
|
|
114
|
+
(name) => !(isRoot && CONTROLLER_SUPPLIED_KEYS.has(name)),
|
|
115
|
+
);
|
|
103
116
|
const additionalFalse = schema.additionalProperties === false;
|
|
104
117
|
|
|
105
118
|
for (const req of required) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { extractAccessChains, validateChainAgainstSchema } from "@telorun/templating";
|
|
2
2
|
import { mergeTypeSchemas } from "@telorun/sdk";
|
|
3
|
+
import { KERNEL_BUILTINS } from "./builtins.js";
|
|
3
4
|
|
|
4
5
|
export interface ContextResolveOpts {
|
|
5
6
|
/** When provided, used to resolve `x-telo-context-from-root` annotations against the
|
|
@@ -23,6 +24,42 @@ export interface ContextResolveOpts {
|
|
|
23
24
|
* - Object with `kind` + `schema`: inline type definition → return the `schema`
|
|
24
25
|
* - Object with `type` or `properties`: raw JSON Schema, return as-is
|
|
25
26
|
*/
|
|
27
|
+
/**
|
|
28
|
+
* Kind names that DECLARE `capability: Telo.Type` — the kernel built-ins plus
|
|
29
|
+
* every definition in scope.
|
|
30
|
+
*
|
|
31
|
+
* Derived from the declared capability, never from the kind's spelling: which
|
|
32
|
+
* kinds are data shapes is a topology fact the analyzer must read off
|
|
33
|
+
* `Telo.Definition` docs, not guess from a name. A name test would silently miss
|
|
34
|
+
* any third-party type kind and would have to be edited every time one is added.
|
|
35
|
+
*
|
|
36
|
+
* Names, not fully-qualified kinds, because a resource writes its kind through
|
|
37
|
+
* whatever alias its file declares (`Type.JsonSchema`, `Telo.JsonSchema`,
|
|
38
|
+
* `Shapes.JsonSchema`) while the definition knows only its own module and name.
|
|
39
|
+
* Memoized per manifest list — this runs on every type-field resolution.
|
|
40
|
+
*/
|
|
41
|
+
const typeKindNames = new WeakMap<object, Set<string>>();
|
|
42
|
+
|
|
43
|
+
function typeCapableNames(allManifests: Record<string, any>[]): Set<string> {
|
|
44
|
+
const cached = typeKindNames.get(allManifests);
|
|
45
|
+
if (cached) return cached;
|
|
46
|
+
const names = new Set<string>();
|
|
47
|
+
for (const def of [...KERNEL_BUILTINS, ...allManifests] as Record<string, any>[]) {
|
|
48
|
+
if (def?.kind !== "Telo.Definition" && def?.kind !== "Telo.Abstract") continue;
|
|
49
|
+
if (def.capability !== "Telo.Type") continue;
|
|
50
|
+
const name = def.metadata?.name;
|
|
51
|
+
if (typeof name === "string") names.add(name);
|
|
52
|
+
}
|
|
53
|
+
typeKindNames.set(allManifests, names);
|
|
54
|
+
return names;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isTypeKind(kind: unknown, allManifests: Record<string, any>[]): boolean {
|
|
58
|
+
if (typeof kind !== "string") return false;
|
|
59
|
+
const suffix = kind.slice(kind.lastIndexOf(".") + 1);
|
|
60
|
+
return typeCapableNames(allManifests).has(suffix);
|
|
61
|
+
}
|
|
62
|
+
|
|
26
63
|
export function resolveTypeFieldToSchema(
|
|
27
64
|
value: unknown,
|
|
28
65
|
allManifests: Record<string, any>[],
|
|
@@ -37,8 +74,7 @@ export function resolveTypeFieldToSchema(
|
|
|
37
74
|
const typeManifest = allManifests.find(
|
|
38
75
|
(m) =>
|
|
39
76
|
(m.metadata as any)?.name === value &&
|
|
40
|
-
|
|
41
|
-
/\bType\b/.test(m.kind) &&
|
|
77
|
+
isTypeKind(m.kind, allManifests) &&
|
|
42
78
|
typeof m.schema === "object" &&
|
|
43
79
|
m.schema !== null,
|
|
44
80
|
);
|
|
@@ -248,11 +284,20 @@ export function resolveContextAnnotations(
|
|
|
248
284
|
|
|
249
285
|
const from = schema["x-telo-context-from"] as string | undefined;
|
|
250
286
|
if (from) {
|
|
251
|
-
const
|
|
252
|
-
//
|
|
287
|
+
const navigated = navigatePath(manifestItem, from.split("/")) as Record<string, any> | undefined;
|
|
288
|
+
// The navigated value is either a plain map of property names → sub-schemas
|
|
289
|
+
// (a transport scope's `request/schema` → `{ query, body, params }`), or a
|
|
290
|
+
// `telo#Type` field naming one contract (`inputType:`). The second form has
|
|
291
|
+
// to be resolved first: the standard library writes it as the inline
|
|
292
|
+
// `{ kind: Type.JsonSchema, schema: … }` wrapper, so merging it verbatim
|
|
293
|
+
// would type the variable as `{ kind, schema }` instead of its properties.
|
|
294
|
+
const asType = resolveTypeFieldToSchema(navigated, allManifests ?? []);
|
|
295
|
+
const resolved = asType?.properties ?? navigated;
|
|
296
|
+
const required = Array.isArray(asType?.required) ? asType.required : undefined;
|
|
253
297
|
return {
|
|
254
298
|
...schema,
|
|
255
299
|
properties: { ...(schema.properties ?? {}), ...(resolved ?? {}) },
|
|
300
|
+
...(required ? { required } : {}),
|
|
256
301
|
additionalProperties: false,
|
|
257
302
|
};
|
|
258
303
|
}
|