Files
rea/src/application/environment/environmentStateBulkAnswer.ts
T

192 lines
6.3 KiB
TypeScript

import {
bulkCollectionOf,
namedEntities,
} from "../../domain/analysisView/environmentStateWriteAddress.js";
import {
bodyProblem,
updatedEntity,
updatedFields,
writtenBulkResponse,
} from "../../domain/analysisView/environmentStateWriteApply.js";
import type { FieldRule } from "../../domain/analysisView/environmentStateWriteRules.js";
import {
ownMember,
type JsonObject,
type StateEntity,
} from "../../domain/analysisView/environmentStateRules.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { err, ok, type Result } from "../../domain/result.js";
import { callSegments } from "./environmentStateCallLines.js";
import {
namedEntity,
refused,
type StateAnswer,
type StateServing,
} from "./environmentStateAnswer.js";
import {
isLoadableValue,
unloadableMember,
} from "./environmentStateDocument.js";
import {
generator,
observedEntity,
pathValues,
unloadable,
heldEntity,
updateTarget,
writeAnswer,
type WriteRequest,
} from "./environmentStateWriteAnswer.js";
import { environmentToolArguments } from "./environmentTools.js";
/** Rules whose generated value is drawn once per call, which one call cannot give several entities. */
const DRAWN_ONCE = new Set<FieldRule["rule"]>(["unique", "renewed"]);
/** One entity a bulk call names, held by the state, with the members it gets. */
interface BulkTarget {
readonly entity: StateEntity;
readonly body: JsonObject | undefined;
}
/** The held entities a bulk call names, with the members each gets, or a refusal for an entity the state does not hold. */
const namedTargets = (
serving: StateServing,
request: WriteRequest,
): Result<BulkTarget[], AnalysisError> => {
const { tool, plan, args } = request;
const { address } = plan;
const type = plan.target ?? "";
const name = environmentToolArguments(tool, true).body;
const body =
name === undefined ? undefined : (ownMember(args, name) as JsonValue);
if (body !== undefined && !isLoadableValue(body))
return err(unloadable(tool, "The body"));
const named =
address === undefined || address.by === "action"
? undefined
: namedEntities(address, body, serving.store.type(type)?.id_field ?? "");
if (named === undefined)
return err(
refused(
tool,
`Tool ${tool.name} names the entities it writes by their ids in its body, as its recording did.`,
),
);
const segments = callSegments(tool, args);
if (segments === undefined)
return err(refused(tool, `Tool ${tool.name} needs every path argument.`));
const collection = bulkCollectionOf(type, segments, (text) => ({
text,
supplied: false,
}));
const targets: BulkTarget[] = [];
for (const { id, body: members } of named) {
// A body's id is the entity's as written, never a percent-encoded spelling of another.
const entity = namedEntity(serving.store, type, {
segments: collection,
id,
literal: true,
});
if (entity === undefined)
return err(
refused(
tool,
`The state holds no ${type} entity ${id}; the state is unchanged.`,
),
);
if (targets.some((target) => target.entity === entity))
return err(
refused(
tool,
`The body names ${type} entity ${entity.id} more than once; the state is unchanged.`,
),
);
targets.push({ entity, body: members });
}
return ok(targets);
};
/** Each named entity as the call writes it, or a refusal: a delete takes only the members its recording sent, an update gets its fields with the call's shared values. */
const writtenTargets = (
serving: StateServing,
request: WriteRequest,
targets: readonly BulkTarget[],
values: ReturnType<typeof generator>,
): Result<StateEntity[], AnalysisError> => {
const { tool, plan, args } = request;
const observed = observedEntity(serving, plan) ?? {};
const path = { path: tool.path_template, pathValues: pathValues(tool, args) };
const written: StateEntity[] = [];
for (const { entity, body } of targets) {
const call = { ...path, body, id: entity.id };
if (plan.effect !== "update") {
const problem =
plan.body === null
? "The tool has no modelled body."
: bodyProblem(plan.body, call, { entity: {}, id: entity.id });
if (problem !== undefined) return err(refused(tool, problem));
written.push(entity);
continue;
}
const fields = updatedFields(
plan,
{ before: entity.fields, observed },
call,
values.forTarget(updateTarget(serving, entity, observed)),
heldEntity(serving),
);
if (!fields.ok) return err(refused(tool, fields.error));
const field = unloadableMember(fields.value);
if (field !== undefined) return err(unloadable(tool, `Field ${field}`));
written.push(updatedEntity(entity, fields.value));
}
return ok(written);
};
/** Apply one served bulk call to every entity its body names and answer it; a refused call leaves the state unchanged. */
export const answerBulk = (
serving: StateServing,
request: WriteRequest,
): Result<StateAnswer, AnalysisError> => {
const { tool, plan } = request;
const targets = namedTargets(serving, request);
if (!targets.ok) return targets;
const update = plan.effect === "update";
if (
update &&
targets.value.length > 1 &&
plan.rules.some((rule) => DRAWN_ONCE.has(rule.rule))
)
return err(
refused(
tool,
`Tool ${tool.name} generates a value per call, so it writes one entity per call here.`,
),
);
const [first] = targets.value;
const values = generator(serving, request);
const written = writtenTargets(serving, request, targets.value, values);
if (!written.ok) return written;
const answer = writeAnswer(
serving,
request,
(recorded) => writtenBulkResponse(plan.response!, recorded, written.value),
{
source: "state",
view: update ? "update" : "delete",
type: first!.entity.type,
collection: first!.entity.collection,
id: first!.entity.id,
ids: written.value.map((entity) => entity.id),
},
);
if (!answer.ok) return answer;
if (update) {
const updated = serving.store.updateAll(written.value);
if (!updated.ok) return updated;
} else serving.store.removeAll(written.value);
serving.store.commitWrite(values.taken);
return answer;
};