/v1/id/_:id/expanded endpoint - #286
Open
thehabes wants to merge 49 commits into
Open
Conversation
This was referenced Aug 11, 2026
…s cannot be expected to paginate simple GET requests. The response is now fully assembled.
…but only has one {} in it, as we can treat that the same way as a body that is just {} for expanding purposes.
…_contextid guard related to latent bug. Documentation cleanup.
…nored /expanded skips the Annotation gather and the merge for a deleted record and hands the tombstone back exactly as GET /v1/id/:_id does, with both count headers at 0. A production record with 8 leaf Annotations targeting it was being re-dressed with the descriptive data of the record it used to be. configureRerumOptions only receives a trustworthy __rerum on the update path, where the caller hands it a record from the database, so a supplied __rerum is read there alone. history and releases are built locally instead of being rebound to the received objects, so caller properties under them no longer reach storage and an affected record is cleaned on its next version.
… expanding the entity.
…ests Introduce tests
thehabes
marked this pull request as ready for review
August 20, 2026 19:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #285.
Summary
Adds a
GETandPOST /v1/id/:_id/expandedendpoint — a generic, unauthenticated, browser-cacheable endpoint that returns a RERUM object with the assertions of every current Annotation targeting it already merged in.Changes
routes/id.js—/:_id/expandedregistered ahead of/:_id.GET,POST(behindrest.verifyJsonContentType), andHEADvia Express's nativeGEThandling. Every other method is a405.OPTIONSis answered204by the CORS layer, which sits ahead of the route.controllers/crud.js—idExpanded()plus three helpers:sanitizeExpansionFilters()reduces a POST body to the filter keys this endpoint will honor.assertionsFrom()reads onlybodyandbodyValueoff an Annotation, so no other property of an Annotation can leak onto the entity.applyExpansionAnnotations()merges those assertions onto a clone of the entity and re-appends__rerumlast.db-controller.js— exportsidExpanded.controllers/utils.js—findLeafAnnotationsFor()builds the Annotation search and reads the whole result set from a single cursor, which the driver pages server-side in strides ofEXPANSION_BATCH_SIZE(200). The query plan promises no order, so the result is sorted by_idbefore_idis dropped. That is roughly Annotation creation order, and it makes the assembled entity byte-stable, which is what lets the weakETaganswer a revalidation with304.Two smaller fixes in the same file, both needed by this endpoint:
_contextid()now skips non-string members of an@contextArray. An inline term definition object names no context, and the old code called.includes()on it —_contextid(["@context" array starting with an object])threw aTypeError. Legal JSON-LD, reachable through any/v1/id/read.idNegotiation()restructured to return early instead of cloning up front and conditionally rebuilding. This prevents needless extra cloning between the routes and this helper function.utils.jsTwo changes here, neither specific to
/expandedbut both on paths it reads through:configureRerumOptions()only inheritsreceived.__rerumwhenupdateis true. This fixes a bug where client supplied values went into the __rerum property. AffectsPOST /v1/api/createandPOST /v1/api/bulkCreate.isContainerType()accepts an Array@type/typeand ignores non-string members. JSON-LD serializes a single type as a one-element Array, and with the old code a numeric type threw aTypeErrorout ofconfigureWebAnnoHeadersFor().Neither form pages
Both methods gather every Annotation matching their filters before the entity is assembled. A client asks once and receives the whole thing. There is no
?limit=or?skip=on this endpoint — the 200 infindLeafAnnotationsFor()is an internal cursor stride, not a page size.GET— the convenience formImagined as the plain
GETwhere nothing else is supplied. Recognizes?generator=and?creator=only. A value shaped like a URI is matched across both thehttpandhttpsspellings; any other value is matched exactly. A parameter supplied more than once arrives as an Array and is dropped rather than applied, as is a bracketed form like?generator[k]=v; the other parameter still applies if it was supplied correctly.POST— the filtered formThe JSON body is an object of literal MongoDB filter keys,
ANDed into the Annotation search — so filtering by generating app means supplying the real property name,__rerum.generatedBy. URL parameters supply no filters here. Not browser-cached.Because every supplied filter is
ANDed in, a filter can only narrow the result set — there is no way to widen it past the entity in the request URI.The
http/httpsdoubling applies to__rerum.generatedByandcreatorin this form too, since it lives in the shared helper. Every other key is applied exactly as given.Response headers
Cache-Control: max-age=86400, must-revalidate— only on theGET, so browsers can cache for 24h. Annotations on an entity are volatile during initial creation and stable afterward, so this trades a hard reload in the rare stale case for not re-running the expand on every visit.Annotations-Gathered— how many current Annotations were gathered for this expansion. This is a complete count, not the size of a page.Annotations-Merged— how many of those could contribute. Annotations with multiple bodies are not counted. A counted Annotation may still assert nothing mergeable, so this is not a count of properties received either.Deleted records
A deleted record is returned exactly as
GET /v1/id/:_idreturns it, with no expansion attempted and both count headers reporting0.controllers/gog.jsRefactored onto the shared helper, which is where the bulk of the deletions come from. Three behavior changes ride along, all on
/gog/id/:_id:hasOwnProperty→Object.hasOwn. An Annotation body assertinghasOwnPropertywould otherwise shadow the method and break the merge. This one is load-bearing, not cosmetic./gog/id/:_idis now restricted to data generated by a Gallery of Glosses agent and expands only with Annotations from the entity's own generator. A deleted record short-circuits ahead of that gate and returns its tombstone, since a tombstone has no generator to match on.Measured against production before merging, over the full population rather than a sample:
rerum.io/v1/id/URI under any of the six target keys.So the generator filter drops nothing today.
Documentation
public/API.htmlgets both forms underGETandPOST, plus anEntity Expansionsection carrying the merge rules, the reserved filter keys, the protected properties, and the deleted-record case. It also picks up unrelated markup repairs in passing — unclosed</span>s, a nested<code>, and two malformed<code __rerum.generatedBy>tags.openapi/contracts/core-provider.openapi.yamlgets/id/{id}/expandedwithget,head, andpost, and documents the response headers for/id/{id}as well. The reserved filter keys thePOSTignores are documented inpublic/API.htmlbut not yet in the contract.Testing
No automated tests have been written for the new endpoint yet, this is TODO.