From c2087b4c496a4b8e7382650b25ea98d0b14d82d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 03:23:43 +0000 Subject: [PATCH] refactor(metadata-protocol): move the row-less expanded view read and the own-name container expansion out of protocol.ts (#25467 cut 22) Move the bodies of resolveRowlessExpandedView and expandUnderOwnName unedited into protocol-view-expansion-names.ts, behind a ViewExpansionNamesHost that hands over the six members they read as closures. RowlessExpandedView and servedViewExpansion gain export in place. The class keeps both methods, their docblocks and declared signatures; their callers keep calling the class methods. Claude-Session: https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6 Co-authored-by: Claude --- .../src/protocol-view-expansion-names.ts | 125 ++++++++++++++++++ packages/metadata-protocol/src/protocol.ts | 75 +++-------- 2 files changed, 143 insertions(+), 57 deletions(-) create mode 100644 packages/metadata-protocol/src/protocol-view-expansion-names.ts diff --git a/packages/metadata-protocol/src/protocol-view-expansion-names.ts b/packages/metadata-protocol/src/protocol-view-expansion-names.ts new file mode 100644 index 00000000000..108c63a6653 --- /dev/null +++ b/packages/metadata-protocol/src/protocol-view-expansion-names.ts @@ -0,0 +1,125 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 22] Two view-container expansion members of + * `ObjectStackProtocolImplementation`, moved out of `protocol.ts` whole: the + * by-name read's answer for a view name no stored row holds but a stored + * container expands (`resolveRowlessExpandedView`, #21442/#21967: the list + * read's own rows, parse, expansion and selection), and the expansion of a + * container on another package's object under the container's own name + * (`expandUnderOwnName`, #21334). + * + * A MOVE, not a redesign. Each function below is the body of the + * `ObjectStackProtocolImplementation` method of the same name as it stood, + * re-indented one level and otherwise unedited: the same reads in the same + * order, the same `#5532` discrimination, the same selection, and the same + * names given to the expanded items. + * + * ## How the moved bodies reach the class + * + * The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts` + * header carries the full reasoning), as `protocol-batch-data.ts` applies it. + * The class keeps both methods — the published `dist/*.d.ts` lists their + * names, docblocks and declared signatures — and each body is one literal of + * exactly the {@link ViewExpansionNamesHost} members it reads, each handed + * over as a closure, plus a `.call`. Every other reader of either method + * keeps calling the class method. + * + * What the bodies read of `protocol.ts`'s own module-level declarations they + * import from it; what they import from elsewhere, they import from where + * `protocol.ts` imported it. Nothing re-exports this module, so the published + * surface does not move. + */ + +import { PLURAL_TO_SINGULAR } from '@objectstack/spec/shared'; +import { expandViewContainer } from '@objectstack/spec/ui'; +import type { ObjectStackProtocolImplementation, RowlessExpandedView } from './protocol.js'; +import { servedViewExpansion } from './protocol.js'; + +/** + * The `ObjectStackProtocolImplementation` members the functions below read — + * every one of them, and no other; a function's `this` is the `Pick` of the + * members it reads. Each is typed AS the class member (an indexed access on + * the class), so a change to a member's signature on the class is a change + * here too, and naming a member the class does not have is a compile error. + */ +export interface ViewExpansionNamesHost { + readonly expandStoredViewContainers: ObjectStackProtocolImplementation['expandStoredViewContainers']; + readonly isShippedByAnotherPackage: ObjectStackProtocolImplementation['isShippedByAnotherPackage']; + readonly namesWithOwnStoredRow: ObjectStackProtocolImplementation['namesWithOwnStoredRow']; + readonly readActiveOverlayRows: ObjectStackProtocolImplementation['readActiveOverlayRows']; + readonly rethrowUnlessMetadataStoreUnprovisioned: ObjectStackProtocolImplementation['rethrowUnlessMetadataStoreUnprovisioned']; + readonly storedOverlayEntries: ObjectStackProtocolImplementation['storedOverlayEntries']; +} + +/** The body of `ObjectStackProtocolImplementation#resolveRowlessExpandedView`; the method's docblock there is its contract. */ +export async function resolveRowlessExpandedView( + this: Pick, + request: { type: string; name: string; packageId?: string }, +): Promise { + if ((PLURAL_TO_SINGULAR[request.type] ?? request.type) !== 'view') return undefined; + let records: any[] = []; + // [#21967] Naming a package, the package-less rows in scope stand in, + // read as the list scoped to that package reads them (the + // package-agnostic read, filtered back to the package-less rows). + let standInRows: any[] = []; + try { + records = await this.readActiveOverlayRows( + { type: request.type, ...(request.packageId ? { packageId: request.packageId } : {}) }, + ); + if (request.packageId) { + standInRows = (await this.readActiveOverlayRows({ type: request.type })) + .filter((row) => (row?.package_id ?? null) === null); + } + } catch (error) { + // [#5532] The list read's rule: only an unprovisioned store means + // "no rows". Any other failure is not answered as "nothing expands + // this name". + this.rethrowUnlessMetadataStoreUnprovisioned(error, 'sys_metadata'); + } + const rows = [...records, ...standInRows]; + if (this.namesWithOwnStoredRow(rows, request.packageId).has(request.name)) return undefined; + return servedViewExpansion( + this.expandStoredViewContainers(request.type, this.storedOverlayEntries(request, rows)), + request.name, + request.packageId, + ); +} + +/** The body of `ObjectStackProtocolImplementation#expandUnderOwnName`; the method's docblock there is its contract. */ +export function expandUnderOwnName( + this: Pick, + type: string, + object: string, + container: Record, + ownPackageId: string | undefined, +): Record[] { + const ownName = typeof container.name === 'string' && container.name !== '' ? container.name : undefined; + if (ownName === undefined) return []; + const under = `${object}.${ownName}`; + const expandAt = under; + const list = container.list; + const bare = !!list && typeof list === 'object' && !(typeof list.name === 'string' && list.name !== ''); + const source = bare ? { ...container, list: { ...list, name: ownName } } : container; + const bareSpelled = `${expandAt}.${ownName}`; + const underIsShipped = this.isShippedByAnotherPackage(type, under, ownPackageId); + return expandViewContainer(expandAt, source).map((vi) => { + const item: Record = { ...vi, object }; + const name = String(item.name); + const fromBare = bare + && item.viewKind === 'list' + && item.config?.name === ownName + && name.startsWith(bareSpelled) + && /^(_\d+)?$/.test(name.slice(bareSpelled.length)); + if (fromBare) { + const authored = { ...item.config }; + delete authored.name; + item.config = authored; + if (!underIsShipped) { + item.name = under; + delete item._diagnostics; + } + } + return item; + }); +} diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index f87c06f30e4..bee4f187c36 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -408,6 +408,8 @@ import { resolveWritePackageScope } from './protocol-write-package-scope.js'; import { readItemFromMetadataService } from './protocol-metadata-service-read.js'; // [#25467 cut 21] The boot report of the stored overlay rows no read serves because they overlay sealed managed content. import { reportSealedOverlayRows } from './protocol-sealed-overlay-report.js'; +// [#25467 cut 22] A row-less view name's expansion for the by-name read, and a container's expansion under its own name. +import { expandUnderOwnName, resolveRowlessExpandedView } from './protocol-view-expansion-names.js'; /** * Canonical Zod schema per metadata type lives in @@ -2869,7 +2871,7 @@ export function addressPackages(packageId: string | undefined): ReadonlyArray( +export function servedViewExpansion( expansions: ReadonlyArray<{ item: Record; container: E }>, name: string, packageId: string | undefined, @@ -6251,7 +6253,7 @@ export interface StoredOverlayEntry { * list read serves under the name, and the stored container row it derives * from. */ -interface RowlessExpandedView { +export interface RowlessExpandedView { item: Record; container: StoredOverlayEntry; } @@ -10306,33 +10308,15 @@ export class ObjectStackProtocolImplementation implements private async resolveRowlessExpandedView( request: { type: string; name: string; packageId?: string }, ): Promise { - if ((PLURAL_TO_SINGULAR[request.type] ?? request.type) !== 'view') return undefined; - let records: any[] = []; - // [#21967] Naming a package, the package-less rows in scope stand in, - // read as the list scoped to that package reads them (the - // package-agnostic read, filtered back to the package-less rows). - let standInRows: any[] = []; - try { - records = await this.readActiveOverlayRows( - { type: request.type, ...(request.packageId ? { packageId: request.packageId } : {}) }, - ); - if (request.packageId) { - standInRows = (await this.readActiveOverlayRows({ type: request.type })) - .filter((row) => (row?.package_id ?? null) === null); - } - } catch (error) { - // [#5532] The list read's rule: only an unprovisioned store means - // "no rows". Any other failure is not answered as "nothing expands - // this name". - this.rethrowUnlessMetadataStoreUnprovisioned(error, 'sys_metadata'); - } - const rows = [...records, ...standInRows]; - if (this.namesWithOwnStoredRow(rows, request.packageId).has(request.name)) return undefined; - return servedViewExpansion( - this.expandStoredViewContainers(request.type, this.storedOverlayEntries(request, rows)), - request.name, - request.packageId, - ); + // [#25467 cut 22] The body moved to `protocol-view-expansion-names.ts` (see `ViewExpansionNamesHost`). + const host: ThisParameterType = { + expandStoredViewContainers: (...a) => this.expandStoredViewContainers(...a), + namesWithOwnStoredRow: (...a) => this.namesWithOwnStoredRow(...a), + readActiveOverlayRows: (...a) => this.readActiveOverlayRows(...a), + rethrowUnlessMetadataStoreUnprovisioned: (...a) => this.rethrowUnlessMetadataStoreUnprovisioned(...a), + storedOverlayEntries: (...a) => this.storedOverlayEntries(...a), + }; + return resolveRowlessExpandedView.call(host, request); } /** @@ -18162,34 +18146,11 @@ export class ObjectStackProtocolImplementation implements container: Record, ownPackageId: string | undefined, ): Record[] { - const ownName = typeof container.name === 'string' && container.name !== '' ? container.name : undefined; - if (ownName === undefined) return []; - const under = `${object}.${ownName}`; - const expandAt = under; - const list = container.list; - const bare = !!list && typeof list === 'object' && !(typeof list.name === 'string' && list.name !== ''); - const source = bare ? { ...container, list: { ...list, name: ownName } } : container; - const bareSpelled = `${expandAt}.${ownName}`; - const underIsShipped = this.isShippedByAnotherPackage(type, under, ownPackageId); - return expandViewContainer(expandAt, source).map((vi) => { - const item: Record = { ...vi, object }; - const name = String(item.name); - const fromBare = bare - && item.viewKind === 'list' - && item.config?.name === ownName - && name.startsWith(bareSpelled) - && /^(_\d+)?$/.test(name.slice(bareSpelled.length)); - if (fromBare) { - const authored = { ...item.config }; - delete authored.name; - item.config = authored; - if (!underIsShipped) { - item.name = under; - delete item._diagnostics; - } - } - return item; - }); + // [#25467 cut 22] The body moved to `protocol-view-expansion-names.ts` (see `ViewExpansionNamesHost`). + const host: ThisParameterType = { + isShippedByAnotherPackage: (...a) => this.isShippedByAnotherPackage(...a), + }; + return expandUnderOwnName.call(host, type, object, container, ownPackageId); } /**