@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 CHANGED
@@ -18,15 +18,16 @@ try {
18
18
  }
19
19
  ```
20
20
 
21
- | Export | Content |
22
- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
- | `@panneau/data` | The `DataAdapter` interface, `createRestAdapter()`, and the errors: `ApiError`, `ValidationError`, `ConflictError`, `UnauthorizedError`, `ForbiddenError`, `NotFoundError` |
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/query` | `DataAdapterProvider` and the TanStack Query hooks: `useDefinition`, `useResourceList`, `useResourceItem`, `useCreateItem`, `useUpdateItem`, `useDeleteItem`, `useRunAction`, `useActionRun` |
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, media, the auth session, search and widget data. Methods reject with an `ApiError` when the backend answers with an error.
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, Media, MediaListResponse, SearchResponse, Session } from '@panneau/protocol';
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
- /** A page of the media library. */
64
- listMedia(query?: MediaQuery, options?: RequestOptions): Promise<MediaListResponse>;
65
- /** One media item. */
66
- getMedia(id: string, options?: RequestOptions): Promise<Media>;
67
- /** Uploads a file, with optional metadata. */
68
- uploadMedia(file: Blob, values?: Values, options?: RequestOptions): Promise<Media>;
69
- /** Updates the metadata or the transform of a media item. */
70
- updateMedia(id: string, values: Values, options?: RequestOptions): Promise<Media>;
71
- /** Deletes a media item. */
72
- deleteMedia(id: string, options?: RequestOptions): Promise<void>;
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. */
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
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, MediaQuery, RequestOptions, UpdateOptions, Values, } from './adapter';
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
@@ -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,UAAU,EACV,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,KAAK,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC"}
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 };
@@ -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
- export interface LaravelAdapterOptions extends RestAdapterOptions {
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;AAE9C,OAAO,EAAE,KAAK,kBAAkB,EAAqB,MAAM,SAAS,CAAC;AAErE,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC7D,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;AAgBD;;;;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,CAiBrC"}
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"}
@@ -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
- const result = new Headers(typeof headers === "function" ? await headers() : headers);
26
- result.set("X-Requested-With", "XMLHttpRequest");
27
- const token = readCookie(cookies(), xsrfCookie);
28
- if (token !== null) result.set(xsrfHeader, token);
29
- return result;
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 LaravelAdapterOptions extends RestAdapterOptions {\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\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 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 const result = new Headers(typeof headers === 'function' ? await 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 createError: (problem, response) =>\n createError?.(problem, response) ??\n (problem.status === 419 ? new UnauthorizedError(problem) : undefined),\n });\n}\n"],"mappings":";;;AAaA,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;;;;;;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,YAAY;GACjB,MAAM,SAAS,IAAI,QAAQ,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAI,OAAO;GACpF,OAAO,IAAI,oBAAoB,gBAAgB;GAC/C,MAAM,QAAQ,WAAW,QAAQ,GAAG,UAAU;GAC9C,IAAI,UAAU,MACV,OAAO,IAAI,YAAY,KAAK;GAEhC,OAAO;EACX;EACA,cAAc,SAAS,aACnB,cAAc,SAAS,QAAQ,MAC9B,QAAQ,WAAW,MAAM,IAAI,kBAAkB,OAAO,IAAI,KAAA;CACnE,CAAC;AACL"}
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"}
@@ -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"}
@@ -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"}
@@ -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"}
@@ -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,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC/F,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"}
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"}
@@ -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 };
@@ -1 +1 @@
1
- {"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../src/rest.ts"],"names":[],"mappings":"AAAA,OAAO,EAMH,KAAK,OAAO,EAIf,MAAM,mBAAmB,CAAC;AAU3B,OAAO,KAAK,EAAkB,WAAW,EAAU,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,KAAK,QAAQ,EAAwC,MAAM,UAAU,CAAC;AAE/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;AAuFD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAiM1E"}
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 { ActionResult, ActionRun, Definition, PROBLEM_CONTENT_TYPE, isProblem, toSearchParams, validate } from "@panneau/protocol";
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
- listMedia(query = {}, { signal } = {}) {
166
- return json({
167
- path: "/media",
168
- query: toSearchParams(query),
169
- signal
170
- });
171
- },
172
- getMedia(id, { signal } = {}) {
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.2",
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.2"
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.2"
63
+ "@panneau/core": "5.0.0-alpha.4"
59
64
  }
60
65
  }