HarperFast / HarperFast/harper
Resource get()-derived attribute silently persists as a stored column on round-trip GET→PUT
- Dominant language
- JavaScript
- Stars
- 89
- Forks
- 10
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 200
Description
## Summary
A derived attribute added **only in a custom Resource's `get()` override** (not declared in the schema) is included in the GET response, and on the common **fetch → mutate → save** round-trip it gets written straight into storage as a real stored column — silently polluting the persisted record.
By contrast, a **schema `@computed(from:)` field is guarded** — a PUT carrying it is rejected (`400 "Computed property may not be directly assigned"`, `resources/Table.ts:3496`) and not persisted. The gap is specifically the resource-layer `get()`-added field, which has no such guard.
## Severity
Medium — silent data pollution on a very common client idiom (read an object, change one field, PUT it back) for any app that derives a view field in a resource `get()`. No crash; the record gains an undeclared stored attribute that then also shadows the derived value on subsequent reads.
## Repro
1. Define a custom Resource over a table whose `get()` adds a non-schema field, e.g. `record.fullName = record.first + ' ' + record.last`.
2. `GET /Person/p1` → response includes `fullName`.
3. Take that response object, change `first`, and `PUT /Person/p1` with the whole object back.
4. `GET /Person/p1` (or inspect storage) → `fullName` is now a **stored** column on the record (it was persisted), not re-derived.
Contrast control: a schema `@computed` field in the same PUT body is rejected with 400 and not stored.
## Recommendation
Guard resource-`get()`-derived fields on write the same way schema `@computed` is — reject (or drop) them on PUT — or document that resource-layer derived fields must be stripped before a write-back.
## Companion observations (computed-field surface, lower priority — D-062)
- REST GET-by-id **omits** computed fields (returns stored columns only, `Table.ts:2566`) while SQL `SELECT *` **includes** them — same record, different surfaces.
- REST `sort(computedField)` alone → 404 "not indexed"; `sort(computed)` + a filter → silently returns `[]` (SQL `ORDER BY computed` works). Filtering *by* a computed field works correctly on all surfaces.
---
*Surfaced by the QA-explorer campaign against Harper `7aaa5a152`. The resource get/merge code path is not changed vs `main` @`6797f091d` (the only feature-branch delta is an unrelated revalidation line). Ready-to-promote test exists. Filed by Claude (Opus 4.8) for @kris.*
Contributor guide
Research direction
Start with the resource get/merge write path and compare it with the computed-field guard at resources/Table.ts:3496; the issue also identifies REST GET handling at Table.ts:2566. Run the ready-to-promote test mentioned in the report, then verify that a GET→PUT round-trip does not persist the resource-derived field and that the existing computed-field behavior remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- api, backend, database
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100