Files
reverse-engineering-anything/src/application/environment/environmentStateCollections.ts
T
Muhammad Hamzaandgensokyo-dev 9e6c6241d7 fix(environment): match served items whose route repeats a separator (#6876)
* fix(environment): match served items whose route repeats a separator

callSegments split the tool path template itself and dropped empty literal
segments, while the held collection is split by segmentsOf, which keeps an
internal empty segment. A captured item route such as /api//books/{id} was
stored under collection /api//books but searched with ["api","books"], so
collectionMatch rejected it and a served item read reported the extracted
entity as missing.

Split the call template through segmentsOf, the same owner used for the held
collection path, so a repeated separator is preserved on both sides and the
item answers from its extracted collection. Single-separator routes stay
distinct and param positions are unchanged.

Fixes #6847

* fix(environment): keep repeated-separator collections writable

Preserving empty literal segments in callSegments made createdCollection's
missing-argument guard reject a create whose route repeats a separator, such
as POST /api//books: the empty literal segment tripped the
segments.some(text.length === 0) check and answerCreate refused the valid
route. Restrict the guard to empty supplied parameter segments, so a missing
path argument is still refused while a repeated-separator collection stays
writable as well as readable.

---------

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-24 04:33:51 +08:00

44 lines
1.9 KiB
TypeScript

import {
collectionPathOf,
selectedCollection,
} from "../../domain/analysisView/environmentStateRules.js";
import type { StateServing } from "./environmentStateAnswer.js";
import { callSegments } from "./environmentStateCallLines.js";
import type { ReplayArguments } from "./environmentReplay.js";
import type { EnvironmentManifestTool } from "./environmentSource.js";
/** The collections a create may go to: those the store knows, held ones first, then those the type's checked list lines read, so an empty one keeps its spelling. */
const creatableCollectionsOf = (
serving: StateServing,
type: string,
): readonly string[] => {
const listed: string[] = [];
for (const view of serving.store.state.views.values())
if (view.shape === "list" && view.type === type)
for (const ordinal of view.checked_record_ordinals) {
const exchange = serving.source.exchanges.get(ordinal);
if (exchange !== undefined)
listed.push(collectionPathOf(exchange, false));
}
return [...new Set([...serving.store.knownCollectionsOf(type), ...listed])];
};
/** The collection a create writes to: a known collection the call's path matches, else the path as supplied. */
export const createdCollection = (
serving: StateServing,
tool: EnvironmentManifestTool,
args: ReplayArguments,
type: string,
): string | undefined => {
const segments = callSegments(tool, args);
// A supplied path argument may not be empty; an empty literal segment, from a
// repeated separator in the route, is part of the collection's spelling.
if (
segments === undefined ||
segments.some(({ text, supplied }) => supplied && text.length === 0)
)
return undefined;
// The spelling the call supplies verbatim wins, as item calls choose their entity, then the first known: held ones lead.
return selectedCollection(segments, creatableCollectionsOf(serving, type));
};