Add multipart document uploads for the 16 resources that have the route
First item on the GOALS.md roadmap. `upload_{x}(id, files, is_public)`
attaches files through `/{resource}/{id}/upload` and returns the
refreshed record. Verified against upstream that all 16 controllers
answer with `itemResponse($x->fresh())`.
Three details that look wrong but are not, each checked in the upstream
source rather than taken from the published spec:
- The request is a POST carrying `_method=PUT`. The route is registered
PUT, but PHP does not parse a multipart body on a real PUT, so the
files never arrive. Laravel's kernel calls
`enableHttpMethodParameterOverride()`, turning the spoofed method back
into a PUT before routing. The spec documents POST with `_method=POST`,
which would 405 against these routes.
- `is_public` is sent as 1/0. Laravel's `boolean` rule accepts only
true, false, 0, 1, '0' and '1' under strict comparison, so "true"
fails validation.
- Uploads are never retried. An upload adds a document, so it is not
idempotent whatever its verb, and a replay after a lost response would
attach the file twice. This falls out of reqwest's multipart body being
un-cloneable, which nothing in the types enforces, so a test pins it
for 500, 503 and 429.
An upload with no files, a nameless file or an unparseable MIME type is
refused locally (`Error::InvalidUpload`), since the server answers an
empty upload with a success that changed nothing.
Corrects two earlier claims:
- GOALS.md and COVERAGE.md said uploads would make `create_document`
possible. They do not: `DocumentController::store` is an empty stub,
and an upload is the only way a document is created. There is no
`create_document`.
- The documented method count (265) had been stale since the typed
`statics`/`search`/`health_check` commit, which added three `_as`
methods without updating it. Recounted from the source: 284 = 210 +
18 + 12 + 16 uploads + 28 hand-written.
Also fixes a README example, `let statics: Value = client.statics()`,
that stopped compiling when `statics` lost its type parameter.
`edocument/upload` is left out: it imports an expense from an
e-document rather than attaching a file, so it moves to the
import/export roadmap item.
Enabling `multipart` adds mime, mime_guess and unicase; MSRV stays 1.88,
re-verified by running the suite on it. 101 tests, up from 91.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -100,7 +100,7 @@ IN_API_TOKEN=... cargo run --example list_clients
|
||||
|
||||
## Coverage
|
||||
|
||||
Every one of the API's 34 resource groups, plus users — 265 methods. Each
|
||||
Every one of the API's 34 resource groups, plus users — 284 methods. Each
|
||||
resource has `list`, `list_all`, `get`, `create`, `update`, `delete`, and
|
||||
(where the API offers one) `bulk`:
|
||||
|
||||
@@ -158,15 +158,33 @@ let pdf: Vec<u8> = client.download_invoice("abc123").await?;
|
||||
let merged: Vec<u8> = client.download_invoices(&["abc123", "def456"]).await?;
|
||||
```
|
||||
|
||||
Reports return their raw export body (CSV for most of them), and the
|
||||
schemaless endpoints decode into any type you choose:
|
||||
Documents are attached through the record they belong to, since that is the
|
||||
only way the API creates one. Sixteen resources support it, as
|
||||
`upload_client`, `upload_invoice`, `upload_expense`, and so on:
|
||||
|
||||
```rust
|
||||
use invoiceninjars::FileUpload;
|
||||
|
||||
let receipt = FileUpload::new("receipt.pdf", std::fs::read("receipt.pdf")?)
|
||||
.mime("application/pdf");
|
||||
|
||||
// `Some(true)` makes the document visible to the client in the portal.
|
||||
let expense = client.upload_expense("abc123", vec![receipt], Some(false)).await?;
|
||||
```
|
||||
|
||||
Files are held in memory while uploading, and an upload is never retried,
|
||||
since a replay after a lost response would attach the file twice.
|
||||
|
||||
Reports return their raw export body (CSV for most of them). `statics`,
|
||||
`search`, and `health_check` return typed results; `chart` has no common
|
||||
response shape, so it decodes into any type you choose:
|
||||
|
||||
```rust
|
||||
use serde_json::{json, Value};
|
||||
|
||||
let csv: Vec<u8> = client.report("invoices", &json!({"date_range": "this_year"})).await?;
|
||||
let totals: Value = client.chart("totals_v2", &json!({})).await?;
|
||||
let statics: Value = client.statics().await?;
|
||||
let statics = client.statics().await?;
|
||||
```
|
||||
|
||||
Add more resources by adding a model under `src/models/` and a
|
||||
|
||||
Reference in New Issue
Block a user