Skip to content

Hook reads pass filter:, which ObjectQL drops silently — 17 call sites read the wrong record #574

Description

@yinlianghui

Hook and action reads across the app pass their predicate as filter:. The
ObjectQL kernel only honours where: on findOne and countfilter is
dropped without an error and without a log line. find happens to alias it,
which is why the spelling looked legitimate and spread by copy-paste.

Kernel-side report: objectstack-ai/objectstack#4419

What actually happens

  • findOne({ filter: { id } }) → the object's first row (the engine also
    applies limit: 1), not the requested record and not null.
  • count({ filter: {...} }) → counts the whole object.

Writes were never affected: every write targets an explicit id. The damage is
entirely on the read side — guards and lookups evaluated against an unrelated
row, and values computed from it.

Impact observed (17 call sites, 8 files)

  • Money is wrong. opportunity_line_item / quote_line_item fill
    list_price / unit_price from the first product in the catalogue instead of
    the selected one, and that value rolls up into deal amounts and quote totals.
    After fix(line-items): F/P expression tags, null guard, shared price-fill (#514 items 8, 3, 15) #570 extracted the shared _line-item-price-fill.ts, the same defect
    applies to both line-item objects from one place.
  • Delete guards on crm_product / crm_account count the entire object,
    blocking deletes with no real references and reporting a fabricated count.
  • Campaign ROI snapshots record whole-object opportunity/lead counts as
    per-campaign attribution.
  • Stage/status gates (quote → opportunity stage, case → account owner,
    contract / opportunity rollup-freeze guards) read an unrelated row; the
    subsequent write then correctly targets the intended id, so the guard
    protected the wrong record.

Why the test suite stayed green

Three layers each failed to catch it:

  1. src/objects/_hook-api.ts hand-rolls the query type with both
    filter? and where? optional, and its comment stated that drivers accept
    either — so TypeScript green-lit the wrong key.
  2. test/helpers/hook-harness.ts resolved its predicate as
    q.filter ?? q.where ?? {} — a stand-in more permissive than the real
    kernel
    turns the whole runtime-hook suite into a green light for broken
    code.
  3. Nothing logs anything at runtime; there is no string to grep for.

Fix

PR #573 — remove filter? from the hand-rolled type so the compiler surfaces
every site (that is what raised the count from the 4 originally suspected to
17), correct the call sites, make the harness throw on filter, and add a
real-kernel characterisation test plus a source scan over all of src/objects/
(not just *.hook.ts — the shared price-fill module is not a .hook.ts file
and an earlier, narrower scan missed exactly the most expensive site).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions