@panneau/data 5.0.0-alpha.2 → 5.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -6
- package/dist/adapter.d.ts +12 -13
- package/dist/adapter.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/laravel/index.d.ts +34 -1
- package/dist/laravel/index.d.ts.map +1 -1
- package/dist/laravel/index.js +43 -10
- package/dist/laravel/index.js.map +1 -1
- package/dist/problem.d.ts +16 -0
- package/dist/problem.d.ts.map +1 -0
- package/dist/problem.js +32 -0
- package/dist/problem.js.map +1 -0
- package/dist/query/context.d.ts +5 -0
- package/dist/query/context.d.ts.map +1 -1
- package/dist/query/context.js +8 -1
- package/dist/query/context.js.map +1 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +2 -2
- package/dist/rest.d.ts.map +1 -1
- package/dist/rest.js +11 -52
- package/dist/rest.js.map +1 -1
- package/dist/uploads/index.d.ts +58 -0
- package/dist/uploads/index.d.ts.map +1 -0
- package/dist/uploads/index.js +61 -0
- package/dist/uploads/index.js.map +1 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -18,15 +18,16 @@ try {
|
|
|
18
18
|
}
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
| Export | Content
|
|
22
|
-
| ----------------------- |
|
|
23
|
-
| `@panneau/data` | The `DataAdapter` interface, `createRestAdapter()`,
|
|
24
|
-
| `@panneau/data/laravel` | `createLaravelAdapter()`: the REST adapter with credentials, the `X-XSRF-TOKEN` header from Laravel's cookie, and `419` as unauthorized
|
|
25
|
-
| `@panneau/data/
|
|
21
|
+
| Export | Content |
|
|
22
|
+
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| `@panneau/data` | The `DataAdapter` interface, `createRestAdapter()`, the errors (`ApiError`, `ValidationError`, `ConflictError`, `UnauthorizedError`, `ForbiddenError`, `NotFoundError`), `createApiError()` and `readProblem()` |
|
|
24
|
+
| `@panneau/data/laravel` | `createLaravelAdapter()`: the REST adapter with credentials, the `X-XSRF-TOKEN` header from Laravel's cookie, and `419` as unauthorized; `getLaravelHeaders()` and `createLaravelError()` for an app's own endpoints |
|
|
25
|
+
| `@panneau/data/uploads` | `createPresignedUploads()`: direct uploads to S3 with URLs that the backend signs, for Uppy 6's `@uppy/aws-s3` |
|
|
26
|
+
| `@panneau/data/query` | `DataAdapterProvider` and the TanStack Query hooks: `useDefinition`, `useResourceList`, `useResourceItem`, `useCreateItem`, `useUpdateItem`, `useDeleteItem`, `useRunAction`, `useActionRun` |
|
|
26
27
|
|
|
27
28
|
## Adapters
|
|
28
29
|
|
|
29
|
-
A `DataAdapter` is everything the engine and the app need from a backend: the definition, resources (list, get, create, update, delete), actions and their runs,
|
|
30
|
+
A `DataAdapter` is everything the engine and the app need from a backend: the definition, resources (list, get, create, update, delete), actions and their runs, the auth session, search and widget data. Media are items of resources of kind `media` ([0026](../../docs/decisions/0026-media-libraries.md)), through the resource methods. Methods reject with an `ApiError` when the backend answers with an error.
|
|
30
31
|
|
|
31
32
|
- **`createRestAdapter({ baseUrl, fetch, headers, credentials })`** implements the HTTP API of the protocol:
|
|
32
33
|
- `application/problem+json` errors become typed errors, by status: `401` `UnauthorizedError`, `403` `ForbiddenError`, `404` `NotFoundError`, `412` `ConflictError` (with `current`), `422` `ValidationError` (with `errors` keyed by dot path), others `ApiError`. Responses that are not problems get a problem built from their status;
|
|
@@ -36,6 +37,56 @@ A `DataAdapter` is everything the engine and the app need from a backend: the de
|
|
|
36
37
|
- **`createLaravelAdapter()`** is the preset for `laravel-panneau` with session authentication. Laravel specifics stay there, not in the protocol.
|
|
37
38
|
- **A custom adapter** implements `DataAdapter` over another API. A product that only needs a few resources can throw from the methods it doesn't support.
|
|
38
39
|
|
|
40
|
+
### An app's own endpoints
|
|
41
|
+
|
|
42
|
+
An app that uses Panneau usually has its own JSON endpoints too. The pieces of the adapters are exported, so that its client gets the same errors, and can pass a `ValidationError` to a Panneau form or handle `UnauthorizedError` in one place:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { readProblem } from '@panneau/data';
|
|
46
|
+
import { createLaravelError, getLaravelHeaders } from '@panneau/data/laravel';
|
|
47
|
+
|
|
48
|
+
const response = await fetch('/api/subscriptions', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
credentials: 'include',
|
|
51
|
+
headers: getLaravelHeaders({ headers: { 'Content-Type': 'application/json' } }),
|
|
52
|
+
body: JSON.stringify(values),
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw createLaravelError(await readProblem(response));
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `readProblem(response)` reads an `application/problem+json` body, or builds a problem from Laravel's `{ message, errors }` body or from the status. It reads a clone, so the response stays readable.
|
|
60
|
+
- `createApiError(problem)` maps a problem to its typed error by status; `createLaravelError(problem)` also treats `419` (an expired CSRF token or session) as `UnauthorizedError`.
|
|
61
|
+
- `getLaravelHeaders({ headers, xsrfCookie, xsrfHeader, cookies })` adds `X-Requested-With` and the CSRF token from Laravel's cookie.
|
|
62
|
+
|
|
63
|
+
### Direct uploads
|
|
64
|
+
|
|
65
|
+
Large files (videos of several gigabytes) go from the browser to S3-compatible storage, in parts, with URLs that the backend signs: the `presigned` mode of the protocol ([0025](../../docs/decisions/0025-presigned-uploads.md)). `createPresignedUploads()` gives Uppy 6 its `signRequest`, and completes each upload. An upload is not a media: values reference it as `{ upload: 'upl_…' }`, such as the `file` of a media.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import AwsS3 from '@uppy/aws-s3';
|
|
69
|
+
import Uppy from '@uppy/core';
|
|
70
|
+
|
|
71
|
+
import { createLaravelError, getLaravelHeaders } from '@panneau/data/laravel';
|
|
72
|
+
import { createPresignedUploads } from '@panneau/data/uploads';
|
|
73
|
+
|
|
74
|
+
const uploads = createPresignedUploads({
|
|
75
|
+
baseUrl: definition.api.baseUrl, // the routes are under {baseUrl}/uploads, or at uploads.endpoint
|
|
76
|
+
headers: () => getLaravelHeaders(),
|
|
77
|
+
createError: createLaravelError,
|
|
78
|
+
});
|
|
79
|
+
const uppy = new Uppy().use(AwsS3, { signRequest: uploads.signRequest });
|
|
80
|
+
uppy.on('upload-success', async (file, response) => {
|
|
81
|
+
const upload = await uploads.complete(response.body.key, { name: file.name });
|
|
82
|
+
const media = await adapter.create('media', { file: { upload: upload.id } }); // media.media.status: 'processing'
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
It is plain `fetch` and doesn't import Uppy. Without direct uploads, `create()` sends a file in `multipart/form-data`.
|
|
87
|
+
|
|
88
|
+
The REST adapter gives the same object, with its own base URL, headers, credentials and errors: `adapter.presignedUploads({ endpoint: definition.uploads.endpoint })` (optional in `DataAdapter`). The fields of `@panneau/fields` and `@panneau/media` upload through it with `createUppyUploadSource()` of `@panneau/fields/uppy`.
|
|
89
|
+
|
|
39
90
|
## Query hooks
|
|
40
91
|
|
|
41
92
|
The hooks need TanStack Query v5 and React (optional peer dependencies, only for this entry point), with a `QueryClientProvider` and a `DataAdapterProvider` above them.
|
|
@@ -60,6 +111,7 @@ function Pages() {
|
|
|
60
111
|
|
|
61
112
|
- Mutations update the cache: create and update cache the item and refetch the lists of the resource; delete removes the item and refetches the lists.
|
|
62
113
|
- `useRunAction()` refreshes what the action result asks (`refresh`: `none`, `items` or `list`, the default). A queued action returns a run instead: `useActionRun(run.id)` follows it until it finishes, then refreshes.
|
|
114
|
+
- `useDataAdapter()` gives the adapter of `DataAdapterProvider`, and throws without one. `useOptionalDataAdapter()` gives `null` instead, for components that also work without a backend, such as the media field.
|
|
63
115
|
- `queryKeys` and the `*QueryOptions()` functions give the keys and options of the hooks, to prefetch or invalidate their data.
|
|
64
116
|
|
|
65
117
|
The package layout, scripts and API reports are explained in [`docs/packages.md`](../../docs/packages.md).
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ActionRequest, ActionResult, ActionRun, Definition, Item, ListQuery, ListResponse,
|
|
1
|
+
import type { ActionRequest, ActionResult, ActionRun, Definition, Item, ListQuery, ListResponse, SearchResponse, Session } from '@panneau/protocol';
|
|
2
|
+
import type { PresignedUploads } from './uploads';
|
|
2
3
|
/** Values of an item's fields, keyed by field name. */
|
|
3
4
|
export type Values = Record<string, unknown>;
|
|
4
5
|
export interface RequestOptions {
|
|
@@ -20,8 +21,6 @@ export type ActionResponse = {
|
|
|
20
21
|
run: ActionRun;
|
|
21
22
|
result?: undefined;
|
|
22
23
|
};
|
|
23
|
-
/** Query of the media list: a list query whose filters include `type` and `tags`. */
|
|
24
|
-
export type MediaQuery = ListQuery;
|
|
25
24
|
/** Credentials of a password login. */
|
|
26
25
|
export interface LoginCredentials {
|
|
27
26
|
email: string;
|
|
@@ -60,16 +59,16 @@ export interface DataAdapter {
|
|
|
60
59
|
listActionRuns(options?: RequestOptions): Promise<ActionRun[]>;
|
|
61
60
|
/** Cancels a run that hasn't finished. */
|
|
62
61
|
cancelActionRun(id: string, options?: RequestOptions): Promise<void>;
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Direct uploads to storage through this adapter, with its headers, cookies and errors, when
|
|
64
|
+
* the definition announces `"uploads": { "mode": "presigned" }`. `endpoint` is the definition's
|
|
65
|
+
* `uploads.endpoint`, when the routes are not under the API. Optional: an adapter without it
|
|
66
|
+
* sends files in multipart requests only. Media are items of resources of kind `media`
|
|
67
|
+
* (decision 0026): `create()` makes one from `{ file: { upload } }`.
|
|
68
|
+
*/
|
|
69
|
+
presignedUploads?(options?: {
|
|
70
|
+
endpoint?: string;
|
|
71
|
+
}): PresignedUploads;
|
|
73
72
|
/** The current session. Rejects with an `UnauthorizedError` without one. */
|
|
74
73
|
getSession(options?: RequestOptions): Promise<Session>;
|
|
75
74
|
/** Logs in with a password. */
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,aAAa,EACb,YAAY,EACZ,SAAS,EACT,UAAU,EACV,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export type { ActionResponse, DataAdapter, LoginCredentials,
|
|
1
|
+
export type { ActionResponse, DataAdapter, LoginCredentials, RequestOptions, UpdateOptions, Values, } from './adapter';
|
|
2
2
|
export { ApiError, ConflictError, ForbiddenError, InvalidResponseError, NotFoundError, UnauthorizedError, ValidationError, createApiError, isApiError, } from './errors';
|
|
3
|
+
export { readProblem } from './problem';
|
|
3
4
|
export { type RestAdapterOptions, createRestAdapter } from './rest';
|
|
5
|
+
export type { PresignableRequest, PresignedResponse, PresignedUploads } from './uploads';
|
|
4
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACR,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACR,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,GACT,MAAM,WAAW,CAAC;AACnB,OAAO,EACH,QAAQ,EACR,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,UAAU,GACb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,KAAK,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AACpE,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { ApiError, ConflictError, ForbiddenError, InvalidResponseError, NotFoundError, UnauthorizedError, ValidationError, createApiError, isApiError } from "./errors.js";
|
|
2
|
+
import { readProblem } from "./problem.js";
|
|
2
3
|
import { createRestAdapter } from "./rest.js";
|
|
3
|
-
export { ApiError, ConflictError, ForbiddenError, InvalidResponseError, NotFoundError, UnauthorizedError, ValidationError, createApiError, createRestAdapter, isApiError };
|
|
4
|
+
export { ApiError, ConflictError, ForbiddenError, InvalidResponseError, NotFoundError, UnauthorizedError, ValidationError, createApiError, createRestAdapter, isApiError, readProblem };
|
package/dist/laravel/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type { Problem } from '@panneau/protocol';
|
|
1
2
|
import type { DataAdapter } from '../adapter';
|
|
3
|
+
import { type ApiError } from '../errors';
|
|
2
4
|
import { type RestAdapterOptions } from '../rest';
|
|
3
|
-
|
|
5
|
+
/** How to send Laravel's CSRF token. */
|
|
6
|
+
export interface LaravelSessionOptions {
|
|
4
7
|
/** Name of the cookie that holds the CSRF token. Defaults to Laravel's `XSRF-TOKEN`. */
|
|
5
8
|
xsrfCookie?: string;
|
|
6
9
|
/** Header that sends the CSRF token. Defaults to `X-XSRF-TOKEN`. */
|
|
@@ -8,6 +11,36 @@ export interface LaravelAdapterOptions extends RestAdapterOptions {
|
|
|
8
11
|
/** Reads the cookies. Defaults to `document.cookie`, or no cookies outside a browser. */
|
|
9
12
|
cookies?: () => string;
|
|
10
13
|
}
|
|
14
|
+
export interface LaravelAdapterOptions extends RestAdapterOptions, LaravelSessionOptions {
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The headers of a request to a Laravel app with session authentication: `X-Requested-With`, so
|
|
18
|
+
* that Laravel answers errors as JSON, and the CSRF token from the `XSRF-TOKEN` cookie. Use them,
|
|
19
|
+
* with `credentials: 'include'`, for an app's own endpoints next to Panneau's.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* fetch('/api/subscriptions', {
|
|
23
|
+
* method: 'POST',
|
|
24
|
+
* credentials: 'include',
|
|
25
|
+
* headers: getLaravelHeaders({ headers: { 'Content-Type': 'application/json' } }),
|
|
26
|
+
* body: JSON.stringify(values),
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function getLaravelHeaders({ headers, xsrfCookie, xsrfHeader, cookies, }?: LaravelSessionOptions & {
|
|
31
|
+
headers?: HeadersInit;
|
|
32
|
+
}): Headers;
|
|
33
|
+
/**
|
|
34
|
+
* The typed error of a failed response of a Laravel app: `createApiError()`, with `419` (an
|
|
35
|
+
* expired CSRF token or session) as `UnauthorizedError`, like `401`.
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* if (!response.ok) {
|
|
39
|
+
* throw createLaravelError(await readProblem(response));
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function createLaravelError(problem: Problem): ApiError;
|
|
11
44
|
/**
|
|
12
45
|
* The REST adapter set up for `laravel-panneau` with Laravel's session authentication: it sends
|
|
13
46
|
* cookies, adds the CSRF token from the `XSRF-TOKEN` cookie to every request, and treats `419`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/laravel/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/laravel/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,KAAK,QAAQ,EAAqC,MAAM,WAAW,CAAC;AAC7E,OAAO,EAAE,KAAK,kBAAkB,EAAqB,MAAM,SAAS,CAAC;AAErE,wCAAwC;AACxC,MAAM,WAAW,qBAAqB;IAClC,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB,EAAE,qBAAqB;CAAG;AAgB3F;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,OAAO,EACP,UAAyB,EACzB,UAA2B,EAC3B,OAAwE,GAC3E,GAAE,qBAAqB,GAAG;IAAE,OAAO,CAAC,EAAE,WAAW,CAAA;CAAO,GAAG,OAAO,CAQlE;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CAE7D;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,UAAyB,EACzB,UAA2B,EAC3B,OAAwE,EACxE,OAAO,EACP,WAAuB,EACvB,WAAW,EACX,GAAG,OAAO,EACb,EAAE,qBAAqB,GAAG,WAAW,CAerC"}
|
package/dist/laravel/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UnauthorizedError } from "../errors.js";
|
|
1
|
+
import { UnauthorizedError, createApiError } from "../errors.js";
|
|
2
2
|
import { createRestAdapter } from "../rest.js";
|
|
3
3
|
//#region src/laravel/index.ts
|
|
4
4
|
function readCookie(cookies, name) {
|
|
@@ -13,6 +13,40 @@ function readCookie(cookies, name) {
|
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
+
* The headers of a request to a Laravel app with session authentication: `X-Requested-With`, so
|
|
17
|
+
* that Laravel answers errors as JSON, and the CSRF token from the `XSRF-TOKEN` cookie. Use them,
|
|
18
|
+
* with `credentials: 'include'`, for an app's own endpoints next to Panneau's.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* fetch('/api/subscriptions', {
|
|
22
|
+
* method: 'POST',
|
|
23
|
+
* credentials: 'include',
|
|
24
|
+
* headers: getLaravelHeaders({ headers: { 'Content-Type': 'application/json' } }),
|
|
25
|
+
* body: JSON.stringify(values),
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function getLaravelHeaders({ headers, xsrfCookie = "XSRF-TOKEN", xsrfHeader = "X-XSRF-TOKEN", cookies = () => typeof document === "undefined" ? "" : document.cookie } = {}) {
|
|
30
|
+
const result = new Headers(headers);
|
|
31
|
+
result.set("X-Requested-With", "XMLHttpRequest");
|
|
32
|
+
const token = readCookie(cookies(), xsrfCookie);
|
|
33
|
+
if (token !== null) result.set(xsrfHeader, token);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The typed error of a failed response of a Laravel app: `createApiError()`, with `419` (an
|
|
38
|
+
* expired CSRF token or session) as `UnauthorizedError`, like `401`.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* if (!response.ok) {
|
|
42
|
+
* throw createLaravelError(await readProblem(response));
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
function createLaravelError(problem) {
|
|
47
|
+
return problem.status === 419 ? new UnauthorizedError(problem) : createApiError(problem);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
16
50
|
* The REST adapter set up for `laravel-panneau` with Laravel's session authentication: it sends
|
|
17
51
|
* cookies, adds the CSRF token from the `XSRF-TOKEN` cookie to every request, and treats `419`
|
|
18
52
|
* (expired CSRF token or session) as unauthorized.
|
|
@@ -21,17 +55,16 @@ function createLaravelAdapter({ xsrfCookie = "XSRF-TOKEN", xsrfHeader = "X-XSRF-
|
|
|
21
55
|
return createRestAdapter({
|
|
22
56
|
...options,
|
|
23
57
|
credentials,
|
|
24
|
-
headers: async () => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
createError: (problem, response) => createError?.(problem, response) ?? (problem.status === 419 ? new UnauthorizedError(problem) : void 0)
|
|
58
|
+
headers: async () => getLaravelHeaders({
|
|
59
|
+
headers: typeof headers === "function" ? await headers() : headers,
|
|
60
|
+
xsrfCookie,
|
|
61
|
+
xsrfHeader,
|
|
62
|
+
cookies
|
|
63
|
+
}),
|
|
64
|
+
createError: (problem, response) => createError?.(problem, response) ?? (problem.status === 419 ? createLaravelError(problem) : void 0)
|
|
32
65
|
});
|
|
33
66
|
}
|
|
34
67
|
//#endregion
|
|
35
|
-
export { createLaravelAdapter };
|
|
68
|
+
export { createLaravelAdapter, createLaravelError, getLaravelHeaders };
|
|
36
69
|
|
|
37
70
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/laravel/index.ts"],"sourcesContent":["import type { DataAdapter } from '../adapter';\nimport { UnauthorizedError } from '../errors';\nimport { type RestAdapterOptions, createRestAdapter } from '../rest';\n\nexport interface
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/laravel/index.ts"],"sourcesContent":["import type { Problem } from '@panneau/protocol';\n\nimport type { DataAdapter } from '../adapter';\nimport { type ApiError, UnauthorizedError, createApiError } from '../errors';\nimport { type RestAdapterOptions, createRestAdapter } from '../rest';\n\n/** How to send Laravel's CSRF token. */\nexport interface LaravelSessionOptions {\n /** Name of the cookie that holds the CSRF token. Defaults to Laravel's `XSRF-TOKEN`. */\n xsrfCookie?: string;\n /** Header that sends the CSRF token. Defaults to `X-XSRF-TOKEN`. */\n xsrfHeader?: string;\n /** Reads the cookies. Defaults to `document.cookie`, or no cookies outside a browser. */\n cookies?: () => string;\n}\n\nexport interface LaravelAdapterOptions extends RestAdapterOptions, LaravelSessionOptions {}\n\nfunction readCookie(cookies: string, name: string): string | null {\n for (const part of cookies.split(';')) {\n const [key, ...value] = part.trim().split('=');\n if (key === name) {\n try {\n return decodeURIComponent(value.join('='));\n } catch {\n return value.join('=');\n }\n }\n }\n return null;\n}\n\n/**\n * The headers of a request to a Laravel app with session authentication: `X-Requested-With`, so\n * that Laravel answers errors as JSON, and the CSRF token from the `XSRF-TOKEN` cookie. Use them,\n * with `credentials: 'include'`, for an app's own endpoints next to Panneau's.\n *\n * ```ts\n * fetch('/api/subscriptions', {\n * method: 'POST',\n * credentials: 'include',\n * headers: getLaravelHeaders({ headers: { 'Content-Type': 'application/json' } }),\n * body: JSON.stringify(values),\n * });\n * ```\n */\nexport function getLaravelHeaders({\n headers,\n xsrfCookie = 'XSRF-TOKEN',\n xsrfHeader = 'X-XSRF-TOKEN',\n cookies = () => (typeof document === 'undefined' ? '' : document.cookie),\n}: LaravelSessionOptions & { headers?: HeadersInit } = {}): Headers {\n const result = new Headers(headers);\n result.set('X-Requested-With', 'XMLHttpRequest');\n const token = readCookie(cookies(), xsrfCookie);\n if (token !== null) {\n result.set(xsrfHeader, token);\n }\n return result;\n}\n\n/**\n * The typed error of a failed response of a Laravel app: `createApiError()`, with `419` (an\n * expired CSRF token or session) as `UnauthorizedError`, like `401`.\n *\n * ```ts\n * if (!response.ok) {\n * throw createLaravelError(await readProblem(response));\n * }\n * ```\n */\nexport function createLaravelError(problem: Problem): ApiError {\n return problem.status === 419 ? new UnauthorizedError(problem) : createApiError(problem);\n}\n\n/**\n * The REST adapter set up for `laravel-panneau` with Laravel's session authentication: it sends\n * cookies, adds the CSRF token from the `XSRF-TOKEN` cookie to every request, and treats `419`\n * (expired CSRF token or session) as unauthorized.\n */\nexport function createLaravelAdapter({\n xsrfCookie = 'XSRF-TOKEN',\n xsrfHeader = 'X-XSRF-TOKEN',\n cookies = () => (typeof document === 'undefined' ? '' : document.cookie),\n headers,\n credentials = 'include',\n createError,\n ...options\n}: LaravelAdapterOptions): DataAdapter {\n return createRestAdapter({\n ...options,\n credentials,\n headers: async () =>\n getLaravelHeaders({\n headers: typeof headers === 'function' ? await headers() : headers,\n xsrfCookie,\n xsrfHeader,\n cookies,\n }),\n createError: (problem, response) =>\n createError?.(problem, response) ??\n (problem.status === 419 ? createLaravelError(problem) : undefined),\n });\n}\n"],"mappings":";;;AAkBA,SAAS,WAAW,SAAiB,MAA6B;CAC9D,KAAK,MAAM,QAAQ,QAAQ,MAAM,GAAG,GAAG;EACnC,MAAM,CAAC,KAAK,GAAG,SAAS,KAAK,KAAK,CAAC,CAAC,MAAM,GAAG;EAC7C,IAAI,QAAQ,MACR,IAAI;GACA,OAAO,mBAAmB,MAAM,KAAK,GAAG,CAAC;EAC7C,QAAQ;GACJ,OAAO,MAAM,KAAK,GAAG;EACzB;CAER;CACA,OAAO;AACX;;;;;;;;;;;;;;;AAgBA,SAAgB,kBAAkB,EAC9B,SACA,aAAa,cACb,aAAa,gBACb,gBAAiB,OAAO,aAAa,cAAc,KAAK,SAAS,WACd,CAAC,GAAY;CAChE,MAAM,SAAS,IAAI,QAAQ,OAAO;CAClC,OAAO,IAAI,oBAAoB,gBAAgB;CAC/C,MAAM,QAAQ,WAAW,QAAQ,GAAG,UAAU;CAC9C,IAAI,UAAU,MACV,OAAO,IAAI,YAAY,KAAK;CAEhC,OAAO;AACX;;;;;;;;;;;AAYA,SAAgB,mBAAmB,SAA4B;CAC3D,OAAO,QAAQ,WAAW,MAAM,IAAI,kBAAkB,OAAO,IAAI,eAAe,OAAO;AAC3F;;;;;;AAOA,SAAgB,qBAAqB,EACjC,aAAa,cACb,aAAa,gBACb,gBAAiB,OAAO,aAAa,cAAc,KAAK,SAAS,QACjE,SACA,cAAc,WACd,aACA,GAAG,WACgC;CACnC,OAAO,kBAAkB;EACrB,GAAG;EACH;EACA,SAAS,YACL,kBAAkB;GACd,SAAS,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAI;GAC3D;GACA;GACA;EACJ,CAAC;EACL,cAAc,SAAS,aACnB,cAAc,SAAS,QAAQ,MAC9B,QAAQ,WAAW,MAAM,mBAAmB,OAAO,IAAI,KAAA;CAChE,CAAC;AACL"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Problem } from '@panneau/protocol';
|
|
2
|
+
/**
|
|
3
|
+
* The problem of a failed response: its body when it is one (`application/problem+json`), or a
|
|
4
|
+
* problem built from Laravel's `{ message, errors }` body, or from the status alone. Pass it to
|
|
5
|
+
* `createApiError()` to get the typed error (`ValidationError`, `UnauthorizedError`…) that Panneau
|
|
6
|
+
* forms and the app handle, for an app's own endpoints too.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const response = await fetch('/api/subscriptions', { method: 'POST', body });
|
|
10
|
+
* if (!response.ok) {
|
|
11
|
+
* throw createApiError(await readProblem(response));
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function readProblem(response: Response): Promise<Problem>;
|
|
16
|
+
//# sourceMappingURL=problem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"problem.d.ts","sourceRoot":"","sources":["../src/problem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAGjD;;;;;;;;;;;;GAYG;AACH,wBAAsB,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CA8BtE"}
|
package/dist/problem.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isProblem } from "@panneau/protocol/runtime";
|
|
2
|
+
//#region src/problem.ts
|
|
3
|
+
/**
|
|
4
|
+
* The problem of a failed response: its body when it is one (`application/problem+json`), or a
|
|
5
|
+
* problem built from Laravel's `{ message, errors }` body, or from the status alone. Pass it to
|
|
6
|
+
* `createApiError()` to get the typed error (`ValidationError`, `UnauthorizedError`…) that Panneau
|
|
7
|
+
* forms and the app handle, for an app's own endpoints too.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const response = await fetch('/api/subscriptions', { method: 'POST', body });
|
|
11
|
+
* if (!response.ok) {
|
|
12
|
+
* throw createApiError(await readProblem(response));
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
async function readProblem(response) {
|
|
17
|
+
const body = await response.clone().json().catch(() => null);
|
|
18
|
+
if (isProblem(body)) return body;
|
|
19
|
+
const record = body !== null && typeof body === "object" ? body : {};
|
|
20
|
+
const problem = {
|
|
21
|
+
type: "about:blank",
|
|
22
|
+
title: typeof record.title === "string" ? record.title : typeof record.message === "string" ? record.message : response.statusText || `HTTP ${response.status}`,
|
|
23
|
+
status: response.status
|
|
24
|
+
};
|
|
25
|
+
const errors = record.errors;
|
|
26
|
+
if (errors !== null && typeof errors === "object" && !Array.isArray(errors)) problem.errors = Object.fromEntries(Object.entries(errors).map(([path, messages]) => [path, (Array.isArray(messages) ? messages : [messages]).map(String)]));
|
|
27
|
+
return problem;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { readProblem };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=problem.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"problem.js","names":[],"sources":["../src/problem.ts"],"sourcesContent":["import type { Problem } from '@panneau/protocol';\nimport { isProblem } from '@panneau/protocol/runtime';\n\n/**\n * The problem of a failed response: its body when it is one (`application/problem+json`), or a\n * problem built from Laravel's `{ message, errors }` body, or from the status alone. Pass it to\n * `createApiError()` to get the typed error (`ValidationError`, `UnauthorizedError`…) that Panneau\n * forms and the app handle, for an app's own endpoints too.\n *\n * ```ts\n * const response = await fetch('/api/subscriptions', { method: 'POST', body });\n * if (!response.ok) {\n * throw createApiError(await readProblem(response));\n * }\n * ```\n */\nexport async function readProblem(response: Response): Promise<Problem> {\n const body: unknown = await response\n .clone()\n .json()\n .catch(() => null);\n if (isProblem(body)) {\n return body;\n }\n const record =\n body !== null && typeof body === 'object' ? (body as Record<string, unknown>) : {};\n const problem: Problem = {\n type: 'about:blank',\n title:\n typeof record.title === 'string'\n ? record.title\n : typeof record.message === 'string'\n ? record.message\n : response.statusText || `HTTP ${response.status}`,\n status: response.status,\n };\n const errors = record.errors;\n if (errors !== null && typeof errors === 'object' && !Array.isArray(errors)) {\n problem.errors = Object.fromEntries(\n Object.entries(errors).map(([path, messages]) => [\n path,\n (Array.isArray(messages) ? messages : [messages]).map(String),\n ]),\n );\n }\n return problem;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,eAAsB,YAAY,UAAsC;CACpE,MAAM,OAAgB,MAAM,SACvB,MAAM,CAAC,CACP,KAAK,CAAC,CACN,YAAY,IAAI;CACrB,IAAI,UAAU,IAAI,GACd,OAAO;CAEX,MAAM,SACF,SAAS,QAAQ,OAAO,SAAS,WAAY,OAAmC,CAAC;CACrF,MAAM,UAAmB;EACrB,MAAM;EACN,OACI,OAAO,OAAO,UAAU,WAClB,OAAO,QACP,OAAO,OAAO,YAAY,WACxB,OAAO,UACP,SAAS,cAAc,QAAQ,SAAS;EACpD,QAAQ,SAAS;CACrB;CACA,MAAM,SAAS,OAAO;CACtB,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,GACtE,QAAQ,SAAS,OAAO,YACpB,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,CAAC,MAAM,cAAc,CAC7C,OACC,MAAM,QAAQ,QAAQ,IAAI,WAAW,CAAC,QAAQ,EAAA,CAAG,IAAI,MAAM,CAAC,CAChE,CACL;CAEJ,OAAO;AACX"}
|
package/dist/query/context.d.ts
CHANGED
|
@@ -12,4 +12,9 @@ export interface DataAdapterProviderProps {
|
|
|
12
12
|
export declare function DataAdapterProvider({ adapter, children }: DataAdapterProviderProps): import("react").JSX.Element;
|
|
13
13
|
/** The adapter of the closest `DataAdapterProvider`. */
|
|
14
14
|
export declare function useDataAdapter(): DataAdapter;
|
|
15
|
+
/**
|
|
16
|
+
* The adapter of the closest `DataAdapterProvider`, or `null` without one: for components that
|
|
17
|
+
* also work without a backend, such as fields that can upload.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useOptionalDataAdapter(): DataAdapter | null;
|
|
15
20
|
//# sourceMappingURL=context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/query/context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAsB,MAAM,OAAO,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI9C,MAAM,WAAW,wBAAwB;IACrC,2FAA2F;IAC3F,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,wBAAwB,+BAElF;AAED,wDAAwD;AACxD,wBAAgB,cAAc,IAAI,WAAW,CAM5C"}
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/query/context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAsB,MAAM,OAAO,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI9C,MAAM,WAAW,wBAAwB;IACrC,2FAA2F;IAC3F,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,wBAAwB,+BAElF;AAED,wDAAwD;AACxD,wBAAgB,cAAc,IAAI,WAAW,CAM5C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,WAAW,GAAG,IAAI,CAE3D"}
|
package/dist/query/context.js
CHANGED
|
@@ -28,7 +28,14 @@ function useDataAdapter() {
|
|
|
28
28
|
if (adapter === null) throw new Error("useDataAdapter() needs a <DataAdapterProvider> above it.");
|
|
29
29
|
return adapter;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The adapter of the closest `DataAdapterProvider`, or `null` without one: for components that
|
|
33
|
+
* also work without a backend, such as fields that can upload.
|
|
34
|
+
*/
|
|
35
|
+
function useOptionalDataAdapter() {
|
|
36
|
+
return use(DataAdapterContext);
|
|
37
|
+
}
|
|
31
38
|
//#endregion
|
|
32
|
-
export { DataAdapterProvider, useDataAdapter };
|
|
39
|
+
export { DataAdapterProvider, useDataAdapter, useOptionalDataAdapter };
|
|
33
40
|
|
|
34
41
|
//# sourceMappingURL=context.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","names":["adapter","children","_c"],"sources":["../../src/query/context.tsx"],"sourcesContent":["import { type ReactNode, createContext, use } from 'react';\n\nimport type { DataAdapter } from '../adapter';\n\nconst DataAdapterContext = createContext<DataAdapter | null>(null);\n\nexport interface DataAdapterProviderProps {\n /** The adapter of the hooks below: `createRestAdapter()`, a preset or a custom adapter. */\n adapter: DataAdapter;\n children?: ReactNode;\n}\n\n/**\n * Provides the adapter used by the query hooks. The hooks also need TanStack Query's\n * `QueryClientProvider` above them.\n */\nexport function DataAdapterProvider({ adapter, children }: DataAdapterProviderProps) {\n return <DataAdapterContext value={adapter}>{children}</DataAdapterContext>;\n}\n\n/** The adapter of the closest `DataAdapterProvider`. */\nexport function useDataAdapter(): DataAdapter {\n const adapter = use(DataAdapterContext);\n if (adapter === null) {\n throw new Error('useDataAdapter() needs a <DataAdapterProvider> above it.');\n }\n return adapter;\n}\n"],"mappings":";;;;AAIA,IAAM,qBAAqB,cAAkC,IAAI;;;;;AAYjE,SAAO,oBAAA,IAAA;CAAA,MAAA,IAAAE,EAAA,CAAA;CAA6B,MAAA,EAAA,SAAA,aAAA;CAA+C,IAAA;CAAA,IAAA,EAAA,OAAA,WAAA,EAAA,OAAA,UAAA;EACxE,KAAA,oBAAC,oBAAD;GAA2BF,OAAA;GAAU;EAAlB,CAAA;EAAgD,EAAA,KAAA;EAAA,EAAA,KAAA;EAAA,EAAA,KAAA;CAAA,OAAA,KAAA,EAAA;CAAA,OAAnE;AAAmE;;AAI9E,SAAgB,iBAA8B;CAC1C,MAAM,UAAU,IAAI,kBAAkB;CACtC,IAAI,YAAY,MACZ,MAAM,IAAI,MAAM,0DAA0D;CAE9E,OAAO;AACX"}
|
|
1
|
+
{"version":3,"file":"context.js","names":["adapter","children","_c"],"sources":["../../src/query/context.tsx"],"sourcesContent":["import { type ReactNode, createContext, use } from 'react';\n\nimport type { DataAdapter } from '../adapter';\n\nconst DataAdapterContext = createContext<DataAdapter | null>(null);\n\nexport interface DataAdapterProviderProps {\n /** The adapter of the hooks below: `createRestAdapter()`, a preset or a custom adapter. */\n adapter: DataAdapter;\n children?: ReactNode;\n}\n\n/**\n * Provides the adapter used by the query hooks. The hooks also need TanStack Query's\n * `QueryClientProvider` above them.\n */\nexport function DataAdapterProvider({ adapter, children }: DataAdapterProviderProps) {\n return <DataAdapterContext value={adapter}>{children}</DataAdapterContext>;\n}\n\n/** The adapter of the closest `DataAdapterProvider`. */\nexport function useDataAdapter(): DataAdapter {\n const adapter = use(DataAdapterContext);\n if (adapter === null) {\n throw new Error('useDataAdapter() needs a <DataAdapterProvider> above it.');\n }\n return adapter;\n}\n\n/**\n * The adapter of the closest `DataAdapterProvider`, or `null` without one: for components that\n * also work without a backend, such as fields that can upload.\n */\nexport function useOptionalDataAdapter(): DataAdapter | null {\n return use(DataAdapterContext);\n}\n"],"mappings":";;;;AAIA,IAAM,qBAAqB,cAAkC,IAAI;;;;;AAYjE,SAAO,oBAAA,IAAA;CAAA,MAAA,IAAAE,EAAA,CAAA;CAA6B,MAAA,EAAA,SAAA,aAAA;CAA+C,IAAA;CAAA,IAAA,EAAA,OAAA,WAAA,EAAA,OAAA,UAAA;EACxE,KAAA,oBAAC,oBAAD;GAA2BF,OAAA;GAAU;EAAlB,CAAA;EAAgD,EAAA,KAAA;EAAA,EAAA,KAAA;EAAA,EAAA,KAAA;CAAA,OAAA,KAAA,EAAA;CAAA,OAAnE;AAAmE;;AAI9E,SAAgB,iBAA8B;CAC1C,MAAM,UAAU,IAAI,kBAAkB;CACtC,IAAI,YAAY,MACZ,MAAM,IAAI,MAAM,0DAA0D;CAE9E,OAAO;AACX;;;;;AAMA,SAAgB,yBAA6C;CACzD,OAAO,IAAI,kBAAkB;AACjC"}
|
package/dist/query/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DataAdapterProvider, type DataAdapterProviderProps, useDataAdapter } from './context';
|
|
1
|
+
export { DataAdapterProvider, type DataAdapterProviderProps, useDataAdapter, useOptionalDataAdapter, } from './context';
|
|
2
2
|
export { type QueryHookOptions, type RunActionVariables, type UpdateItemVariables, type UseActionRunOptions, isRunFinished, useActionRun, useCreateItem, useDefinition, useDeleteItem, useResourceItem, useResourceList, useRunAction, useUpdateItem, } from './hooks';
|
|
3
3
|
export { queryKeys } from './keys';
|
|
4
4
|
export { type DataQueryOptions, definitionQueryOptions, resourceItemQueryOptions, resourceListQueryOptions, } from './options';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,mBAAmB,EACnB,KAAK,wBAAwB,EAC7B,cAAc,EACd,sBAAsB,GACzB,MAAM,WAAW,CAAC;AACnB,OAAO,EACH,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,aAAa,EACb,YAAY,EACZ,aAAa,EACb,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,YAAY,EACZ,aAAa,GAChB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EACH,KAAK,gBAAgB,EACrB,sBAAsB,EACtB,wBAAwB,EACxB,wBAAwB,GAC3B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/query/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DataAdapterProvider, useDataAdapter } from "./context.js";
|
|
1
|
+
import { DataAdapterProvider, useDataAdapter, useOptionalDataAdapter } from "./context.js";
|
|
2
2
|
import { queryKeys } from "./keys.js";
|
|
3
3
|
import { definitionQueryOptions, resourceItemQueryOptions, resourceListQueryOptions } from "./options.js";
|
|
4
4
|
import { invalidateActionResult } from "./refresh.js";
|
|
5
5
|
import { isRunFinished, useActionRun, useCreateItem, useDefinition, useDeleteItem, useResourceItem, useResourceList, useRunAction, useUpdateItem } from "./hooks.js";
|
|
6
|
-
export { DataAdapterProvider, definitionQueryOptions, invalidateActionResult, isRunFinished, queryKeys, resourceItemQueryOptions, resourceListQueryOptions, useActionRun, useCreateItem, useDataAdapter, useDefinition, useDeleteItem, useResourceItem, useResourceList, useRunAction, useUpdateItem };
|
|
6
|
+
export { DataAdapterProvider, definitionQueryOptions, invalidateActionResult, isRunFinished, queryKeys, resourceItemQueryOptions, resourceListQueryOptions, useActionRun, useCreateItem, useDataAdapter, useDefinition, useDeleteItem, useOptionalDataAdapter, useResourceItem, useResourceList, useRunAction, useUpdateItem };
|
package/dist/rest.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../src/rest.ts"],"names":[],"mappings":"AAAA,OAAO,EAMH,KAAK,OAAO,
|
|
1
|
+
{"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../src/rest.ts"],"names":[],"mappings":"AAAA,OAAO,EAMH,KAAK,OAAO,EAGf,MAAM,mBAAmB,CAAC;AAG3B,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAE,KAAK,QAAQ,EAAwC,MAAM,UAAU,CAAC;AAI/E,MAAM,WAAW,kBAAkB;IAC/B,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChE,wFAAwF;IACxF,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACnE,+DAA+D;IAC/D,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,KAAK,QAAQ,GAAG,SAAS,CAAC;CAChF;AAsDD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CA+K1E"}
|
package/dist/rest.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { InvalidResponseError, createApiError } from "./errors.js";
|
|
2
|
-
import {
|
|
2
|
+
import { readProblem } from "./problem.js";
|
|
3
|
+
import { createPresignedUploads } from "./uploads/index.js";
|
|
4
|
+
import { ActionResult, ActionRun, Definition, PROBLEM_CONTENT_TYPE, toSearchParams, validate } from "@panneau/protocol";
|
|
3
5
|
//#region src/rest.ts
|
|
4
6
|
var ACCEPT = `application/json, ${PROBLEM_CONTENT_TYPE}`;
|
|
5
7
|
function isBlob(value) {
|
|
@@ -23,20 +25,6 @@ function appendFormData(data, key, value) {
|
|
|
23
25
|
function encode(path) {
|
|
24
26
|
return encodeURIComponent(path);
|
|
25
27
|
}
|
|
26
|
-
/** Builds the problem of a failed response, from its body when it is one. */
|
|
27
|
-
async function readProblem(response) {
|
|
28
|
-
const body = await response.clone().json().catch(() => null);
|
|
29
|
-
if (isProblem(body)) return body;
|
|
30
|
-
const record = body !== null && typeof body === "object" ? body : {};
|
|
31
|
-
const problem = {
|
|
32
|
-
type: "about:blank",
|
|
33
|
-
title: typeof record.title === "string" ? record.title : typeof record.message === "string" ? record.message : response.statusText || `HTTP ${response.status}`,
|
|
34
|
-
status: response.status
|
|
35
|
-
};
|
|
36
|
-
const errors = record.errors;
|
|
37
|
-
if (errors !== null && typeof errors === "object" && !Array.isArray(errors)) problem.errors = Object.fromEntries(Object.entries(errors).map(([path, messages]) => [path, (Array.isArray(messages) ? messages : [messages]).map(String)]));
|
|
38
|
-
return problem;
|
|
39
|
-
}
|
|
40
28
|
/**
|
|
41
29
|
* A `DataAdapter` over the HTTP API of the protocol. Errors (`application/problem+json`) reject
|
|
42
30
|
* with typed errors, updates send `If-Match` from the item version, and list queries use the
|
|
@@ -162,43 +150,14 @@ function createRestAdapter(options) {
|
|
|
162
150
|
signal
|
|
163
151
|
});
|
|
164
152
|
},
|
|
165
|
-
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return data({
|
|
174
|
-
path: `/media/${encode(id)}`,
|
|
175
|
-
signal
|
|
176
|
-
});
|
|
177
|
-
},
|
|
178
|
-
uploadMedia(file, values = {}, { signal } = {}) {
|
|
179
|
-
return data({
|
|
180
|
-
method: "POST",
|
|
181
|
-
path: "/media",
|
|
182
|
-
body: {
|
|
183
|
-
...values,
|
|
184
|
-
file
|
|
185
|
-
},
|
|
186
|
-
signal
|
|
187
|
-
});
|
|
188
|
-
},
|
|
189
|
-
updateMedia(id, values, { signal } = {}) {
|
|
190
|
-
return data({
|
|
191
|
-
method: "PATCH",
|
|
192
|
-
path: `/media/${encode(id)}`,
|
|
193
|
-
body: values,
|
|
194
|
-
signal
|
|
195
|
-
});
|
|
196
|
-
},
|
|
197
|
-
async deleteMedia(id, { signal } = {}) {
|
|
198
|
-
await send({
|
|
199
|
-
method: "DELETE",
|
|
200
|
-
path: `/media/${encode(id)}`,
|
|
201
|
-
signal
|
|
153
|
+
presignedUploads({ endpoint } = {}) {
|
|
154
|
+
return createPresignedUploads({
|
|
155
|
+
baseUrl,
|
|
156
|
+
...endpoint !== void 0 && { endpoint },
|
|
157
|
+
fetch: fetcher,
|
|
158
|
+
...options.headers !== void 0 && { headers: options.headers },
|
|
159
|
+
credentials: options.credentials ?? "same-origin",
|
|
160
|
+
createError: (problem, response) => options.createError?.(problem, response) ?? createApiError(problem)
|
|
202
161
|
});
|
|
203
162
|
},
|
|
204
163
|
getSession({ signal } = {}) {
|
package/dist/rest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rest.js","names":[],"sources":["../src/rest.ts"],"sourcesContent":["import {\n ActionResult,\n ActionRun,\n Definition,\n type ListQuery,\n PROBLEM_CONTENT_TYPE,\n type Problem,\n isProblem,\n toSearchParams,\n validate,\n} from '@panneau/protocol';\nimport type {\n Item,\n ListResponse,\n Media,\n MediaListResponse,\n SearchResponse,\n Session,\n} from '@panneau/protocol';\n\nimport type { ActionResponse, DataAdapter, Values } from './adapter';\nimport { type ApiError, InvalidResponseError, createApiError } from './errors';\n\nexport interface RestAdapterOptions {\n /** URL of the API, as `api.baseUrl` in the definition (`/panneau/api`). */\n baseUrl: string;\n /** The `fetch` function to use. Defaults to the global one. */\n fetch?: (input: string, init: RequestInit) => Promise<Response>;\n /** Headers added to every request, or a function that returns them for each request. */\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n /** Whether requests send cookies (`fetch`'s `credentials`). */\n credentials?: RequestCredentials;\n /**\n * Creates the error of a failed response. Return `undefined` to use the default mapping from\n * the status (`createApiError()`). The Laravel preset uses it for `419`.\n */\n createError?: (problem: Problem, response: Response) => ApiError | undefined;\n}\n\ninterface Request {\n method?: string;\n path: string;\n query?: URLSearchParams;\n body?: unknown;\n headers?: HeadersInit;\n signal?: AbortSignal | undefined;\n}\n\nconst ACCEPT = `application/json, ${PROBLEM_CONTENT_TYPE}`;\n\nfunction isBlob(value: unknown): value is Blob {\n return typeof Blob !== 'undefined' && value instanceof Blob;\n}\n\nfunction hasBlob(value: unknown): boolean {\n if (isBlob(value)) {\n return true;\n }\n if (Array.isArray(value)) {\n return value.some(hasBlob);\n }\n if (value !== null && typeof value === 'object') {\n return Object.values(value).some(hasBlob);\n }\n return false;\n}\n\n/** Appends a value to form data with bracket keys: `values[tags][]`, `selection[ids][]`. */\nfunction appendFormData(data: FormData, key: string, value: unknown) {\n if (value === undefined) {\n return;\n }\n if (isBlob(value)) {\n data.append(key, value);\n } else if (Array.isArray(value)) {\n value.forEach((item) => appendFormData(data, `${key}[]`, item));\n } else if (value !== null && typeof value === 'object') {\n Object.entries(value).forEach(([name, item]) =>\n appendFormData(data, key === '' ? name : `${key}[${name}]`, item),\n );\n } else if (typeof value === 'boolean') {\n data.append(key, value ? '1' : '0');\n } else {\n data.append(key, value === null ? '' : String(value));\n }\n}\n\nfunction encode(path: string): string {\n return encodeURIComponent(path);\n}\n\n/** Builds the problem of a failed response, from its body when it is one. */\nasync function readProblem(response: Response): Promise<Problem> {\n const body: unknown = await response\n .clone()\n .json()\n .catch(() => null);\n if (isProblem(body)) {\n return body;\n }\n const record =\n body !== null && typeof body === 'object' ? (body as Record<string, unknown>) : {};\n const problem: Problem = {\n type: 'about:blank',\n title:\n typeof record.title === 'string'\n ? record.title\n : typeof record.message === 'string'\n ? record.message\n : response.statusText || `HTTP ${response.status}`,\n status: response.status,\n };\n const errors = record.errors;\n if (errors !== null && typeof errors === 'object' && !Array.isArray(errors)) {\n problem.errors = Object.fromEntries(\n Object.entries(errors).map(([path, messages]) => [\n path,\n (Array.isArray(messages) ? messages : [messages]).map(String),\n ]),\n );\n }\n return problem;\n}\n\n/**\n * A `DataAdapter` over the HTTP API of the protocol. Errors (`application/problem+json`) reject\n * with typed errors, updates send `If-Match` from the item version, and list queries use the\n * protocol's query string format.\n */\nexport function createRestAdapter(options: RestAdapterOptions): DataAdapter {\n const baseUrl = options.baseUrl.replace(/\\/+$/, '');\n const fetcher = options.fetch ?? ((input, init) => globalThis.fetch(input, init));\n\n const send = async ({\n method = 'GET',\n path,\n query,\n body,\n headers: requestHeaders,\n signal,\n }: Request): Promise<Response> => {\n const headers = new Headers(\n typeof options.headers === 'function' ? await options.headers() : options.headers,\n );\n headers.set('Accept', ACCEPT);\n new Headers(requestHeaders).forEach((value, name) => headers.set(name, value));\n\n let payload: BodyInit | undefined;\n if (body !== undefined) {\n if (hasBlob(body)) {\n const data = new FormData();\n appendFormData(data, '', body);\n payload = data;\n } else {\n headers.set('Content-Type', 'application/json');\n payload = JSON.stringify(body);\n }\n }\n\n const search = query?.toString() ?? '';\n const init: RequestInit = { method, headers };\n if (payload !== undefined) {\n init.body = payload;\n }\n if (options.credentials !== undefined) {\n init.credentials = options.credentials;\n }\n if (signal !== undefined) {\n init.signal = signal;\n }\n const response = await fetcher(\n `${baseUrl}${path}${search !== '' ? `?${search}` : ''}`,\n init,\n );\n if (!response.ok) {\n const problem = await readProblem(response);\n throw options.createError?.(problem, response) ?? createApiError(problem);\n }\n return response;\n };\n\n const json = async <T>(request: Request): Promise<T> => {\n const response = await send(request);\n return (await response.json()) as T;\n };\n\n const data = async <T>(request: Request): Promise<T> => (await json<{ data: T }>(request)).data;\n\n return {\n async getDefinition({ signal } = {}) {\n const body = await json<unknown>({ path: '/definition', signal });\n const result = validate(Definition, body);\n if (!result.success) {\n throw new InvalidResponseError('The definition is invalid.', result.issues);\n }\n return result.data;\n },\n\n list(resource, query: ListQuery = {}, { signal } = {}) {\n return json<ListResponse>({\n path: `/resources/${encode(resource)}`,\n query: toSearchParams(query),\n signal,\n });\n },\n get(resource, id, { signal } = {}) {\n return data<Item>({ path: `/resources/${encode(resource)}/${encode(id)}`, signal });\n },\n create(resource, values, { signal } = {}) {\n return data<Item>({\n method: 'POST',\n path: `/resources/${encode(resource)}`,\n body: values,\n signal,\n });\n },\n update(resource, id, values, { version, signal } = {}) {\n return data<Item>({\n method: 'PATCH',\n path: `/resources/${encode(resource)}/${encode(id)}`,\n body: values,\n headers:\n version !== undefined && version !== null ? { 'If-Match': `\"${version}\"` } : {},\n signal,\n });\n },\n async delete(resource, id, { signal } = {}) {\n await send({\n method: 'DELETE',\n path: `/resources/${encode(resource)}/${encode(id)}`,\n signal,\n });\n },\n\n async runAction(resource, action, request, { signal } = {}) {\n const response = await send({\n method: 'POST',\n path: `/resources/${encode(resource)}/actions/${encode(action)}`,\n body: request,\n signal,\n });\n const body: unknown = response.status === 204 ? {} : await response.json();\n if (response.status === 202) {\n const run = validate(ActionRun, (body as { run?: unknown }).run);\n if (!run.success) {\n throw new InvalidResponseError('The action run is invalid.', run.issues);\n }\n return { run: run.data } satisfies ActionResponse;\n }\n const result = validate(ActionResult, body);\n if (!result.success) {\n throw new InvalidResponseError('The action result is invalid.', result.issues);\n }\n return { result: result.data } satisfies ActionResponse;\n },\n async getActionRun(id, { signal } = {}) {\n return (await json<{ run: ActionRun }>({ path: `/actions/runs/${encode(id)}`, signal }))\n .run;\n },\n listActionRuns({ signal } = {}) {\n return data<ActionRun[]>({ path: '/actions/runs', signal });\n },\n async cancelActionRun(id, { signal } = {}) {\n await send({ method: 'DELETE', path: `/actions/runs/${encode(id)}`, signal });\n },\n\n listMedia(query: ListQuery = {}, { signal } = {}) {\n return json<MediaListResponse>({\n path: '/media',\n query: toSearchParams(query),\n signal,\n });\n },\n getMedia(id, { signal } = {}) {\n return data<Media>({ path: `/media/${encode(id)}`, signal });\n },\n uploadMedia(file, values: Values = {}, { signal } = {}) {\n return data<Media>({\n method: 'POST',\n path: '/media',\n body: { ...values, file },\n signal,\n });\n },\n updateMedia(id, values, { signal } = {}) {\n return data<Media>({\n method: 'PATCH',\n path: `/media/${encode(id)}`,\n body: values,\n signal,\n });\n },\n async deleteMedia(id, { signal } = {}) {\n await send({ method: 'DELETE', path: `/media/${encode(id)}`, signal });\n },\n\n getSession({ signal } = {}) {\n return json<Session>({ path: '/auth/session', signal });\n },\n login(credentials, { signal } = {}) {\n return json<Session>({\n method: 'POST',\n path: '/auth/login',\n body: credentials,\n signal,\n });\n },\n async logout({ signal } = {}) {\n await send({ method: 'POST', path: '/auth/logout', signal });\n },\n\n search(q, { signal } = {}) {\n return json<SearchResponse>({\n path: '/search',\n query: new URLSearchParams({ q }),\n signal,\n });\n },\n getWidgetData(widget, { signal } = {}) {\n return json<unknown>({ path: `/dashboard/widgets/${encode(widget)}`, signal });\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rest.js","names":[],"sources":["../src/rest.ts"],"sourcesContent":["import {\n ActionResult,\n ActionRun,\n Definition,\n type ListQuery,\n PROBLEM_CONTENT_TYPE,\n type Problem,\n toSearchParams,\n validate,\n} from '@panneau/protocol';\nimport type { Item, ListResponse, SearchResponse, Session } from '@panneau/protocol';\n\nimport type { ActionResponse, DataAdapter } from './adapter';\nimport { type ApiError, InvalidResponseError, createApiError } from './errors';\nimport { readProblem } from './problem';\nimport { createPresignedUploads } from './uploads';\n\nexport interface RestAdapterOptions {\n /** URL of the API, as `api.baseUrl` in the definition (`/panneau/api`). */\n baseUrl: string;\n /** The `fetch` function to use. Defaults to the global one. */\n fetch?: (input: string, init: RequestInit) => Promise<Response>;\n /** Headers added to every request, or a function that returns them for each request. */\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n /** Whether requests send cookies (`fetch`'s `credentials`). */\n credentials?: RequestCredentials;\n /**\n * Creates the error of a failed response. Return `undefined` to use the default mapping from\n * the status (`createApiError()`). The Laravel preset uses it for `419`.\n */\n createError?: (problem: Problem, response: Response) => ApiError | undefined;\n}\n\ninterface Request {\n method?: string;\n path: string;\n query?: URLSearchParams;\n body?: unknown;\n headers?: HeadersInit;\n signal?: AbortSignal | undefined;\n}\n\nconst ACCEPT = `application/json, ${PROBLEM_CONTENT_TYPE}`;\n\nfunction isBlob(value: unknown): value is Blob {\n return typeof Blob !== 'undefined' && value instanceof Blob;\n}\n\nfunction hasBlob(value: unknown): boolean {\n if (isBlob(value)) {\n return true;\n }\n if (Array.isArray(value)) {\n return value.some(hasBlob);\n }\n if (value !== null && typeof value === 'object') {\n return Object.values(value).some(hasBlob);\n }\n return false;\n}\n\n/** Appends a value to form data with bracket keys: `values[tags][]`, `selection[ids][]`. */\nfunction appendFormData(data: FormData, key: string, value: unknown) {\n if (value === undefined) {\n return;\n }\n if (isBlob(value)) {\n data.append(key, value);\n } else if (Array.isArray(value)) {\n value.forEach((item) => appendFormData(data, `${key}[]`, item));\n } else if (value !== null && typeof value === 'object') {\n Object.entries(value).forEach(([name, item]) =>\n appendFormData(data, key === '' ? name : `${key}[${name}]`, item),\n );\n } else if (typeof value === 'boolean') {\n data.append(key, value ? '1' : '0');\n } else {\n data.append(key, value === null ? '' : String(value));\n }\n}\n\nfunction encode(path: string): string {\n return encodeURIComponent(path);\n}\n\n/**\n * A `DataAdapter` over the HTTP API of the protocol. Errors (`application/problem+json`) reject\n * with typed errors, updates send `If-Match` from the item version, and list queries use the\n * protocol's query string format.\n */\nexport function createRestAdapter(options: RestAdapterOptions): DataAdapter {\n const baseUrl = options.baseUrl.replace(/\\/+$/, '');\n const fetcher = options.fetch ?? ((input, init) => globalThis.fetch(input, init));\n\n const send = async ({\n method = 'GET',\n path,\n query,\n body,\n headers: requestHeaders,\n signal,\n }: Request): Promise<Response> => {\n const headers = new Headers(\n typeof options.headers === 'function' ? await options.headers() : options.headers,\n );\n headers.set('Accept', ACCEPT);\n new Headers(requestHeaders).forEach((value, name) => headers.set(name, value));\n\n let payload: BodyInit | undefined;\n if (body !== undefined) {\n if (hasBlob(body)) {\n const data = new FormData();\n appendFormData(data, '', body);\n payload = data;\n } else {\n headers.set('Content-Type', 'application/json');\n payload = JSON.stringify(body);\n }\n }\n\n const search = query?.toString() ?? '';\n const init: RequestInit = { method, headers };\n if (payload !== undefined) {\n init.body = payload;\n }\n if (options.credentials !== undefined) {\n init.credentials = options.credentials;\n }\n if (signal !== undefined) {\n init.signal = signal;\n }\n const response = await fetcher(\n `${baseUrl}${path}${search !== '' ? `?${search}` : ''}`,\n init,\n );\n if (!response.ok) {\n const problem = await readProblem(response);\n throw options.createError?.(problem, response) ?? createApiError(problem);\n }\n return response;\n };\n\n const json = async <T>(request: Request): Promise<T> => {\n const response = await send(request);\n return (await response.json()) as T;\n };\n\n const data = async <T>(request: Request): Promise<T> => (await json<{ data: T }>(request)).data;\n\n return {\n async getDefinition({ signal } = {}) {\n const body = await json<unknown>({ path: '/definition', signal });\n const result = validate(Definition, body);\n if (!result.success) {\n throw new InvalidResponseError('The definition is invalid.', result.issues);\n }\n return result.data;\n },\n\n list(resource, query: ListQuery = {}, { signal } = {}) {\n return json<ListResponse>({\n path: `/resources/${encode(resource)}`,\n query: toSearchParams(query),\n signal,\n });\n },\n get(resource, id, { signal } = {}) {\n return data<Item>({ path: `/resources/${encode(resource)}/${encode(id)}`, signal });\n },\n create(resource, values, { signal } = {}) {\n return data<Item>({\n method: 'POST',\n path: `/resources/${encode(resource)}`,\n body: values,\n signal,\n });\n },\n update(resource, id, values, { version, signal } = {}) {\n return data<Item>({\n method: 'PATCH',\n path: `/resources/${encode(resource)}/${encode(id)}`,\n body: values,\n headers:\n version !== undefined && version !== null ? { 'If-Match': `\"${version}\"` } : {},\n signal,\n });\n },\n async delete(resource, id, { signal } = {}) {\n await send({\n method: 'DELETE',\n path: `/resources/${encode(resource)}/${encode(id)}`,\n signal,\n });\n },\n\n async runAction(resource, action, request, { signal } = {}) {\n const response = await send({\n method: 'POST',\n path: `/resources/${encode(resource)}/actions/${encode(action)}`,\n body: request,\n signal,\n });\n const body: unknown = response.status === 204 ? {} : await response.json();\n if (response.status === 202) {\n const run = validate(ActionRun, (body as { run?: unknown }).run);\n if (!run.success) {\n throw new InvalidResponseError('The action run is invalid.', run.issues);\n }\n return { run: run.data } satisfies ActionResponse;\n }\n const result = validate(ActionResult, body);\n if (!result.success) {\n throw new InvalidResponseError('The action result is invalid.', result.issues);\n }\n return { result: result.data } satisfies ActionResponse;\n },\n async getActionRun(id, { signal } = {}) {\n return (await json<{ run: ActionRun }>({ path: `/actions/runs/${encode(id)}`, signal }))\n .run;\n },\n listActionRuns({ signal } = {}) {\n return data<ActionRun[]>({ path: '/actions/runs', signal });\n },\n async cancelActionRun(id, { signal } = {}) {\n await send({ method: 'DELETE', path: `/actions/runs/${encode(id)}`, signal });\n },\n\n presignedUploads({ endpoint } = {}) {\n return createPresignedUploads({\n baseUrl,\n ...(endpoint !== undefined && { endpoint }),\n fetch: fetcher,\n ...(options.headers !== undefined && { headers: options.headers }),\n // The default of `fetch`, as the adapter's other requests\n credentials: options.credentials ?? 'same-origin',\n createError: (problem, response) =>\n options.createError?.(problem, response) ?? createApiError(problem),\n });\n },\n getSession({ signal } = {}) {\n return json<Session>({ path: '/auth/session', signal });\n },\n login(credentials, { signal } = {}) {\n return json<Session>({\n method: 'POST',\n path: '/auth/login',\n body: credentials,\n signal,\n });\n },\n async logout({ signal } = {}) {\n await send({ method: 'POST', path: '/auth/logout', signal });\n },\n\n search(q, { signal } = {}) {\n return json<SearchResponse>({\n path: '/search',\n query: new URLSearchParams({ q }),\n signal,\n });\n },\n getWidgetData(widget, { signal } = {}) {\n return json<unknown>({ path: `/dashboard/widgets/${encode(widget)}`, signal });\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Problem, Upload, UploadSignRequest } from '@panneau/protocol';
|
|
2
|
+
import { type ApiError } from '../errors';
|
|
3
|
+
/** A request to sign, as `signRequest` of `@uppy/aws-s3` 6 gives it. */
|
|
4
|
+
export type PresignableRequest = UploadSignRequest;
|
|
5
|
+
/** A signed request, as `signRequest` of `@uppy/aws-s3` 6 takes it. */
|
|
6
|
+
export interface PresignedResponse {
|
|
7
|
+
url: string;
|
|
8
|
+
/** The key the backend chose for a new upload. */
|
|
9
|
+
key?: string;
|
|
10
|
+
/** Headers the URL was signed with, which the browser sends. */
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export interface PresignedUploadsOptions {
|
|
14
|
+
/** URL of the API, as `api.baseUrl` in the definition: the routes are under `/uploads`. */
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
/** The `endpoint` of the definition's `uploads`, when the routes are elsewhere. */
|
|
17
|
+
endpoint?: string;
|
|
18
|
+
/** The `fetch` function to use. Defaults to the global one. */
|
|
19
|
+
fetch?: (input: string, init: RequestInit) => Promise<Response>;
|
|
20
|
+
/**
|
|
21
|
+
* Headers of every request, or a function that gives them: `() => getLaravelHeaders()` for
|
|
22
|
+
* a Laravel backend with session authentication.
|
|
23
|
+
*/
|
|
24
|
+
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
25
|
+
/** Whether requests send cookies. Defaults to `include`. */
|
|
26
|
+
credentials?: RequestCredentials;
|
|
27
|
+
/** Creates the error of a failed response: `createLaravelError` for Laravel. */
|
|
28
|
+
createError?: (problem: Problem, response: Response) => ApiError;
|
|
29
|
+
}
|
|
30
|
+
export interface PresignedUploads {
|
|
31
|
+
/** Signs a request of Uppy's S3 client: pass it as `signRequest` to `@uppy/aws-s3`. */
|
|
32
|
+
signRequest: (request: PresignableRequest) => Promise<PresignedResponse>;
|
|
33
|
+
/**
|
|
34
|
+
* Tells the backend that the upload of a key is complete, once Uppy reports it
|
|
35
|
+
* (`upload-success`, with the key in `response.body.key`), and resolves with the upload.
|
|
36
|
+
* Values reference it as `{ upload: upload.id }`: a file field, an import, or
|
|
37
|
+
* the `file` of a media (`create('media', { file: { upload: upload.id } })`).
|
|
38
|
+
*/
|
|
39
|
+
complete: (key: string, options?: {
|
|
40
|
+
name?: string;
|
|
41
|
+
}) => Promise<Upload>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Direct uploads to S3-compatible storage with URLs that the backend signs, the `presigned` mode
|
|
45
|
+
* of the protocol (decision 0025): the browser sends files itself, in parts for large ones, then
|
|
46
|
+
* completes each upload, which values reference.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* const uploads = createPresignedUploads({ baseUrl: definition.api.baseUrl, headers: () => getLaravelHeaders() });
|
|
50
|
+
* uppy.use(AwsS3, { signRequest: uploads.signRequest });
|
|
51
|
+
* uppy.on('upload-success', async (file, response) => {
|
|
52
|
+
* const upload = await uploads.complete(response.body.key, { name: file.name });
|
|
53
|
+
* const media = await adapter.create('media', { file: { upload: upload.id } });
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function createPresignedUploads({ baseUrl, endpoint, fetch: fetcher, headers, credentials, createError, }: PresignedUploadsOptions): PresignedUploads;
|
|
58
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/uploads/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAsB,MAAM,mBAAmB,CAAC;AAEhG,OAAO,EAAE,KAAK,QAAQ,EAAwC,MAAM,WAAW,CAAC;AAGhF,wEAAwE;AACxE,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AAEnD,uEAAuE;AACvE,MAAM,WAAW,iBAAiB;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,uBAAuB;IACpC,2FAA2F;IAC3F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChE;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACnE,4DAA4D;IAC5D,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,gFAAgF;IAChF,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,KAAK,QAAQ,CAAC;CACpE;AAED,MAAM,WAAW,gBAAgB;IAC7B,uFAAuF;IACvF,WAAW,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACzE;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,EACnC,OAAO,EACP,QAAQ,EACR,KAAK,EAAE,OAAwD,EAC/D,OAAO,EACP,WAAuB,EACvB,WAAkD,GACrD,EAAE,uBAAuB,GAAG,gBAAgB,CAwD5C"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { InvalidResponseError, createApiError } from "../errors.js";
|
|
2
|
+
import { readProblem } from "../problem.js";
|
|
3
|
+
//#region src/uploads/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* Direct uploads to S3-compatible storage with URLs that the backend signs, the `presigned` mode
|
|
6
|
+
* of the protocol (decision 0025): the browser sends files itself, in parts for large ones, then
|
|
7
|
+
* completes each upload, which values reference.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const uploads = createPresignedUploads({ baseUrl: definition.api.baseUrl, headers: () => getLaravelHeaders() });
|
|
11
|
+
* uppy.use(AwsS3, { signRequest: uploads.signRequest });
|
|
12
|
+
* uppy.on('upload-success', async (file, response) => {
|
|
13
|
+
* const upload = await uploads.complete(response.body.key, { name: file.name });
|
|
14
|
+
* const media = await adapter.create('media', { file: { upload: upload.id } });
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
function createPresignedUploads({ baseUrl, endpoint, fetch: fetcher = (input, init) => globalThis.fetch(input, init), headers, credentials = "include", createError = (problem) => createApiError(problem) }) {
|
|
19
|
+
const base = (endpoint ?? `${(baseUrl ?? "").replace(/\/+$/, "")}/uploads`).replace(/\/+$/, "");
|
|
20
|
+
const uploads = /* @__PURE__ */ new Map();
|
|
21
|
+
const post = async (path, body) => {
|
|
22
|
+
const requestHeaders = new Headers(typeof headers === "function" ? await headers() : headers);
|
|
23
|
+
requestHeaders.set("Accept", "application/json");
|
|
24
|
+
const init = {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: requestHeaders,
|
|
27
|
+
credentials
|
|
28
|
+
};
|
|
29
|
+
if (body !== void 0) {
|
|
30
|
+
requestHeaders.set("Content-Type", "application/json");
|
|
31
|
+
init.body = JSON.stringify(body);
|
|
32
|
+
}
|
|
33
|
+
const response = await fetcher(`${base}${path}`, init);
|
|
34
|
+
if (!response.ok) throw createError(await readProblem(response), response);
|
|
35
|
+
return response.json();
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
async signRequest(request) {
|
|
39
|
+
const body = await post("/sign", request);
|
|
40
|
+
if (typeof body?.url !== "string") throw new InvalidResponseError("The signed request has no URL.", []);
|
|
41
|
+
const key = body.key ?? request.key;
|
|
42
|
+
if (typeof body.upload === "string") uploads.set(key, body.upload);
|
|
43
|
+
return {
|
|
44
|
+
url: body.url,
|
|
45
|
+
...body.key !== void 0 && { key: body.key },
|
|
46
|
+
...body.headers !== void 0 && { headers: body.headers }
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
async complete(key, { name } = {}) {
|
|
50
|
+
const upload = uploads.get(key) ?? key.split("/").pop() ?? key;
|
|
51
|
+
const body = await post(`/${encodeURIComponent(upload)}/complete`, name !== void 0 ? { name } : void 0);
|
|
52
|
+
if (typeof body?.data?.id !== "string") throw new InvalidResponseError("The completed upload has no id.", []);
|
|
53
|
+
uploads.delete(key);
|
|
54
|
+
return body.data;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { createPresignedUploads };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/uploads/index.ts"],"sourcesContent":["import type { Problem, Upload, UploadSignRequest, UploadSignResponse } from '@panneau/protocol';\n\nimport { type ApiError, InvalidResponseError, createApiError } from '../errors';\nimport { readProblem } from '../problem';\n\n/** A request to sign, as `signRequest` of `@uppy/aws-s3` 6 gives it. */\nexport type PresignableRequest = UploadSignRequest;\n\n/** A signed request, as `signRequest` of `@uppy/aws-s3` 6 takes it. */\nexport interface PresignedResponse {\n url: string;\n /** The key the backend chose for a new upload. */\n key?: string;\n /** Headers the URL was signed with, which the browser sends. */\n headers?: Record<string, string>;\n}\n\nexport interface PresignedUploadsOptions {\n /** URL of the API, as `api.baseUrl` in the definition: the routes are under `/uploads`. */\n baseUrl?: string;\n /** The `endpoint` of the definition's `uploads`, when the routes are elsewhere. */\n endpoint?: string;\n /** The `fetch` function to use. Defaults to the global one. */\n fetch?: (input: string, init: RequestInit) => Promise<Response>;\n /**\n * Headers of every request, or a function that gives them: `() => getLaravelHeaders()` for\n * a Laravel backend with session authentication.\n */\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n /** Whether requests send cookies. Defaults to `include`. */\n credentials?: RequestCredentials;\n /** Creates the error of a failed response: `createLaravelError` for Laravel. */\n createError?: (problem: Problem, response: Response) => ApiError;\n}\n\nexport interface PresignedUploads {\n /** Signs a request of Uppy's S3 client: pass it as `signRequest` to `@uppy/aws-s3`. */\n signRequest: (request: PresignableRequest) => Promise<PresignedResponse>;\n /**\n * Tells the backend that the upload of a key is complete, once Uppy reports it\n * (`upload-success`, with the key in `response.body.key`), and resolves with the upload.\n * Values reference it as `{ upload: upload.id }`: a file field, an import, or\n * the `file` of a media (`create('media', { file: { upload: upload.id } })`).\n */\n complete: (key: string, options?: { name?: string }) => Promise<Upload>;\n}\n\n/**\n * Direct uploads to S3-compatible storage with URLs that the backend signs, the `presigned` mode\n * of the protocol (decision 0025): the browser sends files itself, in parts for large ones, then\n * completes each upload, which values reference.\n *\n * ```ts\n * const uploads = createPresignedUploads({ baseUrl: definition.api.baseUrl, headers: () => getLaravelHeaders() });\n * uppy.use(AwsS3, { signRequest: uploads.signRequest });\n * uppy.on('upload-success', async (file, response) => {\n * const upload = await uploads.complete(response.body.key, { name: file.name });\n * const media = await adapter.create('media', { file: { upload: upload.id } });\n * });\n * ```\n */\nexport function createPresignedUploads({\n baseUrl,\n endpoint,\n fetch: fetcher = (input, init) => globalThis.fetch(input, init),\n headers,\n credentials = 'include',\n createError = (problem) => createApiError(problem),\n}: PresignedUploadsOptions): PresignedUploads {\n const base = (endpoint ?? `${(baseUrl ?? '').replace(/\\/+$/, '')}/uploads`).replace(/\\/+$/, '');\n // The upload of each key, from the response that created it.\n const uploads = new Map<string, string>();\n\n const post = async (path: string, body?: unknown): Promise<unknown> => {\n const requestHeaders = new Headers(\n typeof headers === 'function' ? await headers() : headers,\n );\n requestHeaders.set('Accept', 'application/json');\n const init: RequestInit = { method: 'POST', headers: requestHeaders, credentials };\n if (body !== undefined) {\n requestHeaders.set('Content-Type', 'application/json');\n init.body = JSON.stringify(body);\n }\n const response = await fetcher(`${base}${path}`, init);\n if (!response.ok) {\n throw createError(await readProblem(response), response);\n }\n return response.json();\n };\n\n return {\n async signRequest(request) {\n const body = (await post('/sign', request)) as UploadSignResponse;\n if (typeof body?.url !== 'string') {\n throw new InvalidResponseError('The signed request has no URL.', []);\n }\n const key = body.key ?? request.key;\n if (typeof body.upload === 'string') {\n uploads.set(key, body.upload);\n }\n return {\n url: body.url,\n ...(body.key !== undefined && { key: body.key }),\n ...(body.headers !== undefined && { headers: body.headers }),\n };\n },\n\n async complete(key, { name } = {}) {\n // The backend names the upload in the sign response; without it, it is the last\n // segment of the key it chose (`uploads/upl_01H…`).\n const upload = uploads.get(key) ?? key.split('/').pop() ?? key;\n const body = (await post(\n `/${encodeURIComponent(upload)}/complete`,\n name !== undefined ? { name } : undefined,\n )) as {\n data?: Upload;\n };\n if (typeof body?.data?.id !== 'string') {\n throw new InvalidResponseError('The completed upload has no id.', []);\n }\n uploads.delete(key);\n return body.data;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA6DA,SAAgB,uBAAuB,EACnC,SACA,UACA,OAAO,WAAW,OAAO,SAAS,WAAW,MAAM,OAAO,IAAI,GAC9D,SACA,cAAc,WACd,eAAe,YAAY,eAAe,OAAO,KACP;CAC1C,MAAM,QAAQ,YAAY,IAAI,WAAW,GAAA,CAAI,QAAQ,QAAQ,EAAE,EAAC,UAAA,CAAY,QAAQ,QAAQ,EAAE;CAE9F,MAAM,0BAAU,IAAI,IAAoB;CAExC,MAAM,OAAO,OAAO,MAAc,SAAqC;EACnE,MAAM,iBAAiB,IAAI,QACvB,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAI,OACtD;EACA,eAAe,IAAI,UAAU,kBAAkB;EAC/C,MAAM,OAAoB;GAAE,QAAQ;GAAQ,SAAS;GAAgB;EAAY;EACjF,IAAI,SAAS,KAAA,GAAW;GACpB,eAAe,IAAI,gBAAgB,kBAAkB;GACrD,KAAK,OAAO,KAAK,UAAU,IAAI;EACnC;EACA,MAAM,WAAW,MAAM,QAAQ,GAAG,OAAO,QAAQ,IAAI;EACrD,IAAI,CAAC,SAAS,IACV,MAAM,YAAY,MAAM,YAAY,QAAQ,GAAG,QAAQ;EAE3D,OAAO,SAAS,KAAK;CACzB;CAEA,OAAO;EACH,MAAM,YAAY,SAAS;GACvB,MAAM,OAAQ,MAAM,KAAK,SAAS,OAAO;GACzC,IAAI,OAAO,MAAM,QAAQ,UACrB,MAAM,IAAI,qBAAqB,kCAAkC,CAAA,CAAE;GAEvE,MAAM,MAAM,KAAK,OAAO,QAAQ;GAChC,IAAI,OAAO,KAAK,WAAW,UACvB,QAAQ,IAAI,KAAK,KAAK,MAAM;GAEhC,OAAO;IACH,KAAK,KAAK;IACV,GAAI,KAAK,QAAQ,KAAA,KAAa,EAAE,KAAK,KAAK,IAAI;IAC9C,GAAI,KAAK,YAAY,KAAA,KAAa,EAAE,SAAS,KAAK,QAAQ;GAC9D;EACJ;EAEA,MAAM,SAAS,KAAK,EAAE,SAAS,CAAC,GAAG;GAG/B,MAAM,SAAS,QAAQ,IAAI,GAAG,KAAK,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;GAC3D,MAAM,OAAQ,MAAM,KAChB,IAAI,mBAAmB,MAAM,EAAC,YAC9B,SAAS,KAAA,IAAY,EAAE,KAAK,IAAI,KAAA,CACpC;GAGA,IAAI,OAAO,MAAM,MAAM,OAAO,UAC1B,MAAM,IAAI,qBAAqB,mCAAmC,CAAA,CAAE;GAExE,QAAQ,OAAO,GAAG;GAClB,OAAO,KAAK;EAChB;CACJ;AACJ"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panneau/data",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.4",
|
|
4
4
|
"description": "The data layer of Panneau: the DataAdapter interface, the REST adapter of the protocol, its Laravel preset and TanStack Query hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
"types": "./dist/query/index.d.ts",
|
|
27
27
|
"default": "./dist/query/index.js"
|
|
28
28
|
},
|
|
29
|
+
"./uploads": {
|
|
30
|
+
"@panneau/source": "./src/uploads/index.ts",
|
|
31
|
+
"types": "./dist/uploads/index.d.ts",
|
|
32
|
+
"default": "./dist/uploads/index.js"
|
|
33
|
+
},
|
|
29
34
|
"./package.json": "./package.json"
|
|
30
35
|
},
|
|
31
36
|
"files": [
|
|
@@ -44,7 +49,7 @@
|
|
|
44
49
|
"react": "^19.2.0"
|
|
45
50
|
},
|
|
46
51
|
"dependencies": {
|
|
47
|
-
"@panneau/protocol": "5.0.0-alpha.
|
|
52
|
+
"@panneau/protocol": "5.0.0-alpha.4"
|
|
48
53
|
},
|
|
49
54
|
"peerDependenciesMeta": {
|
|
50
55
|
"@tanstack/react-query": {
|
|
@@ -55,6 +60,6 @@
|
|
|
55
60
|
}
|
|
56
61
|
},
|
|
57
62
|
"devDependencies": {
|
|
58
|
-
"@panneau/core": "5.0.0-alpha.
|
|
63
|
+
"@panneau/core": "5.0.0-alpha.4"
|
|
59
64
|
}
|
|
60
65
|
}
|