Repository navigation
feat!: pluggable storage backends for 5.0 - #132
Merged
Merged
Conversation
Adds ADR 0004 (storage backends replace fileHandler and roCrateHandler) and a "How it is stored" glossary section (Storage backend, Storage key, Delivery), and rewords ADR 0002 to refer to storage backends. Refs #121
The search backends now sit under a grouped subpath beside storage. Export names are unchanged. BREAKING CHANGE: `arocapi/opensearch` and `arocapi/postgres` are removed with no aliases. Import `OpenSearchBackend` from `arocapi/search/opensearch` and `PostgresBackend` from `arocapi/search/postgres` instead.
The required `storage` option takes a storage backend, `{ metadata, content }`.
Each half makes one call per request, shared by HEAD and GET, and returns a
`stream`, `path`, `redirect` or (Metadata only) `json` delivery, or `null` for a
404. `content` receives the file's entity with its `meta` and the computed
`contentDisposition`. HEAD on a redirect now answers 302.
`DirectoryStorage({ root, locate? })`, from `arocapi/storage/directory`, serves
both halves from below `root`, reading `meta.storagePath` relative to it.
Absolute and `..` keys are refused.
BREAKING CHANGE: `fileHandler` and `roCrateHandler` are replaced by the
required `storage` option: `fileHandler` becomes `storage.content` and
`roCrateHandler` becomes `storage.metadata`. Each half is a single function
replacing the separate `head` and `get`, and returns `null` where a handler
returned `false`. The `file` result is now a `path` delivery, and `accelPath`
(`X-Accel-Redirect`) is gone. The `FileHandler`, `RoCrateHandler`,
`GetFileHandler`, `HeadFileHandler`, `GetRoCrateHandler`, `HeadRoCrateHandler`,
`FileResult`, `FileRedirectResult`, `FileStreamResult`, `FilePathResult` and
`FileMetadata` exports are removed.
arocapi/storage/s3 exports S3Storage({ client, bucket, locate?, delivery?, expiresIn? }).
Content redirects to a presigned GetObject URL carrying File.mediaType and the request's
Content-Disposition, or streams through arocapi with delivery: 'stream'. Metadata always
streams, reading headers with HeadObject and fetching that version with If-Match.
@aws-sdk/client-s3 and @aws-sdk/s3-request-presigner are optional peer dependencies.
Tests run against VersityGW (test-s3) locally and in CI.
arocapi/storage/entity-meta exports EntityMetaMetadata({ key? }), a
Metadata half that delivers the JSON object at Entity.meta.rocrate (or
another key) as a json delivery. A missing key is a 404; a value that
isn't a JSON object throws naming the entity id and key.
It runs through the storage contract mixed with DirectoryStorage's
content half, which gains a keylessMetadata harness option.
arocapi/storage/ocfl exports the async OcflStorage({ root, objectRoot?, locate? }).
It reads ocfl_layout.json at startup, implements 000N-path-direct-storage-layout,
and throws on any other layout unless objectRoot is given. Each request loads the
object's inventory through @ocfl/ocfl-fs (an optional GPL-3.0 peer dependency)
and serves the head version's file as a path delivery with a strong ETag from
its inventory digest.
The OCFL fixtures are built by scripts/buildOcflFixtures.ts with node:fs and
node:crypto only, and checked in under src/test/fixtures/ocfl.
Compare DirectoryStorage, S3Storage and OcflStorage, set out where Metadata comes from, and the trade-offs of stream, path and redirect, including HEAD on a presigned redirect. Drop the nginx X-Accel-Redirect pattern.
…flStorage OcflStorage now maps object ids for 0002-flat-direct, 0003-hash-and-id-n-tuple and 0004-hashed-n-tuple storage roots from ocfl_layout.json and the extension config, without objectRoot. The fixture script builds a storage root for each layout with its own id mapping. Refs #129
… findings - One locator in src/storage/location.ts resolves meta keys or `locate` and checks Storage keys once; shared `inside`, `nullIfMissing` and `pathDelivery` helpers. - Metadata `stream`/`path` deliveries no longer carry `contentType`; the metadata route alone sends application/ld+json. - The delivery responder closes an opened body if headers fail, and refuses unknown delivery types. - A `null` meta value is a wrong shape (500) on every backend. - OCFL checks the logical path, not the object id, and refuses a manifest target that isn't a file; startup checks the root marker without listing the root. - The storage contract splits into Metadata and Content suites; faster S3, OCFL and route tests. - AWS SDK peers are optional and accept any v3. - Docs, ADR 0004 and CLAUDE.md match.
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
2 open issues
1 file not reviewed
pnpm-lock.yaml: Generated file
What changed in this PR
Introduces pluggable storage for arocapi 5.0, replacing separate file and RO-Crate handlers with a shared Metadata/Content delivery contract.
Changes:
- Adds storage contracts, delivery handling, and Directory, Entity-meta, S3 and OCFL support.
- Migrates examples and documentation to
storageand grouped search imports. - Adds storage test helpers, OCFL fixtures and S3 test infrastructure.
A root-containment issue was identified in DirectoryStorage: symlinks can expose files outside its configured root.
For brevity, table paths use these prefixes:
W=website/src/content/docsE=website/examplesF=src/test/fixtures/ocflP=F/path-direct/https_example.com/storageLONG=F/hash-and-id-n-tuple/f3/45/18/94/abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij-f3451894fc18f49bb7cb434c3d7a687bc6e2b82afc978649034abecd8f9ef19a27fee98f4b835469daef8b3f80d0437776caeee792e19d828c9ebcddc5ec9046
| File | Description |
|---|---|
| website/src/content/docs/strategies/search.mdx | Updated as part of this pull request. |
| website/src/content/docs/strategies/access.mdx | Updated as part of this pull request. |
| website/src/content/docs/reference/routes.mdx | Updated as part of this pull request. |
| website/src/content/docs/reference/errors.mdx | Updated as part of this pull request. |
| website/src/content/docs/reference/entity-meta.mdx | Updated as part of this pull request. |
| website/src/content/docs/introduction/what-is-arocapi.mdx | Updated as part of this pull request. |
| website/src/content/docs/introduction/pilars.mdx | Updated as part of this pull request. |
| website/src/content/docs/introduction/crate-works.mdx | Updated as part of this pull request. |
| website/src/content/docs/getting-started/first-api.mdx | Updated as part of this pull request. |
| website/src/content/docs/configuration/transformers.mdx | Updated as part of this pull request. |
| website/src/content/docs/configuration/search.mdx | Updated as part of this pull request. |
| website/src/content/docs/configuration/data-sources.mdx | Updated as part of this pull request. |
| website/src/content/docs/configuration/access.mdx | Updated as part of this pull request. |
| website/src/content/docs/concepts/record-metadata-content.mdx | Updated as part of this pull request. |
| website/package.json | Updated as part of this pull request. |
| website/examples/strategies/storage-signed-url.ts | Updated as part of this pull request. |
| website/examples/strategies/storage-s3-delivery.ts | Updated as part of this pull request. |
| website/examples/strategies/storage-ro-crate.ts | Updated as part of this pull request. |
| website/examples/strategies/storage-presigned.ts | Updated as part of this pull request. |
| website/examples/strategies/storage-mixing-ocfl.ts | Updated as part of this pull request. |
| website/examples/strategies/storage-local.ts | Updated as part of this pull request. |
| website/examples/strategies/storage-accel.ts | Updated as part of this pull request. |
| website/examples/reference/storage-backend-signed-redirect.ts | Updated as part of this pull request. |
| website/examples/reference/entity-meta-storage-path.ts | Updated as part of this pull request. |
| website/examples/introduction/what-is-arocapi-options.ts | Updated as part of this pull request. |
| website/examples/getting-started/src/search.ts | Updated as part of this pull request. |
| website/examples/getting-started/src/first-api-login.ts | Updated as part of this pull request. |
| website/examples/getting-started/src/first-api-index.ts | Updated as part of this pull request. |
| website/examples/getting-started/scripts/first-api-seed.ts | Updated as part of this pull request. |
| website/examples/configuration/storage-s3.ts | Updated as part of this pull request. |
| website/examples/configuration/storage-ocfl.ts | Updated as part of this pull request. |
| website/examples/configuration/storage-mixing.ts | Updated as part of this pull request. |
| website/examples/configuration/storage-entity-meta.ts | Updated as part of this pull request. |
| website/examples/configuration/storage-directory.ts | Updated as part of this pull request. |
| website/examples/configuration/search-postgres.ts | Updated as part of this pull request. |
| website/examples/configuration/search-fields.ts | Updated as part of this pull request. |
| website/examples/configuration/handlers-ro-crate.ts | Updated as part of this pull request. |
| website/examples/configuration/handlers-file.ts | Updated as part of this pull request. |
| test/consumer/src/search.ts | Updated as part of this pull request. |
| src/types/fileHandlers.ts | Updated as part of this pull request. |
| src/test/integration.setup.ts | Updated as part of this pull request. |
| src/test/helpers/storedItems.ts | Updated as part of this pull request. |
| src/test/helpers/storageApp.ts | Updated as part of this pull request. |
| src/test/helpers/s3.ts | Updated as part of this pull request. |
| src/test/helpers/rows.ts | Updated as part of this pull request. |
| src/test/helpers/getAndHead.ts | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/ocfl_layout.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/unparseable/__object__/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/unparseable/__object__/v1/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/unparseable/__object__/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/unparseable/__object__/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/unparseable/__object__/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/unparseable/__object__/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/unparseable/__object__/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/unparseable/__object__/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/item/__object__/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/item/__object__/v1/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/item/__object__/v1/content/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/item/__object__/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/item/__object__/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/item/__object__/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/escaping-manifest/__object__/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/escaping-manifest/__object__/v1/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/escaping-manifest/__object__/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/escaping-manifest/__object__/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/escaping-manifest/__object__/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/escaping-manifest/__object__/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/escaping-manifest/__object__/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/escaping-manifest/__object__/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/directory-manifest/__object__/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/directory-manifest/__object__/v1/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/directory-manifest/__object__/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/directory-manifest/__object__/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/directory-manifest/__object__/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/directory-manifest/__object__/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/directory-manifest/__object__/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/directory-manifest/__object__/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/v2/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/v2/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/v2/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/v1/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/v1/content/collection/item/removed.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/extensions/0005-mutable-head/head/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/extensions/0005-mutable-head/head/content/r1/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/extensions/0005-mutable-head/config.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/contract/__object__/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/bad-sidecar/__object__/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/bad-sidecar/__object__/v1/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/bad-sidecar/__object__/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/bad-sidecar/__object__/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/bad-sidecar/__object__/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/bad-sidecar/__object__/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/bad-sidecar/__object__/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/https_example.com/storage/bad-sidecar/__object__/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/path-direct/0=ocfl_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/ocfl_layout.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/ff/75/53/44/92/48/5e/ab/b3/9f/86/35/67/28/88/4e/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/ff/75/53/44/92/48/5e/ab/b3/9f/86/35/67/28/88/4e/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/ff/75/53/44/92/48/5e/ab/b3/9f/86/35/67/28/88/4e/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/ff/75/53/44/92/48/5e/ab/b3/9f/86/35/67/28/88/4e/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/ff/75/53/44/92/48/5e/ab/b3/9f/86/35/67/28/88/4e/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/ff/75/53/44/92/48/5e/ab/b3/9f/86/35/67/28/88/4e/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/ff/75/53/44/92/48/5e/ab/b3/9f/86/35/67/28/88/4e/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/extensions/0004-hashed-n-tuple-storage-layout/config.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/71/dd/e7/a0/dc/2d/bd/5c/74/4b/8b/7e/e4/7c/83/f0/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/71/dd/e7/a0/dc/2d/bd/5c/74/4b/8b/7e/e4/7c/83/f0/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/71/dd/e7/a0/dc/2d/bd/5c/74/4b/8b/7e/e4/7c/83/f0/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/71/dd/e7/a0/dc/2d/bd/5c/74/4b/8b/7e/e4/7c/83/f0/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/71/dd/e7/a0/dc/2d/bd/5c/74/4b/8b/7e/e4/7c/83/f0/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/71/dd/e7/a0/dc/2d/bd/5c/74/4b/8b/7e/e4/7c/83/f0/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/20/1a/a2/24/55/33/b3/9a/49/72/70/cc/51/92/88/e8/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/20/1a/a2/24/55/33/b3/9a/49/72/70/cc/51/92/88/e8/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/20/1a/a2/24/55/33/b3/9a/49/72/70/cc/51/92/88/e8/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/20/1a/a2/24/55/33/b3/9a/49/72/70/cc/51/92/88/e8/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/20/1a/a2/24/55/33/b3/9a/49/72/70/cc/51/92/88/e8/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/20/1a/a2/24/55/33/b3/9a/49/72/70/cc/51/92/88/e8/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/20/1a/a2/24/55/33/b3/9a/49/72/70/cc/51/92/88/e8/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/08/31/97/66/fb/6c/29/35/dd/17/5b/94/26/77/17/e0/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/08/31/97/66/fb/6c/29/35/dd/17/5b/94/26/77/17/e0/v1/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/08/31/97/66/fb/6c/29/35/dd/17/5b/94/26/77/17/e0/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/08/31/97/66/fb/6c/29/35/dd/17/5b/94/26/77/17/e0/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/08/31/97/66/fb/6c/29/35/dd/17/5b/94/26/77/17/e0/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/08/31/97/66/fb/6c/29/35/dd/17/5b/94/26/77/17/e0/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/08/31/97/66/fb/6c/29/35/dd/17/5b/94/26/77/17/e0/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/08/31/97/66/fb/6c/29/35/dd/17/5b/94/26/77/17/e0/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hashed-n-tuple/0=ocfl_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/ocfl_layout.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/f3/45/18/94/abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij-f3451894fc18f49bb7cb434c3d7a687bc6e2b82afc978649034abecd8f9ef19a27fee98f4b835469daef8b3f80d0437776caeee792e19d828c9ebcddc5ec9046/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/f3/45/18/94/abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij-f3451894fc18f49bb7cb434c3d7a687bc6e2b82afc978649034abecd8f9ef19a27fee98f4b835469daef8b3f80d0437776caeee792e19d828c9ebcddc5ec9046/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/f3/45/18/94/abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij-f3451894fc18f49bb7cb434c3d7a687bc6e2b82afc978649034abecd8f9ef19a27fee98f4b835469daef8b3f80d0437776caeee792e19d828c9ebcddc5ec9046/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/f3/45/18/94/abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij-f3451894fc18f49bb7cb434c3d7a687bc6e2b82afc978649034abecd8f9ef19a27fee98f4b835469daef8b3f80d0437776caeee792e19d828c9ebcddc5ec9046/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/f3/45/18/94/abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij-f3451894fc18f49bb7cb434c3d7a687bc6e2b82afc978649034abecd8f9ef19a27fee98f4b835469daef8b3f80d0437776caeee792e19d828c9ebcddc5ec9046/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/f3/45/18/94/abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij-f3451894fc18f49bb7cb434c3d7a687bc6e2b82afc978649034abecd8f9ef19a27fee98f4b835469daef8b3f80d0437776caeee792e19d828c9ebcddc5ec9046/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/extensions/0003-hash-and-id-n-tuple-storage-layout/config.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/d3/60/1f/87/object-01/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/d3/60/1f/87/object-01/v1/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/d3/60/1f/87/object-01/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/d3/60/1f/87/object-01/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/d3/60/1f/87/object-01/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/d3/60/1f/87/object-01/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/d3/60/1f/87/object-01/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/d3/60/1f/87/object-01/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/ab/a9/bf/5d/info%3afedora%2fObject%7e01%21%27%28%29%2a/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/ab/a9/bf/5d/info%3afedora%2fObject%7e01%21%27%28%29%2a/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/ab/a9/bf/5d/info%3afedora%2fObject%7e01%21%27%28%29%2a/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/ab/a9/bf/5d/info%3afedora%2fObject%7e01%21%27%28%29%2a/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/ab/a9/bf/5d/info%3afedora%2fObject%7e01%21%27%28%29%2a/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/ab/a9/bf/5d/info%3afedora%2fObject%7e01%21%27%28%29%2a/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/ab/a9/bf/5d/info%3afedora%2fObject%7e01%21%27%28%29%2a/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/3a/9f/56/a7/%2e%2eHor%2frib%3al%c3%a8-%24id/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/3a/9f/56/a7/%2e%2eHor%2frib%3al%c3%a8-%24id/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/3a/9f/56/a7/%2e%2eHor%2frib%3al%c3%a8-%24id/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/3a/9f/56/a7/%2e%2eHor%2frib%3al%c3%a8-%24id/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/3a/9f/56/a7/%2e%2eHor%2frib%3al%c3%a8-%24id/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/3a/9f/56/a7/%2e%2eHor%2frib%3al%c3%a8-%24id/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/3a/9f/56/a7/%2e%2eHor%2frib%3al%c3%a8-%24id/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/hash-and-id-n-tuple/0=ocfl_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/ocfl_layout.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/object-01/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/object-01/v1/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/object-01/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/object-01/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/object-01/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/object-01/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/object-01/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/object-01/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/0=ocfl_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/..hor_rib:lé-$id/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/..hor_rib:lé-$id/v1/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/..hor_rib:lé-$id/v1/content/collection/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/..hor_rib:lé-$id/v1/content/collection/item/notes.bin | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/..hor_rib:lé-$id/v1/content/collection/item/greeting.wav | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/..hor_rib:lé-$id/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/..hor_rib:lé-$id/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/flat-direct/..hor_rib:lé-$id/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/custom-layout/ocfl_layout.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/custom-layout/objects/custom/v1/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/custom-layout/objects/custom/v1/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/custom-layout/objects/custom/v1/content/ro-crate-metadata.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/custom-layout/objects/custom/inventory.json.sha512 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/custom-layout/objects/custom/inventory.json | Updated as part of this pull request. |
| src/test/fixtures/ocfl/custom-layout/objects/custom/0=ocfl_object_1.1 | Updated as part of this pull request. |
| src/test/fixtures/ocfl/custom-layout/0=ocfl_1.1 | Updated as part of this pull request. |
| src/storage/s3/s3.ts | Updated as part of this pull request. |
| src/storage/results.ts | Updated as part of this pull request. |
| src/storage/ocfl/ocflFs.ts | Updated as part of this pull request. |
| src/storage/location.ts | Updated as part of this pull request. |
| src/storage/entity-meta/entity-meta.ts | Updated as part of this pull request. |
| src/storage/entity-meta/entity-meta.test.ts | Updated as part of this pull request. |
| src/storage/directory/directory.ts | Updated as part of this pull request. |
| src/storage/directory/directory.test.ts | Updated as part of this pull request. |
| src/storage/deliver.ts | Updated as part of this pull request. |
| src/storage/backend.ts | Updated as part of this pull request. |
| src/routes/capabilities.test.ts | Updated as part of this pull request. |
| src/errorHandling.test.ts | Updated as part of this pull request. |
| src/app.test.ts | Updated as part of this pull request. |
| package.json | Updated as part of this pull request. |
| GLOSSARY.md | Updated as part of this pull request. |
| example/data/seed.ts | Updated as part of this pull request. |
| example/data/README.md | Updated as part of this pull request. |
| docs/adr/0003-pluggable-search-backends.md | Updated as part of this pull request. |
| docs/adr/0002-library-enforces-access.md | Updated as part of this pull request. |
| docker-compose.test.yml | Updated as part of this pull request. |
| CLAUDE.md | Updated as part of this pull request. |
| biome.jsonc | Updated as part of this pull request. |
| .github/workflows/test.yml | Updated as part of this pull request. |
Review effort: Balanced
This was referenced Oct 21, 2026
Closed
Closed
|
🎉 This PR is included in version 5.0.0-beta.7 🎉 The release is available on: Your semantic-release bot 📦🚀 |
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.

Summary
A consumer passes one Storage backend instead of two handlers:
await fastify.register(arocapi, { prisma, search, entityVisibilityFilter, searchVisibilityFilter, access, - fileHandler: { get, head }, - roCrateHandler: { get, head }, + storage: DirectoryStorage({ root: './data' }), + // or mix halves: + // storage: { metadata: EntityMetaMetadata(), content: (await OcflStorage({ root })).content }, }); -import { OpenSearchBackend } from 'arocapi/opensearch'; +import { OpenSearchBackend } from 'arocapi/search/opensearch';GET and HEAD make the same single call, after visibility and access:
sequenceDiagram participant Client participant Route as /file/:id, /entity/:id/metadata participant Half as storage.content / storage.metadata Client->>Route: GET or HEAD Route->>Route: load Record (visibility), enforce access Route->>Half: one call (file, entity with meta, contentDisposition) Half-->>Route: Delivery: stream | path | redirect | json (Metadata only) | null Route-->>Client: headers from storage, and bytes on GET only (302 for redirect, 404 for null, logged 500 for a throw)The decisions are in ADR 0004 and the glossary's "How it is stored" section. The docs cover Configuration → Storage (replacing Handlers), Reference → Storage backend, the Entity meta key table, Strategies → Storage and the tutorial.
Where this differs from the spec:
locateis one object,{ metadata?(entity), content?(file, entity) }.stream/pathdeliveries carry nocontentType, because the route always sendsapplication/ld+json.nullmetavalue is a wrong shape (500) everywhere; only a missing key is a 404.Evidence
Before: HEAD and GET were separate consumer functions, so they could disagree. raspbocrate's HEAD describes compact JSON while its GET sends it pretty-printed. Content-Length came from a possibly stale
File.size.After: a shared storage contract suite runs every reference backend through the full plugin. For Directory, S3 (redirect and stream) and OCFL it checks both halves; for
EntityMetaMetadata, mixed with Directory Content, it checks the Metadata half:pnpm test: 972 tests pass, with 100% coverage on lines, branches, functions and statements, against Postgres, OpenSearch and VersityGW.pnpm lint: biome, types, knip, the docs examples and the packed-tarball consumer check all pass. The consumer check proves thatarocapi/search/*andarocapi/storage/*resolve, thatarocapi/opensearchandarocapi/postgresno longer do, and that acontenthalf can't returnjson.pnpm --filter website buildpasses, including Mermaid parsing and internal link checks.Merge Danger
Door: one-way
Every push to
mainpublishes to npm underbeta(seeRELEASING.md). Because of thefeat!:footers, this merge publishes5.0.0-beta.1, and a published version can't be taken back. The code itself reverts in one step.Blast Radius: consumers
fileHandler/roCrateHandlerwithstorage, and change the search import paths. TheBREAKING CHANGE:footers give the mapping.latest(4.x) isn't affected until 5.0 stable.File.size. Metadata is alwaysapplication/ld+json. HEAD on a redirect answers 302./file/:idloads the entity's fullmeta. WithEntityMetaMetadatathat includes the stored crate.s3:ListBucket. This is documented.@ocfl/ocfl-fsworkarounds: isolated for Before 5.0 stable: adopt upstream @ocfl/ocfl-fs fixes #117 and reported upstream in Storage layouts: 0003 encapsulation directory doesn't follow the spec, and hashed layouts fromstorageLayout()can't map until storage is loaded Language-Research-Technology/ocfl-js#8.Closes #123, closes #124, closes #125, closes #126, closes #127, closes #128, closes #129, closes #130