mirror of
https://github.com/morluto/rea.git
synced 2026-11-08 15:56:35 +00:00
merge: inherit environment review fixes
This commit is contained in:
commit
c37964b080
7 files changed
+83
-10
No files matched your search
@@ -296,6 +296,7 @@ export class EnvironmentStateWriteSimulation {
|
||||
updates: [],
|
||||
sequences: new Map(),
|
||||
starts: new Map(),
|
||||
createCollections: [],
|
||||
steady: new Set(),
|
||||
unstamped: new Set(),
|
||||
kept: new Set(),
|
||||
|
||||
@@ -32,6 +32,22 @@ describe("deletes the application answers for afterwards", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("withholds a soft delete when its read changes another field", () => {
|
||||
const rows: readonly TimedCaptureRow[] = SOFT_DELETED_ROWS.map((row) =>
|
||||
row[0] === "GET" &&
|
||||
row[1] === "/api/contacts/2" &&
|
||||
typeof row[2] === "object" &&
|
||||
row[2] !== null &&
|
||||
!Array.isArray(row[2]) &&
|
||||
row[2]["deleted"] === true
|
||||
? [row[0], row[1], { ...row[2], name: "Changed by deletion" }]
|
||||
: row,
|
||||
);
|
||||
expect(planOf(rows, "delete_contacts_by_id")).toMatchObject({
|
||||
served: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("learns on the item view the flag later reads set on a soft-deleted contact", () => {
|
||||
const item = (rows: readonly TimedCaptureRow[]) =>
|
||||
viewOf(rows).views.find((view) => view.tool === "get_contacts_by_id");
|
||||
|
||||
@@ -29,6 +29,29 @@ const ruleFor = (plan: ReturnType<typeof createOf>, field: string): unknown =>
|
||||
plan.rules.find((rule) => rule.field === field);
|
||||
|
||||
describe("values an application counts and makes with each created entity", () => {
|
||||
it("does not adopt a sequence that contradicts an earlier accepted create", () => {
|
||||
const a = { id: 1, title: "A", index: 1 };
|
||||
const b = { id: 2, title: "B", index: 2 };
|
||||
const c = { id: 3, title: "C", index: 15 };
|
||||
const d = { id: 4, title: "D", index: 15 };
|
||||
const e = { id: 5, title: "E", index: 16 };
|
||||
const rows: readonly TimedCaptureRow[] = [
|
||||
["GET", "/api/tasks", [a, b]],
|
||||
["GET", "/api/tasks/1", a],
|
||||
["POST", "/api/tasks", c, 201, { title: "C" }],
|
||||
["GET", "/api/tasks/3", c],
|
||||
["GET", "/api/tasks", [a, b, c]],
|
||||
["POST", "/api/tasks", d, 201, { title: "D" }],
|
||||
["GET", "/api/tasks/4", d],
|
||||
["GET", "/api/tasks", [a, b, c, d]],
|
||||
["POST", "/api/tasks", e, 201, { title: "E" }],
|
||||
["GET", "/api/tasks/5", e],
|
||||
["GET", "/api/tasks", [a, b, c, d, e]],
|
||||
];
|
||||
const plan = createOf(rows);
|
||||
expect(plan).toMatchObject({ served: false });
|
||||
expect(ruleFor(plan, "index")).toBeUndefined();
|
||||
});
|
||||
it("learns a counter from where the first create stood, a template around it and the object made with each task", () => {
|
||||
const plan = createOf(COUNTED_TASK_ROWS);
|
||||
expect(plan).toMatchObject({
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
ownMember,
|
||||
at,
|
||||
decodedSegment,
|
||||
deletedFlag,
|
||||
isObject,
|
||||
listItems,
|
||||
listsAll,
|
||||
markedDeleted,
|
||||
missingRequiredFields,
|
||||
pathSegmentMatches,
|
||||
renderStateEntity,
|
||||
@@ -57,7 +57,7 @@ export class SimulatedState {
|
||||
readonly #entities = new Map<string, StateEntity>();
|
||||
readonly #collections = new Map<string, string[]>();
|
||||
readonly #documents = new Map<string, JsonValue>();
|
||||
readonly #removed: RemovedTarget[] = [];
|
||||
readonly #removed: StateEntity[] = [];
|
||||
readonly #served = new ServedOrder();
|
||||
/** Each held id's targets, of any type. */
|
||||
readonly #named = new Map<string, Set<string>>();
|
||||
@@ -265,11 +265,20 @@ export class SimulatedState {
|
||||
target.id === entity.id,
|
||||
);
|
||||
if (index >= 0) this.#removed.splice(index, 1);
|
||||
this.#removed.push({
|
||||
type: entity.type,
|
||||
collection: entity.collection,
|
||||
id: entity.id,
|
||||
});
|
||||
this.#removed.push(entity);
|
||||
}
|
||||
|
||||
deleted(
|
||||
type: string,
|
||||
collection: string,
|
||||
id: string,
|
||||
): StateEntity | undefined {
|
||||
return this.#removed.find(
|
||||
(entity) =>
|
||||
entity.type === type &&
|
||||
entity.collection === collection &&
|
||||
entity.id === id,
|
||||
);
|
||||
}
|
||||
|
||||
/** Removed targets of this type the state does not hold again. */
|
||||
@@ -497,9 +506,20 @@ export const checkItemRead = (
|
||||
return check.body === undefined || entity === undefined
|
||||
? undefined
|
||||
: { pass: false, targets, unheld: [] };
|
||||
// An entity the application answers for as soft-deleted is one serving no longer holds.
|
||||
if (markedDeleted(object))
|
||||
return { pass: entity === undefined, targets, unheld: [] };
|
||||
// Serving keeps a removed entity's fields and changes only the soft-delete flag.
|
||||
const flag = deletedFlag(object);
|
||||
if (flag !== undefined) {
|
||||
const removed = state.deleted(view.type, collection, id);
|
||||
if (entity !== undefined) return { pass: false, targets, unheld: [] };
|
||||
if (removed === undefined) return { pass: true, targets, unheld: [] };
|
||||
const deleted = { ...removed, fields: { ...removed.fields, [flag]: true } };
|
||||
const shown = renderStateEntity(deleted, view, check.volatile, object);
|
||||
return {
|
||||
pass: same(replaceAt(check.base, view.pointer, shown), check.body),
|
||||
targets,
|
||||
unheld: [],
|
||||
};
|
||||
}
|
||||
if (entity === undefined) return { pass: false, targets, unheld: targets };
|
||||
// Serving refuses an entity lacking a field every recording carried, so no read can confirm one.
|
||||
if (missingRequiredFields(entity, view, check.volatile).length > 0)
|
||||
|
||||
@@ -311,6 +311,7 @@ const earlierOf = (draft: WriteDraft) => ({
|
||||
kept: draft.kept,
|
||||
clockOf: (field: string) => clockEvidence(draft, field),
|
||||
startOf: (field: string) => draft.starts.get(field),
|
||||
createCollections: draft.createCollections,
|
||||
ampersands: draft.ampersands,
|
||||
});
|
||||
|
||||
@@ -978,6 +979,8 @@ export const applyCreate = (
|
||||
draft.plan.rules,
|
||||
);
|
||||
const inserted = insertCreated(context, draft, entity(stored, id));
|
||||
context.io.retain(collection.length * 2 + 16);
|
||||
draft.createCollections.push(collection);
|
||||
return {
|
||||
kind: "applied",
|
||||
targets: [inserted, collectionTarget(type, collection)],
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface EarlierEvidence {
|
||||
readonly clockOf: (field: string) => JsonValue | undefined;
|
||||
/** The integer the exemplar showed in an unruled field above every value its type held, where a sequence may start. */
|
||||
readonly startOf?: (field: string) => number | undefined;
|
||||
readonly createCollections?: readonly string[];
|
||||
/** How the plan's accepted calls and the type's held entities spelled `&` in each field's slug; absent where none showed one. */
|
||||
readonly ampersands?: ReadonlyMap<string, AmpersandSpelling>;
|
||||
}
|
||||
@@ -269,6 +270,13 @@ const spellsExemplar = (
|
||||
}
|
||||
if (rule.rule !== "sequence") return undefined;
|
||||
const start = calls.earlier.startOf?.(field);
|
||||
const collections = calls.earlier.createCollections ?? [];
|
||||
if (
|
||||
(rule.scope === "type" && collections.length > 1) ||
|
||||
(rule.scope === "collection" &&
|
||||
new Set(collections).size !== collections.length)
|
||||
)
|
||||
return undefined;
|
||||
return start !== undefined && sequenceNumber(value) === start
|
||||
? { ...rule, start }
|
||||
: undefined;
|
||||
|
||||
@@ -100,6 +100,8 @@ export interface WriteDraft {
|
||||
readonly sequences: Map<string, number>;
|
||||
/** Integers the exemplar showed in fields no rule wrote, above every value its type held, where a later call's sequence may start. */
|
||||
readonly starts: Map<string, number>;
|
||||
/** Collections of accepted create calls, used to check late sequence adoption. */
|
||||
readonly createCollections: string[];
|
||||
/** The collection a create writes into when its own path names no state type: the one its returned id was read under. */
|
||||
readonly linked?: string | undefined;
|
||||
/** Fields an item action's call found already at the value it sets them to. */
|
||||
|
||||
Reference in new issue
Block a user