Skip to content

feat(packages): implement multipart .xar upload on POST /api/packages/install#49

Merged
duncdrum merged 4 commits into
eXist-db:developfrom
joewiz:feat/packages-multipart-upload
Jul 8, 2026
Merged

feat(packages): implement multipart .xar upload on POST /api/packages/install#49
duncdrum merged 4 commits into
eXist-db:developfrom
joewiz:feat/packages-multipart-upload

Conversation

@joewiz

@joewiz joewiz commented Jun 6, 2026

Copy link
Copy Markdown
Member

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

Implement multipart .xar upload on POST /api/packages/install

From the existdb-oxygen-plugin (oxex) Package Manager tasking. The endpoint already declared a multipart/form-data body with a binary file field, but packages:install only implemented the JSON registry path ({name, url, version}repo:install-and-deploy). A file upload fell straight through to {"error": "Missing required fields: name, url"} — so clients could install from a registry by name but could not upload and install a locally built .xar, the headline deploy-a-built-app workflow for editor clients.

Change

Dispatch on the presence of a file part:

  • multipart uploadpackages:install-from-upload: store the bytes to /db/system/repo, repo:install-and-deploy-from-db, clean up the temp .xar, and return name/version/target from the deployed descriptor. repo:install-and-deploy-from-db is idempotent, so re-uploading a new build of an already-installed package replaces it — no explicit pre-removal needed. Malformed uploads return a caught {success: false, error: {...}} with HTTP 400, not a 500.
  • JSON body → the existing registry path (refactored into packages:install-from-registry, behavior unchanged).

Response shape matches across both paths: { success, result: { name, version, target } }.

Verification

  • Multipart upload of a fixture .xar installs + deploys (appears in GET /api/packages); re-upload replaces idempotently; temp .xar is cleaned up; JSON path unchanged; malformed JSON still errors as before. Verified on the integration instance and deployed to the oxex test instance.
  • Cypress: 5 new tests in packages.cy.js. The happy-path upload is driven via cy.exec + curl rather than cy.request: Cypress mangles raw binary request bodies (Buffer↔JSON serialization), so a real multipart file upload can't go through cy.request reliably — curl sends correct bytes. Fixture: src/test/cypress/fixtures/test-multipart.xar (566-byte minimal EXPath package, force-added past the *.xar gitignore).

api.json already declared the multipart requestBody and the 200/400 responses, so no spec change was needed.

Downstream

Unblocks the oxygen plugin's "Install .xar…" file-chooser; same local-deploy capability for eXide, vscode-existdb, and the notebook kernel over the shared HTTP contract.

@duncdrum duncdrum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a note we have encoding: binary and the basic form data append to model the upload via cy.request what exactly isn't working?

https://filiphric.com/cypress-basics-uploading-file#upload-via-api

@joewiz

joewiz commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

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

You're right — the curl/cy.exec route was a workaround for a believed cy.request binary-body limitation, but encoding: 'binary' + a hand-built form-data envelope handles it natively (thanks for the link). Switched the multipart test over in 0b18363 — no more curl-on-runner or hardcoded creds, and CI will confirm it green.

Worth noting it's not throwaway even though package-install is converging onto the core servlet (eXist-db/exist#6247, per the api-/exist-strategy plan): #49 stays the working surface until /api/packages is consumer-ready, and the same binary-upload technique will cover eXist-db/exist#6247's endpoint test too.

Comment thread src/test/cypress/e2e/packages.cy.js Outdated

// cy.request returns the response body as a raw string under
// encoding: 'binary', so parse JSON defensively (handles either form).
const asJson = (response) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this is still missing the iniitial formdata blog construction to append to in the request.

@duncdrum duncdrum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

test are failing, we seem to be missing construction of the initial formdata blob to append to

@joewiz joewiz force-pushed the feat/packages-multipart-upload branch from 0b18363 to 099e162 Compare June 21, 2026 04:58
joewiz and others added 4 commits July 7, 2026 22:17
…/install

The endpoint declared a multipart/form-data body with a binary `file`
field, but packages:install only implemented the JSON registry path
({name,url,version} -> repo:install-and-deploy); a file upload fell
through to {"error":"Missing required fields: name, url"}. So clients
could install from a registry by name but could not upload and install a
locally built .xar -- the headline deploy-a-built-app workflow for
editor clients (Oxygen plugin, eXide, vscode, notebook).

Dispatch on the presence of a `file` part: multipart upload ->
packages:install-from-upload (store bytes to /db/system/repo,
repo:install-and-deploy-from-db, clean up the temp .xar, return
name/version/target from the deployed descriptor); else the existing
JSON registry path (refactored into packages:install-from-registry,
unchanged). repo:install-and-deploy-from-db is idempotent, so
re-uploading a new build of an installed package replaces it without
explicit pre-removal. Malformed uploads return a caught
{success:false, error:{...}} with HTTP 400, not a 500.

Verified: multipart upload of a fixture .xar installs + deploys (appears
in GET /api/packages), re-upload replaces idempotently, temp .xar is
cleaned up, the JSON path is unchanged, and a malformed JSON body still
errors as before.

Cypress: 5 new tests in packages.cy.js. The happy-path upload is driven
via cy.exec + curl rather than cy.request -- Cypress mangles raw binary
request bodies (Buffer<->JSON serialization), so a real multipart file
upload can't go through cy.request reliably; curl sends correct bytes.
Fixture: src/test/cypress/fixtures/test-multipart.xar (566-byte minimal
EXPath package).

Refs oxex multipart-install tasking (2026-06-06).
…pload

Align the multipart upload path with how xst, the dashboard, and
atom-editor-support install a .xar:

- Pass the public registry's /find endpoint to repo:install-and-deploy-from-db
  so transitive dependencies are resolved from the registry during install
  (previously the 1-arg form was used, so an upload with unmet dependencies
  failed instead of resolving them).
- Undeploy + remove any previously installed copy of the same package before
  reinstalling, rather than relying solely on install-and-deploy-from-db
  idempotency. The package name is read from the upload's expath-pkg.xml via a
  new packages:xar-package-name helper.

Verified on a clean eXist 7.0.0-beta3: fresh install, re-install (pre-removal
path), and temp-.xar cleanup all succeed; xar-package-name extracts the
package name from the uploaded descriptor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…url/cy.exec

Per review (duncdrum): cy.request can do the binary multipart upload natively
with encoding: 'binary' + a hand-built form-data envelope, so the cy.exec+curl
workaround (and its curl-on-runner / hardcoded-creds dependency) is unnecessary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Model the multipart upload through cy.request (auto multipart/form-data
boundary from a FormData body) rather than a cross-origin native fetch,
per Duncan's review. cy.request returns an empty body on the upload
response, so assert only its HTTP status and read the deployed package's
identity (name, version, abbrev, target) from a follow-up
GET /api/packages/{name}. Keeps the whole flow same-origin through the
Cypress proxy and out of the browser's CORS path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017C5N8P9iJKm8MYA9frPkAf
@joewiz joewiz force-pushed the feat/packages-multipart-upload branch from 099e162 to faa4a5c Compare July 8, 2026 02:25
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 14 complexity · 2 duplication

Metric Results
Complexity 14
Duplication 2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@joewiz

joewiz commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

[This response was prompted by Joe, drafted by Claude Code, and reviewed by Joe.]

Thanks @duncdrum — you were right that this belonged on cy.request, and I've reworked it that way. Two things were going on:

1. The upload now goes through cy.request, not a cross-origin fetch. The earlier commit had drifted to the browser's native fetch because cy.request returns an empty body on the upload response, and I wanted to assert on the install result. The fix is to stop relying on that response body: cy.request uploads the FormData (binary blob appended as the file part, auto multipart boundary) and I assert only on its HTTP status, then read the deployed package's identity — name, version, abbrev, target — from a follow-up GET /api/packages/{name}. That keeps the whole flow same-origin through the Cypress proxy and out of the CORS path, which is what you were pointing at.

2. The branch was badly stale. It was cut at info.version 0.9.7 and had fallen 8 commits behind develop, which by now is incompatible with the current existdb/existdb:latest image (the image ships the langservice signature-help work that landed on develop in the meantime, so every route 500s on the old branch). I rebased onto current develop; the multipart request-body schema was already on develop, so after the rebase this branch's only delta is the handler (packages.xqm), the tests, and the fixture — no api.json change, so no version bump needed.

Verified against a clean existdb/existdb:latest container using CI's exact sequence (xst package install … --force → restart eXist → cypress run): 199/199 cypress tests green, including all four multipart cases. Pushed as faa4a5c; CI on the PR is now green (Cypress e2e + Codacy).

@duncdrum duncdrum merged commit 78f8e03 into eXist-db:develop Jul 8, 2026
2 checks passed
@joewiz joewiz deleted the feat/packages-multipart-upload branch July 8, 2026 14:15
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.

2 participants