mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 22:06:10 +00:00
fix(environment): answer sent empty members as sent, and keep a placed create's order
- A write answer spells an empty list or map the call did not send as its recorded answer did; one the call sent is answered as sent, so a create sending `[]` is not answered `null` because the first recorded create was. - A read listing the whole collection that placed a create leaves the create only the sorted inserts whose order that read follows, so a later rename cannot teach a sort an earlier read ruled out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0c0c6ef7c5
commit
b490c8fd51
9 files changed
+178
-44
No files matched your search
@@ -32,11 +32,12 @@ natural-language tasks ask for, or withheld them:
|
|||||||
the new value sorts, in replay and serving (a bulk update moving each entity in turn), and its plan carries that
|
the new value sorts, in replay and serving (a bulk update moving each entity in turn), and its plan carries that
|
||||||
`insert`. When a read listing the whole collection fails after an update and the type's creates are not sorted by a
|
`insert`. When a read listing the whole collection fails after an update and the type's creates are not sorted by a
|
||||||
field the update writes, replay tries the collection sorted by such a field, unless a create was placed by a sort
|
field the update writes, replay tries the collection sorted by such a field, unless a create was placed by a sort
|
||||||
that excludes it; on a match, the type's creates and updates take that order.
|
that excludes it or a whole-collection read that placed a create did not follow it; on a match, the type's creates
|
||||||
|
and updates take that order.
|
||||||
- **Unloaded answers.** A create's later item read is taken as its observation when it differs from the answer only in
|
- **Unloaded answers.** A create's later item read is taken as its observation when it differs from the answer only in
|
||||||
values the answer had not loaded: empty, zero, `false` or Go's zero time, or objects whose members are each the same
|
values the answer had not loaded: empty, zero, `false` or Go's zero time, or objects whose members are each the same
|
||||||
or loaded so. An unsent update field that changes only between `null`, `[]` and `{}` is unchanged. Write answers
|
or loaded so. An unsent update field that changes only between `null`, `[]` and `{}` is unchanged. Write answers
|
||||||
spell empty lists and maps as their recorded answers did.
|
spell empty lists and maps the call did not send as their recorded answers did, and a sent one as sent.
|
||||||
- **Unruled fields.** A later create that stored a field no rule writes otherwise than the plan keeps it is
|
- **Unruled fields.** A later create that stored a field no rule writes otherwise than the plan keeps it is
|
||||||
`unexplained-change` at the create, so its recorded entity is held and later reads check other writes as before.
|
`unexplained-change` at the create, so its recorded entity is held and later reads check other writes as before.
|
||||||
- **Skill.** The two scratch creates differ in what they send, the second only what a person must give, however they
|
- **Skill.** The two scratch creates differ in what they send, the second only what a person must give, however they
|
||||||
|
|||||||
@@ -757,10 +757,12 @@ no longer confirms the create; one that disagrees or omits an answer field its
|
|||||||
item view renders still checks it. Clock agreement uses the coarser recorded
|
item view renders still checks it. Clock agreement uses the coarser recorded
|
||||||
precision, including microseconds. A list read
|
precision, including microseconds. A list read
|
||||||
only adds fields the answer lacks and still checks it. Serving answers a create
|
only adds fields the answer lacks and still checks it. Serving answers a create
|
||||||
in its answer's shape and clock precision, with empty lists and maps spelled
|
in its answer's shape and clock precision, with empty lists and maps the call
|
||||||
(`null`, `[]` or `{}`) as its answer spelled them, and stores the fields of both with
|
did not send spelled (`null`, `[]` or `{}`) as its answer spelled them, and
|
||||||
the item read-back's clock precision. Updates answer empty lists and maps as
|
stores the fields of both with the item read-back's clock precision. Updates
|
||||||
their answers spelled them too. A `reference` is an object holding fields of the
|
answer empty lists and maps they did not send as their answers spelled them
|
||||||
|
too; a member a call sent is answered as sent. A `reference` is an object
|
||||||
|
holding fields of the
|
||||||
entity a sent member names, as the state holds it (a parent beside `parentId`):
|
entity a sent member names, as the state holds it (a parent beside `parentId`):
|
||||||
the member is accepted with any value, serving looks the entity up and refuses a
|
the member is accepted with any value, serving looks the entity up and refuses a
|
||||||
call naming one the environment does not hold or holds in more than one
|
call naming one the environment does not hold or holds in more than one
|
||||||
@@ -853,7 +855,8 @@ to where the new value sorts (its own `insert`); a bulk update moves each entity
|
|||||||
it writes in turn, as replay applies them. When a read listing the whole
|
it writes in turn, as replay applies them. When a read listing the whole
|
||||||
collection fails after an update and the type's creates are not sorted by a field
|
collection fails after an update and the type's creates are not sorted by a field
|
||||||
that update writes, the view tries the collection sorted by such a field, unless
|
that update writes, the view tries the collection sorted by such a field, unless
|
||||||
a create was placed by a sort that excludes it; on a match, the type's creates
|
a create was placed by a sort that excludes it or a read listing the whole
|
||||||
|
collection that placed a create did not follow it; on a match, the type's creates
|
||||||
and updates take that order, as Grafana keeps folders by title though no earlier
|
and updates take that order, as Grafana keeps folders by title though no earlier
|
||||||
read could tell that from creation order, or from id order where both ascend.
|
read could tell that from creation order, or from id order where both ascend.
|
||||||
|
|
||||||
|
|||||||
@@ -172,7 +172,15 @@ export const answerBulk = (
|
|||||||
const answer = writeAnswer(
|
const answer = writeAnswer(
|
||||||
serving,
|
serving,
|
||||||
request,
|
request,
|
||||||
(recorded) => writtenBulkResponse(plan.response!, recorded, written.value),
|
(recorded) =>
|
||||||
|
writtenBulkResponse(
|
||||||
|
plan.response!,
|
||||||
|
recorded,
|
||||||
|
written.value.map((entity, index) => ({
|
||||||
|
...entity,
|
||||||
|
sent: Object.keys(targets.value[index]!.body ?? {}),
|
||||||
|
})),
|
||||||
|
),
|
||||||
{
|
{
|
||||||
source: "state",
|
source: "state",
|
||||||
view: update ? "update" : "delete",
|
view: update ? "update" : "delete",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
BULK_RENAMED_LABEL_ROWS,
|
BULK_RENAMED_LABEL_ROWS,
|
||||||
|
ECHOED_EMPTY_ROWS,
|
||||||
formLabelRows,
|
formLabelRows,
|
||||||
LISTED_ID_ROWS,
|
LISTED_ID_ROWS,
|
||||||
NOTE_DEFAULT_ROWS,
|
NOTE_DEFAULT_ROWS,
|
||||||
@@ -43,6 +44,20 @@ describe("served creates and updates modelled on form recordings", () => {
|
|||||||
).toMatchObject({ message: expect.stringContaining('"name"') });
|
).toMatchObject({ message: expect.stringContaining('"name"') });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("answers an empty member a call sent as it was sent, whichever spelling the recorded answer used", () => {
|
||||||
|
const labels = servedFrom(ECHOED_EMPTY_ROWS);
|
||||||
|
const tags = (sent: JsonValue) => {
|
||||||
|
const body = labels.answer("post_labels", {
|
||||||
|
body: { name: `Spare ${JSON.stringify(sent)}`, tags: sent },
|
||||||
|
}).body;
|
||||||
|
if (typeof body !== "object" || body === null || Array.isArray(body))
|
||||||
|
throw new Error("Expected an object body");
|
||||||
|
return body["tags"];
|
||||||
|
};
|
||||||
|
expect(tags([])).toEqual([]);
|
||||||
|
expect(tags(null)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("moves a renamed entity to where the collection's order puts its new name", () => {
|
it("moves a renamed entity to where the collection's order puts its new name", () => {
|
||||||
const labels = servedFrom(RENAMED_LABEL_ROWS);
|
const labels = servedFrom(RENAMED_LABEL_ROWS);
|
||||||
labels.answer("patch_labels_by_id", { id: "1", body: { name: "Zinc" } });
|
labels.answer("patch_labels_by_id", { id: "1", body: { name: "Zinc" } });
|
||||||
|
|||||||
@@ -334,7 +334,11 @@ export const writeAnswer = (
|
|||||||
serving: StateServing,
|
serving: StateServing,
|
||||||
request: WriteRequest,
|
request: WriteRequest,
|
||||||
rendered:
|
rendered:
|
||||||
| { readonly fields: JsonObject | undefined; readonly id: string }
|
| {
|
||||||
|
readonly fields: JsonObject | undefined;
|
||||||
|
readonly id: string;
|
||||||
|
readonly sent?: readonly string[];
|
||||||
|
}
|
||||||
| ((
|
| ((
|
||||||
recorded: JsonValue | undefined,
|
recorded: JsonValue | undefined,
|
||||||
) => Result<JsonValue | undefined, string>),
|
) => Result<JsonValue | undefined, string>),
|
||||||
@@ -520,7 +524,7 @@ const answerCreate = (
|
|||||||
const answer = writeAnswer(
|
const answer = writeAnswer(
|
||||||
serving,
|
serving,
|
||||||
request,
|
request,
|
||||||
{ fields: fields.value, id },
|
{ fields: fields.value, id, sent: Object.keys(call.body ?? {}) },
|
||||||
state,
|
state,
|
||||||
);
|
);
|
||||||
if (!answer.ok) return answer;
|
if (!answer.ok) return answer;
|
||||||
@@ -592,7 +596,11 @@ const answerUpdate = (
|
|||||||
const answer = writeAnswer(
|
const answer = writeAnswer(
|
||||||
serving,
|
serving,
|
||||||
request,
|
request,
|
||||||
{ fields: written.value, id: entity.id },
|
{
|
||||||
|
fields: written.value,
|
||||||
|
id: entity.id,
|
||||||
|
sent: Object.keys(call.body ?? {}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: "state",
|
source: "state",
|
||||||
view: "update",
|
view: "update",
|
||||||
|
|||||||
@@ -711,8 +711,10 @@ export class EnvironmentStateWriteSimulation {
|
|||||||
});
|
});
|
||||||
for (const name of shown.confirmed) {
|
for (const name of shown.confirmed) {
|
||||||
this.#drafts.get(name)!.confirmed.push(record.ordinal);
|
this.#drafts.get(name)!.confirmed.push(record.ordinal);
|
||||||
if (showsPlacement(view, outcome, this.#inserted, name))
|
if (showsPlacement(view, outcome, this.#inserted, name)) {
|
||||||
this.#placed.add(name);
|
this.#placed.add(name);
|
||||||
|
if (view.shape === "list") this.#narrowInserts(view, record, name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return shown.unseen;
|
return shown.unseen;
|
||||||
}
|
}
|
||||||
@@ -821,17 +823,12 @@ export class EnvironmentStateWriteSimulation {
|
|||||||
for (const key of failed.keys) this.#taint.fail(key);
|
for (const key of failed.keys) this.#taint.fail(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** What `use` makes of a read's items when the read lists the whole collection the state holds, the only read that shows the order it is kept in. */
|
||||||
* The successful check with the collection sorted by a field an update the read exercised writes, when the read
|
#wholeListing<T>(
|
||||||
* lists the whole collection in that order and no create of the type was placed by a sort that excludes it; the
|
|
||||||
* type's creates then insert by it, and its updates move their entity by it. A sort the creates chose by a field the
|
|
||||||
* update did not write does not stop this, since both may have fit every earlier read.
|
|
||||||
*/
|
|
||||||
#retrySort(
|
|
||||||
view: Extract<StateView, { shape: "list" }>,
|
view: Extract<StateView, { shape: "list" }>,
|
||||||
record: StateRecordFacts,
|
record: StateRecordFacts,
|
||||||
exercised: ReadonlySet<string>,
|
use: (items: readonly JsonObject[]) => T,
|
||||||
): CheckOutcome | undefined {
|
): T | undefined {
|
||||||
const line = this.#line(view, record);
|
const line = this.#line(view, record);
|
||||||
const idField = this.#types.get(view.type)?.id_field;
|
const idField = this.#types.get(view.type)?.id_field;
|
||||||
if (line === undefined || idField === undefined) return undefined;
|
if (line === undefined || idField === undefined) return undefined;
|
||||||
@@ -840,6 +837,53 @@ export class EnvironmentStateWriteSimulation {
|
|||||||
.members(view.type, line.collection)
|
.members(view.type, line.collection)
|
||||||
.map((entity) => entity.id),
|
.map((entity) => entity.id),
|
||||||
);
|
);
|
||||||
|
return this.#input.io.withBody(record.ordinal, (body) => {
|
||||||
|
const items =
|
||||||
|
body === undefined ? undefined : listItems(body, view.pointer);
|
||||||
|
if (
|
||||||
|
items === undefined ||
|
||||||
|
items.length < 2 ||
|
||||||
|
items.length !== held.size ||
|
||||||
|
!items.every((item) =>
|
||||||
|
held.has(scalarText(ownMember(item, idField)) ?? ""),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return undefined;
|
||||||
|
return use(items);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A whole-collection read that placed a create leaves it only the sorted inserts whose order the read follows. */
|
||||||
|
#narrowInserts(
|
||||||
|
view: Extract<StateView, { shape: "list" }>,
|
||||||
|
record: StateRecordFacts,
|
||||||
|
name: string,
|
||||||
|
): void {
|
||||||
|
const draft = this.#drafts.get(name)!;
|
||||||
|
const kept = this.#wholeListing(view, record, (items) =>
|
||||||
|
draft.candidates.filter(
|
||||||
|
(candidate) =>
|
||||||
|
candidate.rule !== "sorted" ||
|
||||||
|
same(candidate, draft.plan.insert) ||
|
||||||
|
orderedBy(items, candidate.field, candidate.direction),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (kept !== undefined) draft.candidates = kept;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The successful check with the collection sorted by a field an update the read exercised writes, when the read
|
||||||
|
* lists the whole collection in that order and no create of the type was placed by a sort that excludes it; the
|
||||||
|
* type's creates then insert by it, and its updates move their entity by it. A sort the creates chose by a field the
|
||||||
|
* update did not write does not stop this when every whole-collection read that placed them followed this order too.
|
||||||
|
*/
|
||||||
|
#retrySort(
|
||||||
|
view: Extract<StateView, { shape: "list" }>,
|
||||||
|
record: StateRecordFacts,
|
||||||
|
exercised: ReadonlySet<string>,
|
||||||
|
): CheckOutcome | undefined {
|
||||||
|
const line = this.#line(view, record);
|
||||||
|
if (line === undefined) return undefined;
|
||||||
const written = new Set(
|
const written = new Set(
|
||||||
[...exercised].flatMap((name) => {
|
[...exercised].flatMap((name) => {
|
||||||
const { plan } = this.#drafts.get(name)!;
|
const { plan } = this.#drafts.get(name)!;
|
||||||
@@ -858,27 +902,16 @@ export class EnvironmentStateWriteSimulation {
|
|||||||
draft.plan.target === view.type &&
|
draft.plan.target === view.type &&
|
||||||
draft.observed !== undefined,
|
draft.observed !== undefined,
|
||||||
);
|
);
|
||||||
const rules = this.#input.io.withBody(record.ordinal, (body) => {
|
const rules =
|
||||||
const items =
|
this.#wholeListing(view, record, (items) =>
|
||||||
body === undefined ? undefined : listItems(body, view.pointer);
|
[...written].flatMap((field) =>
|
||||||
// Only a read of the whole collection shows the order it is kept in.
|
(["ascending", "descending"] as const).flatMap((direction) =>
|
||||||
if (
|
orderedBy(items, field, direction)
|
||||||
items === undefined ||
|
? [{ rule: "sorted", field, direction } as const]
|
||||||
items.length < 2 ||
|
: [],
|
||||||
items.length !== held.size ||
|
),
|
||||||
!items.every((item) =>
|
|
||||||
held.has(scalarText(ownMember(item, idField)) ?? ""),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return [];
|
|
||||||
return [...written].flatMap((field) =>
|
|
||||||
(["ascending", "descending"] as const).flatMap((direction) =>
|
|
||||||
orderedBy(items, field, direction)
|
|
||||||
? [{ rule: "sorted", field, direction } as const]
|
|
||||||
: [],
|
|
||||||
),
|
),
|
||||||
);
|
) ?? [];
|
||||||
});
|
|
||||||
for (const rule of rules.filter(
|
for (const rule of rules.filter(
|
||||||
(candidate) => current === undefined || !same(candidate, current),
|
(candidate) => current === undefined || !same(candidate, current),
|
||||||
)) {
|
)) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
type TimedCaptureRow,
|
type TimedCaptureRow,
|
||||||
} from "../../../tests/fixtures/analysisView.js";
|
} from "../../../tests/fixtures/analysisView.js";
|
||||||
import {
|
import {
|
||||||
|
CREATION_ORDER_ROWS,
|
||||||
DERIVED_TEXT_ROWS,
|
DERIVED_TEXT_ROWS,
|
||||||
formLabelRows,
|
formLabelRows,
|
||||||
ID_OR_TITLE_ROWS,
|
ID_OR_TITLE_ROWS,
|
||||||
@@ -141,6 +142,16 @@ describe("collections kept sorted", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("learns no sort from a rename that an earlier read of the creates' order rules out", () => {
|
||||||
|
const byTitle = { rule: "sorted", field: "title", direction: "ascending" };
|
||||||
|
expect(planOf(CREATION_ORDER_ROWS, "post_folders").insert).not.toEqual(
|
||||||
|
byTitle,
|
||||||
|
);
|
||||||
|
expect(planOf(CREATION_ORDER_ROWS, "put_folders_by_id")).toMatchObject({
|
||||||
|
served: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("moves each written entity in turn, against the others' values before their own writes", () => {
|
it("moves each written entity in turn, against the others' values before their own writes", () => {
|
||||||
const entity = (id: string, name: string) => ({ id, fields: { name } });
|
const entity = (id: string, name: string) => ({ id, fields: { name } });
|
||||||
const [a, b, c, d] = ["a", "b", "c", "d"].map((name, index) =>
|
const [a, b, c, d] = ["a", "b", "c", "d"].map((name, index) =>
|
||||||
|
|||||||
@@ -529,6 +529,8 @@ export const writtenResponse = (
|
|||||||
readonly id: string;
|
readonly id: string;
|
||||||
/** Generated clocks keep the response's spelling independently of the stored entity's. */
|
/** Generated clocks keep the response's spelling independently of the stored entity's. */
|
||||||
readonly clocks?: readonly string[];
|
readonly clocks?: readonly string[];
|
||||||
|
/** Members the call sent, which the answer shows as stored. */
|
||||||
|
readonly sent?: readonly string[];
|
||||||
},
|
},
|
||||||
optional: readonly string[] = [],
|
optional: readonly string[] = [],
|
||||||
): Applied<JsonValue | undefined> => {
|
): Applied<JsonValue | undefined> => {
|
||||||
@@ -569,8 +571,12 @@ export const writtenResponse = (
|
|||||||
fields.map((field) => {
|
fields.map((field) => {
|
||||||
const value = ownMember(written.fields!, field)!;
|
const value = ownMember(written.fields!, field)!;
|
||||||
const answered = ownMember(shown, field);
|
const answered = ownMember(shown, field);
|
||||||
// An answer spells an empty list or map as its recording did, whichever spelling the state holds.
|
// An answer spells an empty list or map the call did not send as its recording did.
|
||||||
if (answered !== undefined && emptySpellings(answered, value))
|
if (
|
||||||
|
answered !== undefined &&
|
||||||
|
written.sent?.includes(field) !== true &&
|
||||||
|
emptySpellings(answered, value)
|
||||||
|
)
|
||||||
return [field, answered];
|
return [field, answered];
|
||||||
const clock =
|
const clock =
|
||||||
written.clocks?.includes(field) === true
|
written.clocks?.includes(field) === true
|
||||||
@@ -590,7 +596,11 @@ export const writtenResponse = (
|
|||||||
export const writtenBulkResponse = (
|
export const writtenBulkResponse = (
|
||||||
response: WriteResponse,
|
response: WriteResponse,
|
||||||
recorded: JsonValue | undefined,
|
recorded: JsonValue | undefined,
|
||||||
written: readonly { readonly fields: JsonObject; readonly id: string }[],
|
written: readonly {
|
||||||
|
readonly fields: JsonObject;
|
||||||
|
readonly id: string;
|
||||||
|
readonly sent?: readonly string[];
|
||||||
|
}[],
|
||||||
): Applied<JsonValue | undefined> => {
|
): Applied<JsonValue | undefined> => {
|
||||||
if (response.shape === "empty") return ok(undefined);
|
if (response.shape === "empty") return ok(undefined);
|
||||||
if (response.shape === "recorded") return ok(recorded);
|
if (response.shape === "recorded") return ok(recorded);
|
||||||
|
|||||||
+45
@@ -321,3 +321,48 @@ export const ID_OR_TITLE_ROWS: readonly TimedCaptureRow[] = (() => {
|
|||||||
["GET", "/api/folders", [renamed, alpha, bravo, charlie]],
|
["GET", "/api/folders", [renamed, alpha, bravo, charlie]],
|
||||||
];
|
];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two seeded folders and two creates kept in creation order, which a list read after both shows (`Charlie` after
|
||||||
|
* `Zulu`), then a rename whose read happens to follow title order.
|
||||||
|
*/
|
||||||
|
export const CREATION_ORDER_ROWS: readonly TimedCaptureRow[] = (() => {
|
||||||
|
const [alpha, bravo] = [folder(1, "Alpha"), folder(2, "Bravo")];
|
||||||
|
const [zulu, charlie] = [folder(3, "Zulu"), folder(4, "Charlie")];
|
||||||
|
const renamed = folder(3, "Aardvark", 2);
|
||||||
|
return [
|
||||||
|
["GET", "/api/folders", [alpha, bravo]],
|
||||||
|
["GET", "/api/folders/1", alpha],
|
||||||
|
["POST", "/api/folders", zulu, 200, { title: "Zulu" }],
|
||||||
|
["GET", "/api/folders/3", zulu],
|
||||||
|
["GET", "/api/folders", [alpha, bravo, zulu]],
|
||||||
|
["POST", "/api/folders", charlie, 200, { title: "Charlie" }],
|
||||||
|
["GET", "/api/folders/4", charlie],
|
||||||
|
["GET", "/api/folders", [alpha, bravo, zulu, charlie]],
|
||||||
|
["PUT", "/api/folders/3", renamed, 200, { title: "Aardvark", version: 1 }],
|
||||||
|
["GET", "/api/folders/3", renamed],
|
||||||
|
["GET", "/api/folders", [renamed, alpha, bravo, charlie]],
|
||||||
|
];
|
||||||
|
})();
|
||||||
|
|
||||||
|
/** Two label creates that send and echo empty tags, one `null` and one `[]`, as an application storing what it is sent does. */
|
||||||
|
export const ECHOED_EMPTY_ROWS: readonly TimedCaptureRow[] = (() => {
|
||||||
|
const tagged = (entity: Entity, tags: JsonValue): Entity => ({
|
||||||
|
...entity,
|
||||||
|
tags,
|
||||||
|
});
|
||||||
|
const [kitchen, tools] = [tagged(KITCHEN, null), tagged(TOOLS, null)];
|
||||||
|
const garden = tagged(label(3, "Garden"), null);
|
||||||
|
const attic = tagged(label(4, "Attic"), []);
|
||||||
|
return [
|
||||||
|
["GET", "/api/labels", [kitchen, tools]],
|
||||||
|
["GET", "/api/labels/1", kitchen],
|
||||||
|
["GET", "/api/labels/2", tools],
|
||||||
|
["POST", "/api/labels", garden, 201, formBody(garden)],
|
||||||
|
["GET", "/api/labels/3", garden],
|
||||||
|
["GET", "/api/labels", [garden, kitchen, tools]],
|
||||||
|
["POST", "/api/labels", attic, 201, formBody(attic)],
|
||||||
|
["GET", "/api/labels/4", attic],
|
||||||
|
["GET", "/api/labels", [attic, garden, kitchen, tools]],
|
||||||
|
];
|
||||||
|
})();
|
||||||
Reference in new issue
Block a user