HarperFast / HarperFast/harper

tables.X.create(record) single-argument form silently escapes the caller's request transaction

Open Beginner friendly
#2,453 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.