Skip to content

/query optimization - #312

Merged
cubap merged 6 commits into
mainfrom
302-query-optimization
Sep 24, 2026
Merged

cubap merged 6 commits into
mainfrom
302-query-optimization

Conversation

@thehabes

@thehabes thehabes commented Sep 17, 2026 •

Copy link
Copy Markdown
Member

Summary

Resolves #302. POST /query responses carry an RFC 8288 Link: <…>; rel="next" while another page exists, and omit it on the final page, so clients stop guessing from record counts.

Resolves #304. HEAD /query is removed. It now answers 405 through the route's existing .all() fallback, like every other POST route.

Resolves #96. The /query HEAD artifact is gone. HEAD on /since/:_id and /history/:_id is unchanged, because those are GET routes where Express answers HEAD from the GET handler.

What changed

rel="next" on /query

/query over-fetches one record (limit + 1) to learn whether another page exists. The extra record is trimmed before serialization and never served.

Link: <http://store.rerum.io/v1/context.json>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
Link: <https://store.rerum.io/v1/api/query?limit=3&skip=9>; rel="next"

That is the response to ?limit=3&skip=6. The rules:

  • Appended, not replaced. The new setNextPageLink(req, res, { limit, skip, hasNext }) helper in controllers/utils.js calls res.append, so the JSON-LD context link survives.
  • Absolute on the RERUM_PREFIX origin, never the request host. Without a usable RERUM_PREFIX the link is path-only. The path is the route that was paged, so the legacy getByProperties.action alias links to /v1/api/query.
  • Only paging parameters are carried: the applied limit (after clamping) and skip + limit.
  • Only next. No first, prev, or last.
  • Still sent past the skip maximum. Following it is answered with the limit and skip silently guess at invalid input, and an over-maximum skip returns the same page forever #301 400 that names the maximum, so a walk too deep to finish ends loudly instead of looking complete.

HEAD /query removed

  • routes/query.js: .head(controller.queryHeadRequest) is dropped.
  • controllers/history.js and db-controller.js: queryHeadRequest, its export, and the getPagination import only it used are deleted.
  • routes/__tests__/route_wrappers.test.js: every route in the 405 table without a GET handler is asserted to have no HEAD handler of its own, so adding one to a route that has no GET fails a test.

Contract and documentation

  • openapi/contracts/core-provider.openapi.yaml: a NextPageLink header component is referenced as Link on the /api/query 200, and the head: operation under /api/query is removed along with its entry in __tests__/core_provider_contract.test.js.
  • public/API.html: the Custom Query text points paged clients at rel="next". pagedQuery is rewritten to follow rel="next" until it is gone, re-sending the same body, and to warn when a next past the skip maximum is answered with the 400. A new "Following rel="next"" section documents the link. The result-ordering text no longer mentions HEAD.
  • public/API.html and the getPagination JSDoc no longer claim the pagination headers tell a truncated page from a final one. rel="next" does that now.

Tests

  • routes/__tests__/query.test.js: next is present and advances skip while more records exist, absent on a final page of exactly limit records, and coexists with the context link. The over-fetched record is never served at the maximum limit. next is still sent at the skip maximum and following it is a 400. A walk that follows only next returns every record exactly once. The existing test that checks what reaches the database cursor now expects the over-fetched limit + 1. The HEAD /query tests are removed.
  • __tests__/utils.test.js: setNextPageLink builds an absolute link on the RERUM_PREFIX origin, a path-only link without it, nothing on the final page, and only ever appends.

Verification

Read-only, against the local pm2 app reading the dev collection:

Request Result
POST ?limit=3&skip=6 context link and next to skip=9
POST ?limit=2 over exactly 2 matches context link only
POST ?limit=1000&foo=bar next carries limit=500, drops foo
POST ?limit=500&skip=100000 next to skip=100500
POST ?limit=500&skip=100500 400 with Pagination-Skip-Max: 100000
POST /v1/api/getByProperties.action?limit=3 next to /v1/api/query
POST with no matches 200 [], context link only
HEAD /v1/api/query 405
HEAD /v1/api/getByProperties.action 405

The published pagedQuery example, run against the same app over {"_id":{"$type":"string","$gte":"69e9"}} at limit=500, read 2433 records in 5 pages with no duplicates and stopped when next was absent.

Breaking

  • rel="next" is additive. Existing clients ignore the header and behave as they do now.
  • HEAD /query now answers 405 where it answered 200 or 404. A client relying on it was already unreliable: HEAD cannot carry the filter body, and a bodiless HEAD matched every record.

@thehabes
thehabes marked this pull request as ready for review September 17, 2026 16:18
@thehabes
thehabes requested a review from cubap as a code owner September 17, 2026 16:18
@thehabes
thehabes deployed to development September 17, 2026 16:19 — with GitHub Actions Active
@cubap

cubap commented Sep 18, 2026

Copy link
Copy Markdown
Member

Apologies for earlier comments being posted as general PR comments. Here are the specific inline suggestions:

These points target the code changes in this PR and should be applied accordingly.

@thehabes
thehabes added this pull request to stack #315 September 18, 2026 16:18
@thehabes thehabes mentioned this pull request Sep 18, 2026
PR #295 added the sendMethodNotAllowed helper with 'POST,HEAD' on main.
This PR removes the HEAD handler, so the Allow header must advertise only
POST per RFC 9110 section 15.5.6. Merge main in to keep the branch
conflict-free.

@cubap cubap left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Reviewed the full diff against issues #302, #304, and #96. The implementation is solid and spec-compliant:

  • #302 (rel="next" paging): over-fetch limit + 1 and trim before serialization, setNextPageLink helper, OpenAPI NextPageLink component, API.html pagedQuery rewritten to follow rel="next" ? all correct. Verified the alias path too: /v1/api/getByProperties.action?limit=2 correctly links to /v1/api/query?limit=2&skip=2.
  • #304 (remove HEAD /query): handler, controller, export, OpenAPI op, contract test, and docs all removed consistently.
  • #96 (HEAD artifacts): only /query had an explicit HEAD handler; /since and /history are GET routes where Express auto-answers HEAD, so nothing to remove there.

One issue found and fixed

After #295 merged (Allow headers on 405), main's routes/query.js advertises Allow: POST,HEAD. This PR removes the HEAD handler, so the merged result would advertise HEAD on a resource that no longer supports it ? a violation of RFC 9110 ?15.5.6, and it broke the route_wrappers test (rejects unsupported methods for /query).

I pushed a fix (commit 0d055df) that merges main into the branch and changes the 405 fallback to rest.sendMethodNotAllowed(res, '...', 'POST'). Full test suite now passes (132/132).

Minor note (no action needed)

db-controller.js.backup is a tracked stale backup that still contains queryHeadRequest ? pre-existing, not part of the build.

@cubap
cubap deployed to development September 24, 2026 17:47 — with GitHub Actions Active
@cubap

cubap commented Sep 24, 2026

Copy link
Copy Markdown
Member

@thehabes ? I've pushed the suggested changes to 302-query-optimization (commit 0d055df): merged main in and updated the /query 405 fallback to rest.sendMethodNotAllowed(res, '...', 'POST') so the Allow header no longer advertises HEAD after this PR removes the HEAD handler. Tests pass (132/132). Ready for merge.

This branch was successfully deployed

1 active deployment
development — 0d055df4 Deployed Sep 24, 2026 by cubap via deploy (24, vlcdhp02) #572
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants