Skip to content

feat(db): path-in-URL streaming endpoint /api/db/resource/{path} (closes #35) - #38

Closed
joewiz wants to merge 2 commits into
eXist-db:developfrom
joewiz:feat/binary-resource-transport
Closed

joewiz wants to merge 2 commits into
eXist-db:developfrom
joewiz:feat/binary-resource-transport

Conversation

@joewiz

@joewiz joewiz commented Jun 4, 2026

Copy link
Copy Markdown
Member

[This PR was co-authored with Claude Code. -Joe]

Closes #35.

Summary

Adds a path-in-URL sibling endpoint at /api/db/resource/{path} that streams via eXist's REST servlet, complementing the existing JSON-envelope endpoint:

URL shape Use for Behavior
PUT /api/db/resource (path in JSON body) Text + metadata in a single roundtrip JSON envelope; Roaster handler; body buffered (tens-of-MB ceiling)
PUT /api/db/resource/{path} (path in URL) Binary or any large upload Raw bytes, mime from Content-Type header — streams via /exist/rest

Same URL prefix (/exist/apps/existdb-openapi/api/db/resource…), no web.xml changes, no second URL space. controller.xq routes the path-in-URL variant to /exist/rest/{path} via <forward url="/rest{$path}" absolute="yes"/>. The absolute="yes" flag escapes the controller context so the URL resolves to the servlet root rather than /db/apps/existdb-openapi/rest/….

Why this shape

The original PR design (base64 inside the JSON envelope) was rethought after surveying peer systems:

System Wire format for binary Same endpoint as text?
eXist WebDAV (Jackrabbit) Raw bytes over HTTP, streamed Yes
eXist XML-RPC base64 inside XML-RPC envelope (wire-format quirk) No (separate methods)
eXide (/api/storage) Raw bytes via fetch(url, {body: <File>}) Yes
xst CLI XML-RPC (base64 in envelope) n/a
atom-editor-support Raw bytes via request:get-data() Yes
fusion-studio-api Raw bytes via RESTXQ %rest:PUT("{$body}") Yes

Five of six peers use raw bytes over HTTP for binary; nobody uses base64-in-JSON. The path-in-URL forward matches the eXide / fusion-studio-api / WebDAV idiom (path-in-URL, raw bytes, mime from Content-Type) while keeping the JSON envelope available at the bare URL for text-with-metadata.

A second concern that tipped this design: Roaster's request-body pipeline always materializes the body in JVM memory (see roaster:content/body.xqm's body:parse — every Content-Type branch ends up reading the full body into XQuery values before the handler runs), so the JSON-envelope endpoint inherits a memory ceiling that grows with file size. A path-in-URL forward to /exist/rest sidesteps Roaster entirely and streams through eXist's RESTServer.java, where the body becomes an InputStream that broker.storeDocument(InputSource) consumes directly — no JVM buffer.

controller.xq dispatch

else if (matches($exist:path, "^/+api/db/resource/.+")) then
    let $db-path := replace($exist:path, "^/+api/db/resource", "")
    return
        <dispatch xmlns="http://exist.sourceforge.net/NS/exist">
            <forward url="/rest{$db-path}" absolute="yes">
                <set-header name="Access-Control-Allow-Origin" value="*"/>
                <set-header name="Access-Control-Allow-Methods" value="GET, PUT, DELETE, HEAD, OPTIONS"/>
                <set-header name="Access-Control-Allow-Headers" value="Content-Type, Authorization"/>
            </forward>
        </dispatch>

The discovery that made this work: eXist URL-rewriting docs explicitly note that absolute="yes" resolves the url against the servlet context root rather than the controller context. Without it, /rest/... resolves to /db/apps/existdb-openapi/rest/... and the forward silently misfires (returns 201 but the request never reaches EXistServlet — an interesting URLRewrite framework quirk I hit while developing this).

End-to-end verification

Manual curl with a 68-byte transparent PNG:

$ curl -u admin: -X PUT -H "Content-Type: image/png" --data-binary @pixel.png \
    http://localhost:8080/exist/apps/existdb-openapi/api/db/resource/db/pixel.png
HTTP/1.1 201

$ curl -u admin: http://localhost:8080/exist/apps/existdb-openapi/api/db/properties?path=/db/pixel.png
{"mime-type": "image/png", "size": 68, ...}

$ curl -u admin: http://localhost:8080/exist/apps/existdb-openapi/api/db/resource/db/pixel.png -o back.png
$ diff pixel.png back.png      # ✓ byte-identical
$ xxd -l 8 back.png             # 89 50 4e 47 0d 0a 1a 0a  ← canonical PNG signature

DELETE round-trips too:

$ curl -u admin: -X DELETE http://localhost:8080/exist/apps/existdb-openapi/api/db/resource/db/pixel.png
HTTP/1.1 200
$ curl -u admin: http://localhost:8080/exist/apps/existdb-openapi/api/db/properties?path=/db/pixel.png
HTTP/1.1 404

Cypress

5 new tests in db.cy.js for the path-in-URL endpoint covering PUT / GET / DELETE routing with text content. The controller.xq forward logic is body-type-agnostic; text exercises the same routing path as binary. (cy.request can't cleanly send raw binary bytes — Cypress's Buffer ↔ JSON serialization mangles them — so the binary round-trip is verified via the curl recipe above. Considered using cy.task to invoke node-side HTTP for the binary case but the marginal CI value didn't justify the plugin overhead.) Full cypress suite: 110/110 passing.

OpenAPI spec

/api/db/resource/{path} documented as a sibling endpoint with PUT/GET/DELETE. The request/response bodies are described as application/octet-stream (type: string, format: binary) per OpenAPI conventions. The bare /api/db/resource endpoint's description gained a note pointing at the path-in-URL variant for binary uploads.

Out of scope / known limits

  • Roaster's request-body buffering ceiling still applies to the JSON-envelope endpoint. Clients hitting that ceiling for large text uploads should also use the path-in-URL variant (with Content-Type: text/plain or appropriate).
  • The JSON envelope GET on a binary resource is still lossyutil:binary-to-string interprets bytes as UTF-8 and mangles anything non-UTF-8. Recommendation in the OpenAPI description: use the path-in-URL variant for binary GET.
  • Multipart upload is not added; clients that want to upload many resources at once should make multiple requests.

Dependency on #34

This PR is branched off #34 (fix/store-resource-mime-auto-detect). The two PRs share modules/db.xqm but the path-in-URL endpoint is implemented entirely in controller.xq (no db.xqm changes specific to this PR beyond the comment update). Merge #34 first → this rebases cleanly.

Considered and rejected: wrapping REST's response in a JSON envelope via <view>

A natural follow-up question: could a <forward> + <view> pipeline normalize REST's minimal response (empty body, just an HTTP status) into the same {stored, runPath} / {removed} / {error} JSON envelope the bare /api/db/resource handler returns? The eXist URL-rewriting framework supports exactly this kind of two-stage chain (the documentation app and eXide both use it for HTML templating).

Tried it on a side branch. The pattern composes cleanly when the first forward targets a DB-backed XQuery — the framework already knows how to dispatch through to a view handler in that case. But our first forward targets a servlet (EXistServlet for streaming) which lives outside the URL-rewriting tree, and the view's subsequent <forward url="modules/wrap-rest-response.xq"/> fails to resolve cleanly:

  • absolute path ({$exist:controller}/modules/wrap-rest-response.xq) → 500 with NPE in XQueryServlet.process line 358 (Paths.get(path) with null path — neither xquery.source nor xquery.url attribute is set, so XQueryServlet falls through to the filesystem-path branch)
  • relative path (modules/wrap-rest-response.xq) → 400 with empty body; the framework resolves relative paths against the current request path, which after the rest forward is /exist/rest/db/..., so the lookup never finds the XQuery

There's likely a way to make it work (manually setting xquery.source or xquery.url attributes on the view forward, or routing through a different servlet mapping) but it would be reverse-engineering framework internals to glue together what isn't designed to chain. The cost outweighs the benefit, given that REST's HTTP status codes already convey what clients need to know and the peer survey shows no precedent for envelope-wrapping binary responses (none of WebDAV, XML-RPC, eXide, xst, atom-editor-support, or fusion-studio-api do this).

If we ever decide the response envelope matters enough to justify the work, the cleaner path would be a thin Java handler that takes raw bytes via servlet InputStream and emits JSON directly — but that requires a web.xml change, which would prevent existdb-openapi from being a self-contained XAR install (the same reason we rejected the "bypass roaster with a Java servlet" option earlier in the binary-transport discussion).

joewiz and others added 2 commits June 4, 2026 10:41
The PUT /api/db/resource handler was defaulting the mime-type to
"application/xml" when a client omitted it. That meant every upload
without an explicit Content-Type was routed into eXist's XML parser
regardless of the path's extension:

  - .xq / .xqm content → XPST0003 ("Content is not allowed in prolog")
    because XQuery source isn't well-formed XML.
  - .svg → stored as application/xml (the parser happens to accept it,
    but the stored mime is wrong; clients fetching back see XML rather
    than SVG and lose any download/render-by-mime affordance).
  - .json → also stored as application/xml or rejected, depending on
    well-formedness.

Drop the default and let the 3-arg form of xmldb:store consult eXist's
MimeTable (see XMLDBStore.java lines 152-154: when the function is
called without an explicit mime-type it falls back to
MimeTable.getInstance().getContentTypeFor(docName), which honors the
server's mime-types.xml — `.xq` → application/xquery, `.svg` →
image/svg+xml, `.json` → application/json, etc.).

The OpenAPI spec for this endpoint already promised "MIME type is
auto-detected from the file extension if not specified"; this commit
makes the implementation match the documented contract. The spec
wording is also tightened to name mime-types.xml as the lookup source.

Concrete downstream impact:

  - existdb-oxygen-plugin can drop its hardcoded MIME_BY_EXTENSION
    table (currently in ExistURLConnection.java:51-70, ~20 lines)
    and just omit mime-type on uploads — the server will pick the
    right one from the path.
  - Any future client that uploads .xq / .xqm / images / JSON
    without sending a Content-Type gets the right behavior
    out of the box rather than a 500.

Additional cleanup: the catch arm's text/html → store-as-binary
fallback is removed. It never actually worked — xmldb:store-as-binary
runs string content through eXist's XML parser anyway, and
xmldb:set-mime-type refuses to relabel a binary resource with an
XML-class mime (`text/html` is XML-class per mime-types.xml). The
fallback has been broken since written. Verified that no in-tree
consumer relies on it: eXide stores HTML as application/xhtml+xml
by default (only template-created HTML uses text/html), and the
oxygen plugin always supplies an explicit mime. Clients that need to
preserve raw bytes for unparseable content can now send
mime-type=application/octet-stream explicitly (covered by a new
test).

Cypress: 27 cases on db.cy.js, 7 new — auto-detect for .xq / .xqm /
.svg / .json, well-formed .html → text/html, unparseable HTML → 400
with parse-error message, and octet-stream preserves raw bytes. All
27 pass. Full cypress suite: 4 pre-existing failures on develop
(packages.cy.js × 1, query_scope.cy.js × 2, query_pool_reuse.cy.js
× 2) remain unchanged — verified by running them against develop
without this fix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes eXist-db#35.

Adds a path-in-URL sibling to /api/db/resource that streams via eXist's
REST servlet. Uses controller.xq's <forward url="/rest{path}" absolute="yes">
to internally dispatch the request to /exist/rest, where the body
streams directly into broker.storeDocument(InputSource) without
materializing in JVM memory. The `absolute="yes"` attribute is what
escapes the controller context — without it, /rest/* resolves relative
to /db/apps/existdb-openapi/ rather than the servlet root.

| URL shape | Use for | Behavior |
|---|---|---|
| `PUT /api/db/resource` | Text + metadata in single roundtrip | JSON envelope, path in body, Roaster handler — buffered (tens-of-MB ceiling) |
| `PUT /api/db/resource/{path}` | Binary or any large upload | Raw bytes, Content-Type from header — streams via REST |

The path-in-URL endpoint supports PUT, GET, and DELETE — forwarded through
to /exist/rest, which returns its native responses (HTTP status + raw bytes
on GET). The JSON-envelope endpoint at /api/db/resource is unchanged.

Surveyed against eXist's WebDAV (Jackrabbit) and XML-RPC, eXide, xst,
atom-editor-support, and fusion-studio-api: five of six peers use raw
bytes over HTTP for binary transport (XML-RPC excepted, where base64 in
the XML envelope is a wire-format artifact). None use base64-in-JSON.
The path-in-URL forward matches the eXide / fusion-studio-api / WebDAV
idiom (path-in-URL, raw bytes, mime from Content-Type) while keeping
the JSON envelope available at the bare URL for text-with-metadata.

Verified end-to-end via curl: 68-byte transparent PNG PUT → /exist/rest
GET confirms storage at correct path with correct mime → /api/db/properties
sees the resource → forwarded GET retrieves byte-identical bytes (PNG
signature 89 50 4e 47 0d 0a 1a 0a survives). DELETE round-trips too.

Cypress: 5 new tests in db.cy.js for the path-in-URL endpoint. They
exercise PUT/GET/DELETE routing with text content (cy.request can't
cleanly send raw bytes — Cypress's Buffer↔JSON serialization mangles
them — so the binary round-trip is verified via curl in the PR
description; the controller.xq forward logic is the same regardless of
body type). Full cypress suite: 110/110.

Memory ceiling: roaster's request-body parser is bypassed by this
forward, so this endpoint is streaming-capable up to whatever
Jetty/eXist accept (tens of GB with config tuning). The JSON-envelope
endpoint at /api/db/resource still has Roaster's per-request buffering
ceiling (~tens of MB for the default Jetty config).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@line-o line-o 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.

I would rather like to see us come up with a way to serve binaries from roaster efficiently.

If this is done within the controller I would want to avoid to have it within the api path.

@joewiz

joewiz commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

Agreed that roaster would be the best option. An analysis of options is underway.

@joewiz

joewiz commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

[This response was co-authored with Claude Code. -Joe]

Agreed on both points — the Roaster-native path is the right north star, and parking a /rest-forward under /api/* does muddy the contract. Let me lay out what I'm seeing in Roaster as the actual blockers, in case it's useful for shaping the work.

The two ends are blocked by different things

Upload side, roaster/content/body.xqm:29-73. Every branch of body:parse() ends in request:get-data() (or request:get-uploaded-file-data() for multipart) — all of them materialize the full body into a JVM value (xs:base64Binary, parsed JSON map, XML tree) before the handler runs at router.xql:288. So even a handler that wanted to stream to xmldb:store-as-binary-resource() can't — by the time it runs, the body is already on the heap. The XQuery request:* API simply doesn't expose an InputStream handle. Nothing in Roaster can fix that on its own.

Download side, router.xql:454-477. router:write-response() returns $response?($router:RESPONSE_BODY) as the final expression, which eXist then runs through the XQuery serializer based on output:method. For binary that means the body must be a complete value already in memory. eXist does have response:stream-binary(), but that still takes a fully-materialized xs:base64Binary — there's no zero-copy "stream from stored BinaryDocument to HTTP response" path callable from XQuery today.

What I think Roaster would need

Three layers, roughly:

  1. Opt-out from body:parse(). An OpenAPI extension like x-roaster-raw-body: true on the operation tells Roaster to skip parsing and hand the handler a stream handle in $request?body-raw instead. This is the easy part — Roaster-side change is ~15 lines. But it's only useful once initial release #2 exists.

  2. An eXist-level streaming primitive. Either request:get-input-stream() returning a usable handle (for uploads) or response:stream-binary-resource($path, $media-type) that opens the BinaryDocument and copies bytes to the response OutputStream without an intermediate XQuery value (for downloads). These belong in exist-core, not Roaster — Roaster just exposes them through the route contract.

  3. A "dispatch to servlet" passthrough. The deepest version: an OpenAPI extension like x-roaster-passthrough: { servlet: "exist", path: "/rest{path}" } that lets Roaster honor the spec for routing/validation/docs but hand the actual byte handling off to EXistServlet without ever crossing the XQuery boundary. This is basically what RESTServer.java already does well — Roaster would just route to it. It's also what the controller workaround in this PR is achieving externally; lifting it into Roaster proper would make it a first-class spec-driven feature instead of a per-app side door.

(1) gives the API surface, (2) gives it teeth, (3) is the "do what /exist/rest does without a controller" version. They can land independently — (1) alone is harmless, (2) alone is useful even for non-Roaster apps, (3) is the eventual destination.

On /api/* specifically

Hear you on the path concern. If we keep #38 as a controller workaround in the interim, I'd move the path-in-URL variant out of /api/* — something like /files/{path} under the app root — so /api stays Roaster's. Want me to revise the PR along those lines, or hold it pending the Roaster work above?

@joewiz

joewiz commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

[This response was co-authored with Claude Code. -Joe]

Surveyed the five neighboring systems for prior art on this — the convergence is striking and points at why a Roaster-side fix alone won't fully close the gap.

Two patterns exist, nothing in between

Pattern A — Java-servlet-direct, fully streaming, never crosses XQuery:

  • eXist REST (RESTServer.java) — PUT is request.getInputStream()CachingFilterInputStreambroker.storeDocument(InputSource). Binary GET is broker.readBinaryResource(BinaryDocument, response.getOutputStream()) — direct byte copy from internal store to the response. No XQuery layer involved either direction.
  • eXist WebDAV — pure Java (Milton on eXist 6, Jackrabbit on eXist 7). Streaming by definition; never touches XQuery.
  • BaseX REST — docs explicitly call out application/octet-stream PUT as curl -T file … with raw bytes; servlet-direct in the same fashion.

Pattern B — XQuery-value binding, materialized to xs:base64Binary or equivalent:

  • eXist RESTXQ (HttpServletRequestAdapter.java) — body bound via %rest:POST("{$body}"). Backed by CachingFilterInputStreamCache, so large bodies disk-spill rather than blowing JVM heap, but the XQuery handler still sees an xs:base64Binary value (just disk-backed instead of heap-backed).
  • BaseX RESTXQ%rest:POST("{$body}") with $body as xs:base64Binary. Docs: unrecognized content types "default to xs:base64Binary." Multipart parts "are represented as a sequence, and each part is converted to an XQuery item." Full materialization.
  • eXist XML-RPC — base64 inside the XML-RPC envelope. Worst case: whole body in heap plus base64 inflation tax.
  • Roaster — same as Pattern B; one rung worse than eXist RESTXQ because Roaster's body:parse() reaches request:get-data() for every content type, without the caching layer.

Nothing in any of these surveyed systems offers a spec-driven binding (annotation-based or OpenAPI-based) that also exposes a raw InputStream to the handler. The choice everywhere has been: servlet-direct = streaming + no spec, or spec-driven = value materialization. The two haven't been bridged yet, in any of them.

BaseX Request module specifically

No streaming hook there either — full list of body-adjacent functions is request:parameter*, request:header*, request:cookie*, request:attribute*, request:method, plus URI/connection accessors. No request:get-input-stream or equivalent. Same gap as eXist's request:*.

What that means for the Roaster work

A genuine free improvement Roaster can grab today: swap body:parse()'s request:get-data() for the CachingFilterInputStream pattern eXist RESTXQ uses. Same $request?body contract for handlers, but large bodies disk-spill instead of OOM-ing. Roaster-only change, no exist-core dependency.

The bigger win — handler sees a real InputStream and can pipe directly to broker.storeDocument() without an intermediate value — needs the eXist-side request:get-input-stream() primitive I mentioned in the previous reply, and then a thin Roaster opt-out (x-roaster-raw-body: true) to surface it. That would put Roaster ahead of both RESTXQ implementations on this axis, since neither offers the InputStream escape hatch.

If both of those land, the x-roaster-passthrough idea (route directly to EXistServlet, skip XQuery entirely) becomes less urgent — though still potentially worth having for cases where you want to keep an OpenAPI-documented surface but defer 100% of byte handling to existing servlet code.

@joewiz

joewiz commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

[This response was co-authored with Claude Code. -Joe]

Closing as superseded by #59, which consolidates all resource-content transport onto a single binary-safe /api/db/resource and retires the path-in-URL /api/db/resource/{path} shape introduced here — the outcome of the design review captured in #59. Planned merge order: #54#55#59.

@joewiz joewiz closed this Jun 15, 2026
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jun 16, 2026
…th, set-mime

Adds the audit §2 items 2–6 to db-core (the single implementation), exposed
through the thin roaster wrapper and api.json, with Cypress coverage:

- item 2 — GET /api/db/resource?meta=full flattens the resource's metadata
  (owner, group, mode, acl, size, created, last-modified) alongside the
  content, sparing a second /properties round trip. Flat (not nested) to
  match both eXide's load response and our own /properties shape. Default
  (no meta) is unchanged.
- item 3 — every list item carries a `writable` boolean (sm:has-access "w"),
  a file-browser affordance evaluated authoritatively server-side rather
  than left for the client to derive from mode bits.
- item 5 — a flat (non-recursive) listing carries start/count pagination and
  a `total`; child collections then resources, sliced by start (1-based) and
  count (default: all). Tree listings are unaffected.
- item 6 — get-resource always returns runPath.
- item 4 — set MIME via POST /api/db/permissions (xmldb:set-mime-type). A
  mime incompatible with the resource's storage class (eXist only allows an
  XML-class mime on an XML resource, a binary-class mime on a binary one)
  now surfaces as a clean 400 instead of an undeclared-500 → SENR0001.

writable + pagination are always-on (per design decision): default output is
a superset of the prior shape, never a truncation. Serialization (PR eXist-db#48) and
binary streaming (eXist-db#38/eXist-db#35) remain out of scope.

Verified on a live eXist: 54 db Cypress tests pass (46 prior + 8 new),
query.cy.js stays green (routing intact).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jun 18, 2026
…th, set-mime

Adds the audit §2 items 2–6 to db-core (the single implementation), exposed
through the thin roaster wrapper and api.json, with Cypress coverage:

- item 2 — GET /api/db/resource?meta=full flattens the resource's metadata
  (owner, group, mode, acl, size, created, last-modified) alongside the
  content, sparing a second /properties round trip. Flat (not nested) to
  match both eXide's load response and our own /properties shape. Default
  (no meta) is unchanged.
- item 3 — every list item carries a `writable` boolean (sm:has-access "w"),
  a file-browser affordance evaluated authoritatively server-side rather
  than left for the client to derive from mode bits.
- item 5 — a flat (non-recursive) listing carries start/count pagination and
  a `total`; child collections then resources, sliced by start (1-based) and
  count (default: all). Tree listings are unaffected.
- item 6 — get-resource always returns runPath.
- item 4 — set MIME via POST /api/db/permissions (xmldb:set-mime-type). A
  mime incompatible with the resource's storage class (eXist only allows an
  XML-class mime on an XML resource, a binary-class mime on a binary one)
  now surfaces as a clean 400 instead of an undeclared-500 → SENR0001.

writable + pagination are always-on (per design decision): default output is
a superset of the prior shape, never a truncation. Serialization (PR eXist-db#48) and
binary streaming (eXist-db#38/eXist-db#35) remain out of scope.

Verified on a live eXist: 54 db Cypress tests pass (46 prior + 8 new),
query.cy.js stays green (routing intact).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jun 18, 2026
…t-db#38)

Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource
transport, the clean alternative to the JSON-envelope /api/db/resource (which is
text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller
workaround (Juri's concern about an /api/* side door).

- GET streams a binary resource's raw bytes with response:stream-binary (NOT via
  the serializer, which would emit base64 text and corrupt it — the established
  roaster pattern, the same path eXist's REST server uses); an XML/text resource
  is returned as a node so roaster serializes it once with its stored mime
  (returning a pre-serialized string would be XML-escaped a second time).
- PUT stores the raw request body via db-core (binary bodies arrive intact;
  mime inferred from the name), returning { stored, runPath } (201/200).

No exist-core or roaster change needed — binary transport works on stock eXist
via the existing response:stream-binary. (Zero-copy streaming of very large
stored binaries is a separate exist-core optimization, tracked with
exist-strategy.) Self-contained Cypress coverage (raw round-trip not
base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full
db suite stays green (59 + 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jun 18, 2026
…ource

Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.

- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
  serialized from the node tree (eXist has no raw byte form for XML). Content-Type
  is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
  Content-Type or inferred from the name; returns { path } (201/200) so the caller
  can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
  namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
  add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
  on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
  client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
  and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.

Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
duncdrum pushed a commit that referenced this pull request Jun 18, 2026
…th, set-mime

Adds the audit §2 items 2–6 to db-core (the single implementation), exposed
through the thin roaster wrapper and api.json, with Cypress coverage:

- item 2 — GET /api/db/resource?meta=full flattens the resource's metadata
  (owner, group, mode, acl, size, created, last-modified) alongside the
  content, sparing a second /properties round trip. Flat (not nested) to
  match both eXide's load response and our own /properties shape. Default
  (no meta) is unchanged.
- item 3 — every list item carries a `writable` boolean (sm:has-access "w"),
  a file-browser affordance evaluated authoritatively server-side rather
  than left for the client to derive from mode bits.
- item 5 — a flat (non-recursive) listing carries start/count pagination and
  a `total`; child collections then resources, sliced by start (1-based) and
  count (default: all). Tree listings are unaffected.
- item 6 — get-resource always returns runPath.
- item 4 — set MIME via POST /api/db/permissions (xmldb:set-mime-type). A
  mime incompatible with the resource's storage class (eXist only allows an
  XML-class mime on an XML resource, a binary-class mime on a binary one)
  now surfaces as a clean 400 instead of an undeclared-500 → SENR0001.

writable + pagination are always-on (per design decision): default output is
a superset of the prior shape, never a truncation. Serialization (PR #48) and
binary streaming (#38/#35) remain out of scope.

Verified on a live eXist: 54 db Cypress tests pass (46 prior + 8 new),
query.cy.js stays green (routing intact).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jun 18, 2026
…t-db#38)

Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource
transport, the clean alternative to the JSON-envelope /api/db/resource (which is
text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller
workaround (Juri's concern about an /api/* side door).

- GET streams a binary resource's raw bytes with response:stream-binary (NOT via
  the serializer, which would emit base64 text and corrupt it — the established
  roaster pattern, the same path eXist's REST server uses); an XML/text resource
  is returned as a node so roaster serializes it once with its stored mime
  (returning a pre-serialized string would be XML-escaped a second time).
- PUT stores the raw request body via db-core (binary bodies arrive intact;
  mime inferred from the name), returning { stored, runPath } (201/200).

No exist-core or roaster change needed — binary transport works on stock eXist
via the existing response:stream-binary. (Zero-copy streaming of very large
stored binaries is a separate exist-core optimization, tracked with
exist-strategy.) Self-contained Cypress coverage (raw round-trip not
base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full
db suite stays green (59 + 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jun 18, 2026
…ource

Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.

- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
  serialized from the node tree (eXist has no raw byte form for XML). Content-Type
  is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
  Content-Type or inferred from the name; returns { path } (201/200) so the caller
  can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
  namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
  add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
  on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
  client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
  and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.

Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jul 8, 2026
…t-db#38)

Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource
transport, the clean alternative to the JSON-envelope /api/db/resource (which is
text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller
workaround (Juri's concern about an /api/* side door).

- GET streams a binary resource's raw bytes with response:stream-binary (NOT via
  the serializer, which would emit base64 text and corrupt it — the established
  roaster pattern, the same path eXist's REST server uses); an XML/text resource
  is returned as a node so roaster serializes it once with its stored mime
  (returning a pre-serialized string would be XML-escaped a second time).
- PUT stores the raw request body via db-core (binary bodies arrive intact;
  mime inferred from the name), returning { stored, runPath } (201/200).

No exist-core or roaster change needed — binary transport works on stock eXist
via the existing response:stream-binary. (Zero-copy streaming of very large
stored binaries is a separate exist-core optimization, tracked with
exist-strategy.) Self-contained Cypress coverage (raw round-trip not
base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full
db suite stays green (59 + 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jul 8, 2026
…ource

Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.

- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
  serialized from the node tree (eXist has no raw byte form for XML). Content-Type
  is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
  Content-Type or inferred from the name; returns { path } (201/200) so the caller
  can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
  namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
  add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
  on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
  client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
  and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.

Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jul 8, 2026
…t-db#38)

Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource
transport, the clean alternative to the JSON-envelope /api/db/resource (which is
text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller
workaround (Juri's concern about an /api/* side door).

- GET streams a binary resource's raw bytes with response:stream-binary (NOT via
  the serializer, which would emit base64 text and corrupt it — the established
  roaster pattern, the same path eXist's REST server uses); an XML/text resource
  is returned as a node so roaster serializes it once with its stored mime
  (returning a pre-serialized string would be XML-escaped a second time).
- PUT stores the raw request body via db-core (binary bodies arrive intact;
  mime inferred from the name), returning { stored, runPath } (201/200).

No exist-core or roaster change needed — binary transport works on stock eXist
via the existing response:stream-binary. (Zero-copy streaming of very large
stored binaries is a separate exist-core optimization, tracked with
exist-strategy.) Self-contained Cypress coverage (raw round-trip not
base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full
db suite stays green (59 + 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jul 8, 2026
…ource

Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.

- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
  serialized from the node tree (eXist has no raw byte form for XML). Content-Type
  is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
  Content-Type or inferred from the name; returns { path } (201/200) so the caller
  can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
  namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
  add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
  on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
  client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
  and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.

Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jul 8, 2026
…t-db#38)

Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource
transport, the clean alternative to the JSON-envelope /api/db/resource (which is
text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller
workaround (Juri's concern about an /api/* side door).

- GET streams a binary resource's raw bytes with response:stream-binary (NOT via
  the serializer, which would emit base64 text and corrupt it — the established
  roaster pattern, the same path eXist's REST server uses); an XML/text resource
  is returned as a node so roaster serializes it once with its stored mime
  (returning a pre-serialized string would be XML-escaped a second time).
- PUT stores the raw request body via db-core (binary bodies arrive intact;
  mime inferred from the name), returning { stored, runPath } (201/200).

No exist-core or roaster change needed — binary transport works on stock eXist
via the existing response:stream-binary. (Zero-copy streaming of very large
stored binaries is a separate exist-core optimization, tracked with
exist-strategy.) Self-contained Cypress coverage (raw round-trip not
base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full
db suite stays green (59 + 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jul 8, 2026
…ource

Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.

- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
  serialized from the node tree (eXist has no raw byte form for XML). Content-Type
  is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
  Content-Type or inferred from the name; returns { path } (201/200) so the caller
  can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
  namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
  add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
  on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
  client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
  and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.

Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jul 8, 2026
…t-db#38)

Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource
transport, the clean alternative to the JSON-envelope /api/db/resource (which is
text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller
workaround (Juri's concern about an /api/* side door).

- GET streams a binary resource's raw bytes with response:stream-binary (NOT via
  the serializer, which would emit base64 text and corrupt it — the established
  roaster pattern, the same path eXist's REST server uses); an XML/text resource
  is returned as a node so roaster serializes it once with its stored mime
  (returning a pre-serialized string would be XML-escaped a second time).
- PUT stores the raw request body via db-core (binary bodies arrive intact;
  mime inferred from the name), returning { stored, runPath } (201/200).

No exist-core or roaster change needed — binary transport works on stock eXist
via the existing response:stream-binary. (Zero-copy streaming of very large
stored binaries is a separate exist-core optimization, tracked with
exist-strategy.) Self-contained Cypress coverage (raw round-trip not
base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full
db suite stays green (59 + 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jul 8, 2026
…ource

Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.

- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
  serialized from the node tree (eXist has no raw byte form for XML). Content-Type
  is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
  Content-Type or inferred from the name; returns { path } (201/200) so the caller
  can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
  namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
  add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
  on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
  client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
  and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.

Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jul 9, 2026
…t-db#38)

Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource
transport, the clean alternative to the JSON-envelope /api/db/resource (which is
text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller
workaround (Juri's concern about an /api/* side door).

- GET streams a binary resource's raw bytes with response:stream-binary (NOT via
  the serializer, which would emit base64 text and corrupt it — the established
  roaster pattern, the same path eXist's REST server uses); an XML/text resource
  is returned as a node so roaster serializes it once with its stored mime
  (returning a pre-serialized string would be XML-escaped a second time).
- PUT stores the raw request body via db-core (binary bodies arrive intact;
  mime inferred from the name), returning { stored, runPath } (201/200).

No exist-core or roaster change needed — binary transport works on stock eXist
via the existing response:stream-binary. (Zero-copy streaming of very large
stored binaries is a separate exist-core optimization, tracked with
exist-strategy.) Self-contained Cypress coverage (raw round-trip not
base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full
db suite stays green (59 + 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joewiz added a commit to joewiz/existdb-openapi that referenced this pull request Jul 9, 2026
…ource

Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.

- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
  serialized from the node tree (eXist has no raw byte form for XML). Content-Type
  is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
  Content-Type or inferred from the name; returns { path } (201/200) so the caller
  can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
  namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
  add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
  on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
  client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
  and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.

Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resource GET/PUT are text-only: add binary resource transport (base64)

2 participants