Harper v5 Upgrade with Various v5 Relevant Changes#2
Conversation
- Convert GetAll.get() to static method (Harper v5 Resource API) - Fix config.yaml: routes/index.js → routes/** (Harper bug workaround: literal file paths in jsResource.files cause the parent directory to be ignored by chokidar, so subdirectory files are never discovered) - topic-crud.test.ts: handle v5 POST returning id via Location header instead of response body; merge GetAll tests into single suite to avoid concurrent Harper instances; fix auth test to use invalid credentials (authorizeLocal=true permits unauthenticated local requests) - sse.test.ts: warm up table before subscribing (v5 SSE only initialises after first write); use Location header for created record id Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Apply mandatory harperBinPath fix to both existing test suites: the harper package only exports "." so the harness default resolution of harper/dist/bin/harper.js throws ERR_PACKAGE_PATH_NOT_EXPORTED. Resolve the CLI from the package root and pass it as harperBinPath. - Add MQTT real-time transport tests (native TCP 1883 + MQTT-over-WebSocket on the HTTP port) so the real-time template covers MQTT/WS in addition to the existing SSE and REST CRUD suites. - Add .github/workflows/integration-tests.yml (Node 22/24/26, actions pinned to commit hashes). - Add test:integration script alongside the existing test script. - Add typescript devDependency (Phase 7) for type-checking. - Branding: package.json description HarperDB -> Harper. - Stop tracking .DS_Store and add it to .gitignore. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`npm ci` failed on Linux (Node 22/24/26) with "Missing: bufferutil / utf-8-validate / node-gyp-build from lock file". macOS incremental installs omit these optional native deps of harper/ws; a from-scratch lockfile regeneration includes them so `npm ci` is cross-platform. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The closing delimiter was `**/` (valid JS) but the opener `/**` treated all content as a JSDoc comment, so `import`/`export class Topic` were never compiled. Changed `/**` → `//` for the description line. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
Review follow-up (autonomous agent): Fixed the blocking finding — |
Without returning the parent subscription iterator, Harper treats the response as empty (204), breaking all SSE streams and MQTT subscriptions. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Chris Barber <chris@cb1inc.com>
kriszyp
left a comment
There was a problem hiding this comment.
Nice work on the v4→v5 migration — the Topic.subscribe override is exactly the idiomatic shape (extend the exported table, override the static, delegate to super). One teaching-idiom note, non-blocking:
The new GetAll Resource re-wraps what the exported table already gives you.
export class GetAll extends Resource {
static get(target) {
return tables.Topic.search({});
}
}Topic is already @table @export, so GET /Topic/ already lists all records via the table's own static read path — GetAll is a hand-rolled tables.Topic.search({}) passthrough re-exposing the default collection behavior at a second URL. In a template this teaches users to reach for a custom Resource before reaching for the table the framework already exported. Any one of these resolves it:
- Drop
GetAlland let the template showGET /Topic/listing records straight off the@exported table (the integration tests already hit/Topicdirectly, so nothing depends on/GetAll) — the most idiomatic v5 demonstration. - Or, if you want a custom-endpoint example, make it demonstrate the static methods with a reason to override — a filtered/derived query (
tables.Topic.search({ conditions: [...] })ortables.Topic.get(id)) — likeTopic.subscribedoes. A baresearch({})passthrough teaches the opposite. - Minor:
static get(target)ignorestarget; if it stays, it should use the request target rather than always returning everything, to model howgetdispatches in v5.
Everything else reads clean. Thanks for pushing the v5 patterns through the template!
— Claude (Opus 4.8)
Co-authored-by: Chris Barber <chris@cb1inc.com>
kriszyp
left a comment
There was a problem hiding this comment.
Good progress on the v5 migration — the fastifyRoutes→jsResource pivot, the static method resource classes, and the harperBinPath fix are all correct. Two blockers need resolving before this merges.
Blockers
1. CI failing on Node 24 and Node 26 — lockfile out of sync
npm ci fails with EUSAGE: Missing bufferutil@4.1.0 and utf-8-validate@5.0.10 from lock file. Looks like the lockfile was generated on Node 22 (npm 11 is more lenient). Fix: run npm install on Node 24+ and recommit the updated package-lock.json.
2. subscribe(options) crashes when called without arguments
static async subscribe(options) {
if (!options.startTime) // ← TypeError if options is undefinedA subscriber connecting without specifying startTime will hit options.startTime on undefined and crash the socket handler. Fix:
static subscribe(options = {}) {
if (!options.startTime)
options.previousCount = 5;
return super.subscribe(options);
}(Also removed async since no await is used — the current version incurs a state-machine allocation on every subscription handshake.)
Significant
XSS pattern in web/index.html — insertAdjacentHTML is used to inject stringified event data. The immediate threat is limited since type is application-controlled, but this is a template — developers who adapt this by dropping a field into the template string get textbook XSS. Recommend switching to textContent assignments:
const li = document.createElement('li');
const strong = document.createElement('strong');
strong.textContent = `${type}:`;
const code = document.createElement('code');
code.textContent = JSON.stringify(parsedData, null, 2);
li.append(strong, document.createElement('br'), code, document.createElement('hr'));
eventsList.appendChild(li);Also: JSON.parse(event.data) on the same SSE handler has no try/catch — a heartbeat or malformed frame will throw SyntaxError and break the listener.
GetAll unbounded scan (flagged in the prior review, still open) — tables.Topic.search({}) with no limit materializes the full Topic table. For a pub/sub log with high write volume this is a memory hazard. A limit: 100 default, or at minimum a comment calling out that pagination is required before production use, would prevent this from propagating into customer apps.
GHA node-version input interpolated directly into shell — ${{ github.event.inputs.node-version }} in the run: block is injectable via the workflow_dispatch API (type: choice only constrains the UI). Bind to an env var and reference $NODE_VERSION in shell.
Questions
- Was there a reason to keep the unbounded
GetAllfrom the prior review? If the intent is "keep it simple for the demo," a brief comment in the code would save future reviewers from re-raising it. - The Node 22 integration test passing is a good sign. Is MQTT coverage verified beyond CI (local or otherwise)?
Reviewed by KrAIs (claude-sonnet-4-6) integrating Gemini/agy findings.
Harper v5 Upgrade — real-time-template (Path B: verify/complete)
This branch was already on
harper@^5with migration changes and two test suites in place. This work completes and verifies it.Dependency
harper@^5.0.11(already on v5; noharperdbdependency or imports remain).typescriptdevDependency (Phase 7) for type-checking;@harperfast/integration-testing@^0.3.1already present.Migration items
harperdb→harper): already applied —routes/index.jsimports fromharper;resources.jsexample is commented out and already referencesharper. No remaining v4 imports/globals.routes/index.jsusesstatic get(target)— v5-correct.Table.get()return shape /RequestTarget: app code does not callTable.get()directly (usestables.Topic.search({})).getContext().transaction.commit(): no polling-on-transaction loops.allowedSpawnCommands): nospawn/exec/execFilein app code.blob.save()→saveBeforeCommit: no blob usage.allowedDirectory: template has no install scripts and no cross-directory module loading; defaults are appropriate.Tests (real-time coverage: REST + SSE + MQTT/WS)
harperBinPathfix to both existing suites (integrationTests/topic-crud.test.ts,integrationTests/sse.test.ts). Theharperpackage'sexportsonly exposes".", so the harness's default resolution ofharper/dist/bin/harper.jsthrowsERR_PACKAGE_PATH_NOT_EXPORTED. The CLI is now resolved from the package root and passed explicitly. (Confirmed locally that the deep subpath fails and the root-resolved path exists.)integrationTests/topic-crud.test.ts— REST CRUD on/Topic(POST/GET/PUT/DELETE/list), the/GetAllcustom route, 404 and 401 auth paths.integrationTests/sse.test.ts— SSEtext/event-streamsubscription; put and delete events delivered in real time.integrationTests/mqtt.test.ts(new) — real-time MQTT transport: a subscriber over native MQTT (1883) and a subscriber over MQTT-over-WebSocket (HTTP port/mqtt) each receive the message published when aTopic/<id>record is written via REST. This covers the MQTT/WS transports the template demonstrates inmqtt_client.js.test:integrationscript (alongside the existingtestscript) so CI and the standard command line up.Test status:
EADDRNOTAVAILon 127.0.0.2+). This is environmental — the dev machine has no loopback aliases and the alias setup needs interactivesudo. It is not a code or test bug; the harness gets past CLI resolution to the bind stage, confirming theharperBinPathwiring is correct.ubuntu-latest, which supports the full127.0.0.0/8range with no aliasing. The pushed workflow run is the authoritative test gate.CI
.github/workflows/integration-tests.ymlat the repo root. Node matrix[22, 24, 26]; actions pinned to commit hashes (actions/checkoutv6.0.3,actions/setup-nodev6.4.0,actions/upload-artifactv7.0.1); runsnpm run test:integrationand uploads Harper logs on failure.Branding
package.jsondescription: "A template for building HarperDB applications" → "A template for building Harper real-time applications". README prose was already Harper-branded.Housekeeping
.DS_Storeand added it to.gitignore.npm scope
harper(correct current package name); no scoped-package move applies here.Known issues / blockers
tsc --noEmitreports a type-level mismatch on thesuite((ctx: ContextWithHarper) => ...)signature under strictnode:testtypes — this is pre-existing to the branch's original test files and does not affect execution: the harness runs tests vianode:test'srun()with native type-stripping (notsctypecheck), so the suites execute correctly.🤖 Generated with Claude Code