Skip to content

fix(list): leave out getters the route's pluck mode does not list - #117

Merged
Upd4ting merged 3 commits into
mainfrom
fix/ant-1400
Nov 9, 2026
Merged

Upd4ting merged 3 commits into
mainfrom
fix/ant-1400

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Nov 8, 2026

Copy link
Copy Markdown
Member

Why

A list route with a non-default pluckMode should leave out a getter that its mode does not list. Today it evaluates every readable getter, so a module author who declares @Listable(["name", "email"]) get fullName() for the default mode gets fullName: "Alice <undefined>" on a detailed route that never plucked email.

Fixes #92

What changes

  • listRows (src/index.ts) passes the route's pluck mode to ReadProperties. It passes no mode when the route sets noPluck, which reads every field.
  • ReadProperties (src/internal/read-properties.ts) skips a getter when its listable has no own entry for that mode. An entry with no fields, as @Listable([]) writes, still lists the getter.
  • The public Query.ReadProperties(obj, meta, dbData, action, onlyList) and the get route behave as before.
  • The verify-interface skill now drives issue-92-getter-mode as a passing journey. Its docs no longer say it fails on purpose.
flowchart LR
  R["GET /people/brief<br/>pluckMode: brief"] --> P["pluck fields of 'brief'"]
  P --> RP["ReadProperties(listMode: 'brief')"]
  RP --> G{"getter listable<br/>has 'brief'?"}
  G -- yes --> E["evaluate getter"]
  G -- no --> S["leave it out"]
  N["noPluck route"] --> A["ReadProperties(no listMode)<br/>evaluates every getter"]
Loading

Proof

src/tests/index/list_getter_modes.test.ts (commit af72592). The controller lists fullName and kind (@Listable([])) in the default mode and initial in detailed, with a third route that sets noPluck.

Before the fix (af72592):

  List getters per pluck mode
    1) evaluates a getter in the mode that lists it
    2) leaves out a getter the route's mode does not list
    ✔ evaluates every getter on a route that does not pluck


  1 passing (61ms)
  2 failing

  1) List getters per pluck mode
       evaluates a getter in the mode that lists it:

      AssertionError: expected [ { name: 'Alice', …(4) } ] to deeply equal [ { name: 'Alice', …(3) } ]
      + expected - actual

       [
         {
           "email": "alice@example.com"
           "fullName": "Alice <alice@example.com>"
      -    "initial": "A"
           "kind": "person"
           "name": "Alice"
         }
       ]
      
      at Context.<anonymous> (dist/tests/index/list_getter_modes.test.js:116:42)
      at process.processTicksAndRejections (node:internal/process/task_queues:104:5)

  2) List getters per pluck mode
       leaves out a getter the route's mode does not list:

      AssertionError: expected [ { name: 'Alice', …(3) } ] to deeply equal [ { name: 'Alice', initial: 'A' } ]
      + expected - actual

       [
         {
      -    "fullName": "Alice <undefined>"
           "initial": "A"
      -    "kind": "person"
           "name": "Alice"
         }
       ]
      
      at Context.<anonymous> (dist/tests/index/list_getter_modes.test.js:128:42)
      at process.processTicksAndRejections (node:internal/process/task_queues:104:5)



After the fix (caeb4c6):

  List getters per pluck mode
    ✔ evaluates a getter in the mode that lists it
    ✔ leaves out a getter the route's mode does not list
    ✔ evaluates every getter on a route that does not pluck


  3 passing (100ms)

pnpm run checks at 568eb14: Checks: 8 passed, 0 failed.

The verify-interface skill, run on the packed interface at caeb4c6:

Attention

  • A getter with no @Listable is now left out of list routes, as docs/6.listable.md says for any field without the decorator. Before this change, list rows carried it.
  • @Listable(false, mode) on a getter still evaluates it in that mode, as before. The metadata stores false and [] the same way, so telling them apart needs a change to setListable. That is out of scope here.
  • Pre-existing and unchanged: a getter redeclared in a child controller built with Controller(path, Base) also changes the base controller's routes.

A list route evaluates every readable getter, whatever its pluck mode,
so a getter only the default mode lists answers with the fields that
mode plucked missing.
@Upd4ting
Upd4ting merged commit bbd2114 into main Nov 9, 2026
8 checks passed
@Upd4ting
Upd4ting deleted the fix/ant-1400 branch November 9, 2026 14:21
Upd4ting added a commit that referenced this pull request Nov 9, 2026
## Why

A route registered as `DefaultRoutes.WithOptions(DefaultRoutes.Edit, {
index: "slug" })` should edit the row the index finds. It found that
row, then wrote the update to the row whose primary key equals `id`.
Usually no such row exists, so the edit was silently lost and the route
still answered 200 with the old row. Fixes #90.

## What changes

- `editRow` in `src/index.ts` takes the primary key (`_id`) of the row
it found, asserts it is a string, and uses it for the update and for the
read-back. Reading back by primary key also gives the right answer when
the edit changes the indexed field itself. The old read through the
index returned the unchanged row in that case.
- Without `index` the behaviour is the same as before, because the found
row's `_id` is the requested `id`.
- `docs/3.routes.md` describes the fixed behaviour in the Edit options
table.
- The verify journey `issue-90-edit-index` is renamed from "fails on
purpose" to a guard, and `features/README.md` no longer lists it as
failing.

```mermaid
flowchart LR
  R["PUT /edit?id=hello (index: slug)"] --> G["Query.Get(table, id, index)"]
  G -->|stored row, _id a1| W["WriteProperties + Lock"]
  W --> U_old["before: table.get('hello').update"]
  W --> U_new["after: table.get(stored._id).update"]
  U_new --> Rd["readRow by stored._id"]
```

## Proof

`src/tests/index/edit_index.test.ts` drives the route against the
in-memory MongoDB. One row's `_id` equals the indexed value, so an
implementation that writes to the primary-key row fails the test.

Before, with `main`'s `src/index.ts`:

```text
  Edit with the index option
[2026-11-08 03:46:25] [ERROR] HTTP 500 PUT /edit-index/edit?id=hello: After applying the update, the (immutable) field '_id' was found to have been altered to _id: "a1"
    1) updates the row the index finds, not the row whose primary key is the id
    2) answers with the edited row when the edit changes the indexed field


  0 passing (93ms)
  2 failing

  1) Edit with the index option
       updates the row the index finds, not the row whose primary key is the id:

      AssertionError: expected 500 to equal 200
      + expected - actual

      -500
      +200
      

  2) Edit with the index option
       answers with the edited row when the edit changes the indexed field:

      AssertionError: expected { slug: 'rename-me', title: 'Before' } to have deep property 'slug' of 'renamed', but got 'rename-me'
      + expected - actual

      -rename-me
      +renamed
      



```

After:

```text
  Edit with the index option
    ✔ updates the row the index finds, not the row whose primary key is the id (53ms)
    ✔ answers with the edited row when the edit changes the indexed field


  2 passing (152ms)

```

`pnpm run checks` prints `Checks: 8 passed, 0 failed.`

`antelopejs-verify` at `0d0e4a1`, before the rebases, every feature
`plan` lists: `route-options/issue-90-edit-index` passes. The only
failures were `issue-92-getter-mode` and `issue-93-multi-foreign`, which
#117 and #116 have since fixed on `main`. With #90 fixed too,
`features/README.md` no longer lists any journey that fails on purpose.

After the rebase onto `f4872d3`, `pnpm run checks` prints `Checks: 9
passed, 0 failed.` and both new tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(list): a getter declared listable in the default mode is evaluated in other list modes

1 participant