Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .agents/skills/verify-interface/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ One file per feature. `covers:` lists every source file that computes something
| [metadata](metadata.md) | A module author reads metadata and declares controllers. | effective-modes, declaration-errors, exports-runtime, consumer-compiles, derived-own-model, foreign-target-meta |
| [smoke](smoke.md) | The fixed journey every verdict runs. | every-function, smoke-roundtrip |

## Known issues, driven on purpose

One journey fails on head and cites its issue: `issue-90-edit-index` ([#90](https://github.com/AntelopeJS/interface-data-api/issues/90)). A verdict that runs its feature is FAIL until the issue is fixed; the fix turns the journey green.

## Not driven

Every documented behaviour of the README, `docs/` and the TSDoc has a journey above, or a row here with the reason.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/verify-interface/features/route-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ A route is customized with `DefaultRoutes.WithOptions`; its options win over the

- `option-overrides`: `maxPage` caps the `limit` of a query and is the page size without one, `noMandatory` as a route option lets a mandatory field be cleared while the plain route refuses it, any non-empty string (`"false"` included) turns the option on, `index` reads through a secondary index, `noPluck` still answers only readable fields, and a `pluckMode` no field declares answers `No fields found for pluckMode 'nothing'`.
- `list-unknown`: a list route with `noPluck` answers 200 for a `pluckMode` no field declares, and a URL no route matches answers 404.
- `issue-90-edit-index`: fails on purpose until [#90](https://github.com/AntelopeJS/interface-data-api/issues/90) is fixed: an `edit` with the `index` option updates the row the index found.
- `issue-90-edit-index`: guards [#90](https://github.com/AntelopeJS/interface-data-api/issues/90): an `edit` with the `index` option updates the row the index found.
- `no-mandatory-strings`: `noMandatory` skips the mandatory check for any non-empty string, `"false"` included, on `edit` and on `new`, and not for `""` or when absent.
- `no-pluck-strings`: `noPluck` skips the pluck-mode check only when true.
- `no-model-500`: a controller whose model reference holds no model answers 500.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/verify-interface/journeys-probe-1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export const readOverrides = shopJourney("a per-action read override hides or gr
context.expect(row.folder, [equal("a write-only field is written on new", pick(row.rows[0] ?? null, "nickname"), "nn")]);
});

export const issue90 = shopJourney("fails on purpose until #90 is fixed: edit with the index option updates the row the index found", async (shop) => {
export const issue90 = shopJourney("#90: edit with the index option updates the row the index found", async (shop) => {
const { context } = shop;
await seed(shop, PROBES);
const edited = await shop.request("edit p1 by its tag", "PUT", "/probes/slugEdit?id=A", { body: '{"title":"New"}' });
Expand Down
8 changes: 4 additions & 4 deletions docs/3.routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,10 @@ The New route takes this option.

The Edit route takes these options.

| Option | Type | Description |
| ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `index` | `string` | Find the record and read the response through a secondary index. The update itself targets the record whose primary key equals `id`. |
| `noMandatory` | `string` | Skip the mandatory field check |
| Option | Type | Description |
| ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| `index` | `string` | Find the record through a secondary index instead of the primary key. The update and the response target the record found. |
| `noMandatory` | `string` | Skip the mandatory field check |

## Amending the body of `new` and `edit`

Expand Down
10 changes: 5 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import type { WriteDocument } from "@antelopejs/interface-database/common";

import { Parameters, Query, Validation } from "./components";
import { isWriteDocument, isWriteValue } from "./internal/guards";
import { isString, isWriteDocument, isWriteValue } from "./internal/guards";
import { prototypeOf } from "./internal/prototypes";
import { ReadProperties as readProperties } from "./internal/read-properties";
import {
Expand Down Expand Up @@ -308,6 +308,8 @@ export namespace DefaultRoutes {

const stored = await Query.Get(model.table, params.id, params.index);
assert(stored, 404, "Not Found");
const { _id: primaryKey } = stored;
assert(isString(primaryKey), 500, "Stored row has no primary key");

const dbData = await Query.WriteProperties(
this,
Expand All @@ -319,11 +321,9 @@ export namespace DefaultRoutes {
Validation.Lock(this, meta, dbData);

triggerEvent(dbData, "update");
await model.table.get(params.id).update(writtenRow(dbData));
await model.table.get(primaryKey).update(writtenRow(dbData));

return readRow(this, meta, {
params: { id: params.id, index: params.index },
});
return readRow(this, meta, { params: { id: primaryKey } });
}

async function deleteRows(
Expand Down
97 changes: 97 additions & 0 deletions src/tests/index/edit_index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { expect } from "chai";
import { Controller } from "@antelopejs/interface-api";
import { Access, AccessMode, ModelReference } from "../../metadata";
import {
DataController,
DefaultRoutes,
RegisterDataController,
} from "../../index";
import {
BasicDataModel,
Field,
Index,
Model,
RegisterSchema,
RegisterTable,
Table,
} from "@antelopejs/interface-database-decorators";

import { editRequest, getSchemaInstance } from "../utils";

const TABLE = "edit-index-articles";
const SCHEMA = "default";
const LOCATION = "edit-index";

@RegisterTable(TABLE, SCHEMA)
class Article extends Table {
declare _id: string;

@Index()
@Field("string")
declare slug: string;

@Field("string")
declare title: string;
}
class ArticleModel extends BasicDataModel(Article, TABLE) {}

@RegisterDataController()
class _ArticleAPI extends DataController(
Article,
{ edit: DefaultRoutes.WithOptions(DefaultRoutes.Edit, { index: "slug" }) },
Controller(`/${LOCATION}`),
) {
@ModelReference()
@Model(ArticleModel)
declare model: ArticleModel;

@Access(AccessMode.ReadWrite)
declare slug: string;

@Access(AccessMode.ReadWrite)
declare title: string;
}

function articles() {
return getSchemaInstance(SCHEMA).table(TABLE);
}

describe("Edit with the index option", () => {
before(async () => {
await RegisterSchema(SCHEMA);
await articles().insert([
{ _id: "a1", slug: "hello", title: "Old" },
{ _id: "hello", slug: "decoy", title: "Decoy" },
{ _id: "a2", slug: "rename-me", title: "Before" },
]);
});

it("updates the row the index finds, not the row whose primary key is the id", async () => {
const response = await editRequest(
LOCATION,
{ title: "New" },
{ id: "hello" },
);
expect(response.status).to.equal(200);
expect(await response.json()).to.deep.include({ title: "New" });
expect(await articles().get("a1")).to.deep.include({ title: "New" });
expect(await articles().get("hello")).to.deep.include({ title: "Decoy" });
});

it("answers with the edited row when the edit changes the indexed field", async () => {
const response = await editRequest(
LOCATION,
{ slug: "renamed", title: "After" },
{ id: "rename-me" },
);
expect(response.status).to.equal(200);
expect(await response.json()).to.deep.include({
slug: "renamed",
title: "After",
});
expect(await articles().get("a2")).to.deep.include({
slug: "renamed",
title: "After",
});
});
});
Loading