HarperFast / HarperFast/harper
tables.X.create(record) single-argument form silently escapes the caller's request transaction
- Dominant language
- JavaScript
- Stars
- 89
- Forks
- 10
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 200
Description
`tables.X.create(record)` — the single-argument form — silently runs outside the caller's request
transaction. A write followed by a throw therefore **persists**, while the same write issued as
`tables.X.create(record, this.getContext())` correctly rolls back. Verified in both directions in one
run.
## Mechanism (harper, `origin/main`)
`resources/Resource.ts`, static `create()`, the default (`loadAsInstance !== false`) "two argument
form, shift" branch:
```js
context = record || {}; // single-arg call: record is undefined -> context = {}
record = idPrefix;
```
`{}` is truthy, so the subsequent `if (context) { … } else { context = contextStorage.getStore() ?? {}; }`
takes the truthy branch and the ambient-context lookup is never reached.
`resources/transaction.ts`'s `transaction(context, cb)` then sees a context with no `.transaction`
property, so `isJoinableScope` is false and it allocates a fresh `DatabaseTransaction({ scopeOwned: true })`
that commits itself via `onComplete` / `doneWriting: true`, independent of the caller's
request-scoped transaction.
## Why it matters
It only manifests on the error path, which is exactly where atomicity is supposed to hold: the
caller's transaction rolls back, the escaped write stays committed. Nothing logs, and the
single-argument form reads like a supported convenience shape.
## Fix shape
In that shift branch, only fabricate `{}` when there is genuinely no ambient store — mirroring the
fallback the very next lines already contain:
```js
context = record ?? contextStorage.getStore() ?? {};
```
Confined to the `loadAsInstance !== false` default-Table path.
Contributor guide
Research direction
Start in resources/Resource.ts at static create(), specifically the default loadAsInstance !== false two-argument shift branch, and then read resources/transaction.ts to understand how context determines transaction ownership. Preserve the caller's ambient request transaction for the single-argument form while retaining the empty-context fallback; verify that a write followed by a throw rolls back rather than persisting.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100