|
60 | 60 | import type { AudienceCaller, Book, ResolvedBook, ResolverDoc } from '@objectstack/spec/system'; |
61 | 61 | import { preferredLocaleFromHeader } from '@objectstack/spec/system'; |
62 | 62 | import { apiExposureDenialReason } from '@objectstack/spec/data'; |
63 | | -import { resolveObjectSortability, type GetMetaItemLayeredRequest } from '@objectstack/spec/api'; |
| 63 | +import { resolveObjectSortability } from '@objectstack/spec/api'; |
64 | 64 | import { canonicalMetaUrlType, pluralToSingular, unrecognisedMetaTypeRefusal } from '@objectstack/spec/shared'; |
65 | 65 | import { |
66 | 66 | ObjectSchemaMaskEvaluationError, |
@@ -2496,36 +2496,11 @@ const META_ITEM_LAYERS = ['effective', 'code', 'overlay'] as const; |
2496 | 2496 | /** The layers in the order the ADR-0106 mask projects them. */ |
2497 | 2497 | const META_ITEM_MASKED_LAYERS = ['code', 'overlay', 'effective'] as const; |
2498 | 2498 |
|
2499 | | -/** Everything {@link createMetaLayeredAnswer} reads, supplied by the transport. */ |
2500 | | -export interface MetaLayeredAnswerSources extends MetaItemReadGateSources { |
2501 | | - /** |
2502 | | - * The protocol's layered read (`getMetaItemLayered`), handed the request |
2503 | | - * this chain builds — the item, its `?package=` scope (ADR-0048) and the |
2504 | | - * caller's organization — plus whatever the transport scopes every read by |
2505 | | - * itself (`RestServer`: its environment). A THROW propagates: the transport |
2506 | | - * answers that fault, ⛔ never a layered view with a layer missing. |
2507 | | - * |
2508 | | - * ⚠️ {@link MetaReadGateAudienceSources.resolveCaller} MUST carry |
2509 | | - * {@link MetaReadGateCaller.mayWriteItem} here — the transport's own save |
2510 | | - * door's admission of the item — because the layered view is a door that |
2511 | | - * honours the author exemption ({@link STORED_VERSION_DOOR_POLICY}). Absent |
2512 | | - * reads as `false`: every caller is served the pruned layers. |
2513 | | - */ |
2514 | | - readLayered(request: GetMetaItemLayeredRequest): Promise<unknown>; |
2515 | | -} |
2516 | | - |
2517 | 2499 | /** The request facts the layered chain reads — no transport shape. */ |
2518 | 2500 | export interface MetaLayeredRequest { |
2519 | | - /** |
2520 | | - * The `:type` segment AS RECEIVED. The protocol folds it at its own |
2521 | | - * boundary, so it is handed down unfolded (a pre-fold here would hide a |
2522 | | - * drift between the two from the protocol's tests); the gate and the |
2523 | | - * organization are decided on the fold, here. |
2524 | | - */ |
2525 | | - readonly type: string; |
| 2501 | + /** The SINGULAR type (the caller folds `/meta/apps/:name` once, at its boundary). */ |
| 2502 | + readonly metaType: string; |
2526 | 2503 | readonly name: string; |
2527 | | - /** `?package=` — ONE owning package (ADR-0048), or `undefined`. */ |
2528 | | - readonly packageId?: string; |
2529 | 2504 | /** |
2530 | 2505 | * [ADR-0106 D2/D3] The caller's field-visibility posture for this item, |
2531 | 2506 | * resolved by the transport BEFORE the read, the not-applicable passthrough |
@@ -2554,96 +2529,100 @@ export type MetaLayeredAnswer = |
2554 | 2529 | /** |
2555 | 2530 | * [#5882 · #20156 · #20478] THE answer of the layered view — the three-layer |
2556 | 2531 | * diagnostic projection (`code` / `overlay` / `effective`) declared by |
2557 | | - * `GetMetaItemLayeredResponseSchema` — on both of its spellings |
2558 | | - * (`GET /meta/:type/:name/layers`, and the deprecated `?layers=` flag on the item |
2559 | | - * read), on both transports. |
| 2532 | + * `GetMetaItemLayeredResponseSchema` — after the store read, on both of its |
| 2533 | + * spellings (`GET /meta/:type/:name/layers`, and the deprecated `?layers=` flag |
| 2534 | + * on the item read), on both transports. |
2560 | 2535 | * |
2561 | | - * ## Why here |
| 2536 | + * ## Why one chain |
2562 | 2537 | * |
2563 | 2538 | * `RestServer` served both spellings through one private helper, and the |
2564 | 2539 | * runtime dispatcher — the only answer on a host that mounts just the |
2565 | 2540 | * `${prefix}/*` catch-all — served neither: the route answered a located |
2566 | 2541 | * `404 ROUTE_NOT_FOUND`, and the flag answered the PLAIN read's |
2567 | 2542 | * `{ type, name, item }` with a `200`, so a client reading `overlay` or |
2568 | | - * `effective` there read `undefined`, and an author was served the item pruned |
2569 | | - * where ruling 5856774816 serves it whole. The helper moved here, unchanged, and |
2570 | | - * each transport supplies only its I/O. ⛔ A step is added HERE, never in a |
2571 | | - * transport — one added in one of them is the defect this closed, reopened. |
| 2543 | + * `effective` there read `undefined`, and an author was served the app pruned |
| 2544 | + * where ruling 5856774816 serves it whole. Everything the helper did after the |
| 2545 | + * read moved here, unchanged, and each transport hands its read's answer to |
| 2546 | + * THIS function. ⛔ A step is added HERE, never in a transport — one added in |
| 2547 | + * one of them is the defect this closed, reopened. |
2572 | 2548 | * `meta-list-projection-parity.test.ts` in `@objectstack/runtime` drives both |
2573 | 2549 | * spellings through both transports. |
2574 | 2550 | * |
| 2551 | + * The read stays each transport's, in the caller's VETTED partition |
| 2552 | + * ({@link metaReadOrganizationId} over the folded type — the partition the plain |
| 2553 | + * read reads, [#9454] so an author who has just saved an org overlay is not |
| 2554 | + * shown `overlay: null`) and its `?package=` scope (ADR-0048), exactly as the |
| 2555 | + * item read's does ({@link createMetaItemAnswer}). |
| 2556 | + * |
2575 | 2557 | * Not translated and not cached, both deliberately: this is a diagnostic view of |
2576 | 2558 | * what is STORED at each layer, so locale-collapsing it (or serving it from the |
2577 | 2559 | * published-value cache) would misreport the thing being diagnosed. |
2578 | 2560 | * |
2579 | 2561 | * ## The steps, in `RestServer`'s order (unchanged) |
2580 | 2562 | * |
2581 | | - * 1. The read, scoped to the caller's VETTED organization |
2582 | | - * ({@link metaReadOrganizationId} over the folded type — the partition the |
2583 | | - * plain read reads, [#9454] so an author who has just saved an org overlay |
2584 | | - * is not shown `overlay: null`) and to `?package=` (ADR-0048). |
2585 | | - * 2. [#20156] THE per-caller gate on EVERY present layer, `effective` first, |
2586 | | - * under {@link STORED_VERSION_DOOR_POLICY}: per-caller arms only (these are |
2587 | | - * STORED versions, which Studio's designer loads and saves back), and |
2588 | | - * ruling 5856774816 — a caller who may write an app reads every layer |
2589 | | - * whole, and any other caller who may open it reads each layer pruned, |
2590 | | - * exactly as the plain read prunes it. Every layer is judged before any is |
2591 | | - * served, so a refusal sends nothing of the others. |
2592 | | - * 3. [ADR-0106 D5(4)] The mask on every layer — each is a full object schema — |
| 2563 | + * 1. [#20156] THE per-caller gate on EVERY present layer, `effective` first |
| 2564 | + * (it is what the plain read serves, so its refusal is the plain read's |
| 2565 | + * own), under {@link STORED_VERSION_DOOR_POLICY}: per-caller arms only |
| 2566 | + * (these are STORED versions, which Studio's designer loads and saves |
| 2567 | + * back), and ruling 5856774816 — a caller who may write the item reads |
| 2568 | + * every layer whole, and any other caller who may open it reads each layer |
| 2569 | + * pruned, exactly as the plain read prunes it. ⚠️ So the transport's |
| 2570 | + * {@link MetaReadGateAudienceSources.resolveCaller} MUST carry |
| 2571 | + * {@link MetaReadGateCaller.mayWriteItem} — its own save door's admission; |
| 2572 | + * absent reads as `false` (every caller pruned). Every layer is judged |
| 2573 | + * before any is served, so a refusal sends nothing of the others. |
| 2574 | + * 2. [ADR-0106 D5(4)] The mask on every layer — each is a full object schema — |
2593 | 2575 | * through {@link projectMetaObjectSchema} under the posture resolved before |
2594 | 2576 | * the read, and the `private, no-store` an undetermined posture owes. |
2595 | 2577 | * |
2596 | | - * The protocol's answer is never mutated. A layer the gate or the mask leaves |
2597 | | - * as it was is served as it was, and every other key of the answer |
| 2578 | + * The protocol's answer is never mutated. A layer the gate or the mask leaves as |
| 2579 | + * it was is served as it was, and every other key of the answer |
2598 | 2580 | * (`overlayScope`, `_diagnostics`, the ADR-0010 protection envelope) rides |
2599 | 2581 | * through untouched. With nothing behind the name the protocol answers every |
2600 | | - * layer `null`, and so does this chain: no layer is present to judge. |
| 2582 | + * layer `null`, and so does this chain: no layer is present to judge. A gate |
| 2583 | + * input that cannot be read REJECTS: the transport answers that fault, ⛔ never |
| 2584 | + * a layered view with a layer missing. |
2601 | 2585 | */ |
2602 | | -export async function createMetaLayeredAnswer( |
2603 | | - sources: MetaLayeredAnswerSources, |
| 2586 | +export function createMetaLayeredAnswer( |
| 2587 | + sources: MetaItemReadGateSources, |
2604 | 2588 | request: MetaLayeredRequest, |
2605 | | -): Promise<MetaLayeredAnswer> { |
2606 | | - const { type, name, packageId, maskPosture } = request; |
2607 | | - |
2608 | | - // 1. [#9454 · #20408] The read, in the caller's vetted partition. |
2609 | | - const organizationId = metaReadOrganizationId(type, await sources.resolveCaller()); |
2610 | | - const layered: any = await sources.readLayered({ |
2611 | | - type, |
2612 | | - name, |
2613 | | - ...(packageId ? { packageId } : {}), |
2614 | | - ...(organizationId ? { organizationId } : {}), |
2615 | | - }); |
| 2589 | +): (layered: unknown) => Promise<MetaLayeredAnswer> { |
| 2590 | + const { metaType, name, maskPosture } = request; |
| 2591 | + return async (raw) => { |
| 2592 | + const layered = raw as Record<string, unknown> | null | undefined; |
2616 | 2593 |
|
2617 | | - // 2. [#20156] THE per-caller gate, on every present layer. |
2618 | | - const served = new Map<string, unknown>(); |
2619 | | - { |
2620 | | - const present = META_ITEM_LAYERS.filter((layer) => layered?.[layer] != null); |
2621 | | - const judge = createMetaItemReadGate( |
2622 | | - sources, pluralToSingular(type), name, present.map((layer) => layered[layer]), STORED_VERSION_DOOR_POLICY, |
2623 | | - ); |
2624 | | - for (const layer of present) { |
2625 | | - const verdict = await judge(layered[layer]); |
2626 | | - if (verdict.kind === 'refuse') return verdict; |
2627 | | - served.set(layer, verdict.document); |
| 2594 | + // 1. [#20156] THE per-caller gate, on every present layer. |
| 2595 | + const served = new Map<string, unknown>(); |
| 2596 | + { |
| 2597 | + const present = META_ITEM_LAYERS.filter((layer) => layered?.[layer] != null); |
| 2598 | + const judge = createMetaItemReadGate( |
| 2599 | + sources, metaType, name, present.map((layer) => layered![layer]), STORED_VERSION_DOOR_POLICY, |
| 2600 | + ); |
| 2601 | + for (const layer of present) { |
| 2602 | + const verdict = await judge(layered![layer]); |
| 2603 | + if (verdict.kind === 'refuse') return verdict; |
| 2604 | + served.set(layer, verdict.document); |
| 2605 | + } |
2628 | 2606 | } |
2629 | | - } |
2630 | 2607 |
|
2631 | | - // 3. [ADR-0106 D5(4)] The mask, on every layer. |
2632 | | - let cacheControl: typeof META_UNDETERMINED_CACHE_CONTROL | undefined; |
2633 | | - for (const layer of META_ITEM_MASKED_LAYERS) { |
2634 | | - const document = served.has(layer) ? served.get(layer) : layered?.[layer]; |
2635 | | - const masked = projectMetaObjectSchema(maskPosture, document); |
2636 | | - if (!masked.ok) return { kind: 'mask-fault', object: name }; |
2637 | | - cacheControl ??= masked.cacheControl; |
2638 | | - if (masked.document !== document) served.set(layer, masked.document); |
2639 | | - } |
| 2608 | + // 2. [ADR-0106 D5(4)] The mask, on every layer. |
| 2609 | + let cacheControl: typeof META_UNDETERMINED_CACHE_CONTROL | undefined; |
| 2610 | + for (const layer of META_ITEM_MASKED_LAYERS) { |
| 2611 | + const document = served.has(layer) ? served.get(layer) : layered?.[layer]; |
| 2612 | + const masked = projectMetaObjectSchema(maskPosture, document); |
| 2613 | + if (!masked.ok) return { kind: 'mask-fault', object: name }; |
| 2614 | + cacheControl ??= masked.cacheControl; |
| 2615 | + if (masked.document !== document) served.set(layer, masked.document); |
| 2616 | + } |
2640 | 2617 |
|
2641 | | - if (!layered || typeof layered !== 'object') { |
2642 | | - return cacheControl ? { kind: 'serve', layered, cacheControl } : { kind: 'serve', layered }; |
2643 | | - } |
2644 | | - const answer: Record<string, unknown> = { ...layered }; |
2645 | | - for (const [layer, document] of served) answer[layer] = document; |
2646 | | - return cacheControl ? { kind: 'serve', layered: answer, cacheControl } : { kind: 'serve', layered: answer }; |
| 2618 | + let answer: unknown = raw; |
| 2619 | + if (layered && typeof layered === 'object') { |
| 2620 | + const replaced: Record<string, unknown> = { ...layered }; |
| 2621 | + for (const [layer, document] of served) replaced[layer] = document; |
| 2622 | + answer = replaced; |
| 2623 | + } |
| 2624 | + return cacheControl ? { kind: 'serve', layered: answer, cacheControl } : { kind: 'serve', layered: answer }; |
| 2625 | + }; |
2647 | 2626 | } |
2648 | 2627 |
|
2649 | 2628 | // ── THE book tree ───────────────────────────────────────────────────────────── |
|
0 commit comments