From 7e83d635665bfd43228869a74cc9331622be4077 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sun, 8 Nov 2026 03:42:54 +0000 Subject: [PATCH 1/2] fix(routes): update the row the Edit index option finds Edit with the index option wrote to the row whose primary key equals id. It now writes to and reads back the stored row's primary key. Fixes #90 --- docs/3.routes.md | 8 +-- src/index.ts | 10 +-- src/tests/index/edit_index.test.ts | 97 ++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 src/tests/index/edit_index.test.ts diff --git a/docs/3.routes.md b/docs/3.routes.md index 3709d91..b6bbf9c 100644 --- a/docs/3.routes.md +++ b/docs/3.routes.md @@ -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` diff --git a/src/index.ts b/src/index.ts index 611d146..7bcbfcf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { @@ -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, @@ -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( diff --git a/src/tests/index/edit_index.test.ts b/src/tests/index/edit_index.test.ts new file mode 100644 index 0000000..c314558 --- /dev/null +++ b/src/tests/index/edit_index.test.ts @@ -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", + }); + }); +}); From 0bbcc5207fa533ce5bff677519602ac6ddb291a1 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sun, 8 Nov 2026 03:42:57 +0000 Subject: [PATCH 2/2] test(verify): mark the issue-90 journey as a guard now that #90 is fixed --- .agents/skills/verify-interface/features/README.md | 4 ---- .agents/skills/verify-interface/features/route-options.md | 2 +- .agents/skills/verify-interface/journeys-probe-1.ts | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.agents/skills/verify-interface/features/README.md b/.agents/skills/verify-interface/features/README.md index 20ba0ce..8856eb5 100644 --- a/.agents/skills/verify-interface/features/README.md +++ b/.agents/skills/verify-interface/features/README.md @@ -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. diff --git a/.agents/skills/verify-interface/features/route-options.md b/.agents/skills/verify-interface/features/route-options.md index 625f15a..1fb6a4c 100644 --- a/.agents/skills/verify-interface/features/route-options.md +++ b/.agents/skills/verify-interface/features/route-options.md @@ -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. diff --git a/.agents/skills/verify-interface/journeys-probe-1.ts b/.agents/skills/verify-interface/journeys-probe-1.ts index c086916..a2786a2 100644 --- a/.agents/skills/verify-interface/journeys-probe-1.ts +++ b/.agents/skills/verify-interface/journeys-probe-1.ts @@ -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"}' });