/v1/id/_:id/expanded endpoint - #286
Conversation
…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.
…ests Introduce tests
|
Reviewed the diff end to end. The Required: strip
queryObj["$and"].push({ [key]: value })So a client can POST Add a guard in Recommended: confirm two behavior changes
|
cubap
left a comment
There was a problem hiding this comment.
Reviewed the diff end to end. The __rerum forgery fix and the hasOwnProperty to Object.hasOwn change are both correct and well tested. One thing needs to change before this ships.
| const RESERVED_FILTER_KEYS = ["target", "type", "@type", "__rerum.history"] | ||
|
|
||
| /** | ||
| * The Annotation body types whose value is kept whole rather than read as a single assertion. |
There was a problem hiding this comment.
This drops four reserved keys but leaves every other key alone, including $-prefixed MongoDB operators. findLeafAnnotationsFor() pushes each surviving key straight into the query as { [key]: value }, so a client can POST {"$where": "<js>"} and it lands as a live operator. $where runs JavaScript on the server and this endpoint is unauthenticated, so that is a real RCE path scoped to POST /v1/id/:_id/expanded (the GET path only reads ?generator/?creator, so it is safe).
Add a guard here that drops any key starting with $, and a test asserting a $where or $or key is stripped.
|
Change request split out into its own scope via #289 |
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.