HarperFast / HarperFast/harper
Docs/UX gaps from QA-explorer campaign (compression, audit+TTL, write-authz, bulk POST, 4xx-as-500, …)
- Dominant language
- JavaScript
- Stars
- 89
- Forks
- 10
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 200
Description
## Summary
Aggregated docs/UX gaps surfaced by the QA-explorer exploratory campaign (Harper `7aaa5a152`; all cited code byte-identical to `main` @`6797f091d` except one unrelated revalidation line). Each is defensible-but-surprising — a documentation or small-UX clarification rather than a correctness bug. Grouped to reduce issue noise; split out any that warrant their own tracking. Companion notes tied to specific filed defects are referenced there and omitted here.
## Items
1. **Compression is brotli-only; `gzip` Accept-Encoding silently ignored; default threshold disables it.** Response compression honors only `Accept-Encoding: br` (`contentTypes.ts`). A `gzip`-only client (incl. Node's built-in http client) gets uncompressed bodies with no signal. Also `http.compressionThreshold` defaults to **0**, which *disables* compression entirely (easy to misread as "compress everything"). A `Content-Encoding: gzip` request body → 500 (no request decompression; should be 415). *Suggest: doc brotli-only + the threshold default; consider gzip support; map the request-body case to 4xx.*
2. **TTL expiry is invisible in the audit log and erases the record's history.** On an `audit:true` + TTL table, expiry produces no audit entry (eviction is `removeEntry`, not a tombstone — `Table.ts:1606`, deliberate) and prunes prior-version history; time-travel to a pre-expiry timestamp returns 404. So "expired" is indistinguishable from "never existed," and pre-expiry forensics are impossible. *Suggest: doc that TTL eviction is invisible to the audit log; audited tables needing expiry-forensics should model deletes explicitly.*
3. **Resource-layer row-write guards are a complete boundary only if the principal's role lacks direct table-write grants.** `allowUpdate`/`allowDelete`/`allowCreate` are enforced consistently across REST write surfaces (correct 403s), but SQL UPDATE/DELETE go through the operations-API admin path (by design) gated only by role table-permissions — they do not consult resource-layer overrides. *Suggest: doc that row guards + role table-permissions must be configured together.*
4. **REST collection POST has no array/bulk write path.** `POST /T/` with a JSON **array** body → 500 `Can not create a record without an object` (`Table.ts:1711` `Array.isArray` guard) instead of a clear 4xx or bulk support, while ops `insert` and SQL multi-row both accept arrays. *Suggest: return a clear 400 ("use the operations API `insert` for bulk") or add a REST bulk path; don't 500.*
5. **A degenerate single-child `or`/`and` condition group returns 500 instead of 400.** `search_by_conditions` with `{operator:'or', conditions:[]}` → 500 `An "or" operator requires at least two conditions` (`Table.ts:2348`); the Joi validator enforces no min-length, so it passes validation then throws a plain `Error` in the engine. *Suggest: enforce min-2 at validation (400).* (The boolean condition engine itself is correct — deep nested/negation/contradiction/fuzz trees all match the oracle.)
6. **An empty-string range lower bound is coerced to a null-search and rejected.** `attr >= ''` (used as a "from the start" sentinel) → 400 `"" is not indexed for nulls` (`search.js:309`). Loud, not silent-wrong, but `''`-as-null for range bounds is non-obvious. *Suggest: doc, or treat `''` as a lexical minimum.*
7. **ops-API `insert` silently drops an inline `@relationship` array** (REST POST loudly rejects it with 400). A client creating a parent + inline children via ops-insert gets the parent created, children silently dropped, success response. *Suggest: reject/warn consistently, or doc that relationship edges are written as separate child rows.*
8. **Slash-form blob sub-attribute GET (`/T//payload`) returns a quiet 404**; only the dot-form (`/T/.payload`) and `?select(payload)` serve the blob. *Suggest: serve the blob on the slash path too, or doc the dot/select forms.*
9. **Selecting a removed `@relationship` field returns 500, not 400.** After a `@relationship` is dropped, a query still selecting it as a nested sub-object → 500 `Cannot read properties of undefined (reading 'propertyResolvers')` (`Resource.ts:831`/`Table.ts:1221`). Read-only, non-corrupting. *Suggest: 400 unknown-field / ignore.* (Part of the systemic "4xx-as-500" pattern, cf. #1299.)
---
*Surfaced by the QA-explorer campaign for @kris. Filed by Claude (Opus 4.8). Several items share the systemic "client errors surface as HTTP 500" theme already noted on #1299 — a single error-status-mapping pass would close many of them.*
Contributor guide
Research direction
Begin by reviewing the named entry points: contentTypes.ts, Table.ts, search.js, and Resource.ts, then compare the REST, operations API, and SQL behaviors described in each item. Use #1299 as context for the 4xx-as-500 pattern; done should mean each selected gap has an explicit documentation or UX outcome, with unrelated items split into focused issues if needed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js, sql
- Domain
- api, backend, database, documentation, security
- Issue type
- Documentation
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100