@colixsystems/assets-client
v0.4.0
Published
Typed, scoped client for the AppStudio Asset Manager (the /assets endpoints — assets/images). Used by widgets through the injected WidgetContext and the SDK useAsset() hook.
Readme
@colixsystems/assets-client
Typed, scoped client for the AppStudio Asset Manager — the /assets endpoints that serve a tenant's assets and images. It is a standalone fetch-based client you instantiate with createAssetsClient({ baseUrl, getToken, getTenantId }). It is the sibling of @colixsystems/datastore-client, @colixsystems/directory-client, and @colixsystems/payments-client, and shares the same factory contract.
This is the surface the SDK useAsset(id) hook resolves: an asset metadata row stamped with a guaranteed absolute url you can paste straight into an <img> / <Image>.
Folders, shares, and the end-user file archive live elsewhere. Anything under
/filestore/*— folders, shares, and the per-user file archive — belongs to@colixsystems/filestore-client, not this package. This client is the Asset Manager only.
Two surfaces, one package. This client serves two callers:
- External / server-side integrations instantiate it directly with an API key and call the methods below.
- The widget runtime — the Player and exported Expo app instantiate the same package and inject it into
WidgetContext.assets. Widgets never import this package; they call the SDKuseAsset()hook from@colixsystems/widget-sdk, which readsctx.assets. Both surfaces speak the identical snake_case REST contract.
Status
v0.4.0 — pre-publish. Not yet published to npm.
snake_case, no transform
The wire format is snake_case in both directions (REQ-GEN-09), and that is the client contract. This client does no camelCase↔snake_case transform:
- Request bodies (multipart form fields) are sent snake_case verbatim (e.g.
description,tags). - Response rows are returned snake_case verbatim (e.g.
{ id, stored_filename, mime_type }). - Query params are snake_case (
type,tag,q,mime_type,limit,offset).
The only camelCase is the JS method names (get, list, upload) and the factory option names.
List endpoints return the { data, meta } envelope (meta = { total, limit, offset }) verbatim — list methods do not unwrap to a bare array.
Public API
import {
createAssetsClient,
AssetsError,
NotFoundError,
ForbiddenError,
ValidationError,
RateLimitedError,
ServerError,
} from "@colixsystems/assets-client";
const client = createAssetsClient({
baseUrl: "https://api.appstudio.io/api/v1",
getToken: () => "Bearer ...",
getTenantId: () => "tenant_abc",
// Optional: extra headers merged per request (e.g. per-widget scope tokens).
getRequestHeaders: ({ namespace, operation }) => ({
"X-Widget-Scopes": "...",
}),
// fetchImpl defaults to globalThis.fetch
});
// Resolve a file id to a row with a guaranteed absolute `url`.
// This is exactly what the SDK `useAsset(id)` hook calls — the host injects
// this client as `ctx.assets`, so `useAsset()` calls `ctx.assets.get(id)`.
const file = await client.get("file_123");
// file.url is absolute — paste straight into <img src> / <Image source>.
// List assets (all filters optional). The backend clamps limit/offset.
const { data, meta } = await client.list({
type: "image",
tag: "hero",
q: "logo",
limit: 50,
offset: 0,
});
// Multipart upload — pass a FormData verbatim (no transform, no JSON header).
// The file part MUST use the field name "file".
const fd = new FormData();
fd.append("file", blob, "photo.png");
fd.append("tags", "hero");
fd.append("description", "Landing hero image");
const uploaded = await client.upload(fd);URL resolution
get(id) stamps an absolute url on the returned row, resolved exactly like the web host facade (frontend/src/services/widgetHostFiles.js + frontend/src/utils/assets.js):
- If the payload's
urlis already absolute (https?://…), use it. - Otherwise, if a relative
urlor astored_filenameis present, glue it onto the origin ofbaseUrl(/uploads/<stored_filename>). - If neither resolves,
urlis left as the payload provided.
Surface
| Method | HTTP | Returns |
| --- | --- | --- |
| get(id) | GET /assets/{id} | Asset (with absolute url) |
| list(query?) | GET /assets | Page<Asset> |
| upload(formData) | POST /assets | Asset |
All three are top-level so the host can inject this client as ctx.assets and the SDK useAsset() hook calls ctx.assets.get(id) cleanly. The widget runtime strictly needs only get; list and upload cover the rest of the Asset Manager surface (Gallery / FormInput / FormBuilder widgets).
Transport
| Concern | Behaviour |
| --- | --- |
| Auth header | getToken() value; Bearer prefix added if missing |
| Tenant header | x-tenant-id from getTenantId() |
| Extra headers | merged from optional getRequestHeaders({ namespace, operation }) per request |
| Retries | idempotent GETs retried 3× with exponential backoff (200/400/800 ms) |
| Timeouts | 10 s default, configurable per call |
| Error model | typed AssetsError hierarchy (NotFound/Forbidden/Validation/RateLimited/Server) |
| Platform | browser and React Native (uses fetch) |
Dependencies
None. The client uses only platform fetch, FormData, and AbortController, all available in modern browsers, Node 18+, and React Native.
