@jsondrop/drupal 0.0.0-stage → 0.1.1
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/LICENSE.md +21 -0
- package/README.md +78 -2
- package/dist/account.d.ts +65 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +96 -0
- package/dist/account.js.map +1 -0
- package/dist/entities.d.ts +205 -0
- package/dist/entities.d.ts.map +1 -0
- package/dist/entities.js +2 -0
- package/dist/entities.js.map +1 -0
- package/dist/fields.d.ts +73 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +7 -0
- package/dist/fields.js.map +1 -0
- package/dist/files.d.ts +27 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +40 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/menu.d.ts +48 -0
- package/dist/menu.d.ts.map +1 -0
- package/dist/menu.js +35 -0
- package/dist/menu.js.map +1 -0
- package/dist/router.d.ts +56 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +52 -0
- package/dist/router.js.map +1 -0
- package/dist/views.d.ts +29 -0
- package/dist/views.d.ts.map +1 -0
- package/dist/views.js +32 -0
- package/dist/views.js.map +1 -0
- package/package.json +56 -4
- package/src/account.ts +125 -0
- package/src/entities.ts +232 -0
- package/src/fields.ts +81 -0
- package/src/files.ts +52 -0
- package/src/index.ts +7 -0
- package/src/menu.ts +71 -0
- package/src/router.ts +89 -0
- package/src/views.ts +50 -0
package/dist/files.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isStub } from '@jsondrop/core';
|
|
2
|
+
/**
|
|
3
|
+
* Absolute URL of a file. Drupal 11 serves `uri.url` root-relative
|
|
4
|
+
* (`/sites/default/files/…`); this prefixes the site's origin.
|
|
5
|
+
*/
|
|
6
|
+
export function fileUrl(client, file) {
|
|
7
|
+
const url = file.uri.url;
|
|
8
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith('//') ? url : `${client.baseUrl}${url}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* URL of an image style derivative, as the consumer_image_styles module
|
|
12
|
+
* adds them to the file's links. NULL when the style is not enabled for
|
|
13
|
+
* the requesting consumer.
|
|
14
|
+
*/
|
|
15
|
+
export function imageStyleUrl(file, style) {
|
|
16
|
+
return file.links?.[style]?.href ?? null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* An image field's value, ready for an `<img>`: absolute URL plus the alt
|
|
20
|
+
* text and dimensions from the relationship. NULL for an empty field or a
|
|
21
|
+
* file that was not included.
|
|
22
|
+
*/
|
|
23
|
+
export function image(client, value) {
|
|
24
|
+
if (!value || typeof value !== 'object' || isStub(value)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const file = value;
|
|
28
|
+
if (!file.uri?.url) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const meta = file.relationshipMeta ?? {};
|
|
32
|
+
return {
|
|
33
|
+
url: fileUrl(client, file),
|
|
34
|
+
alt: meta.alt ?? '',
|
|
35
|
+
title: meta.title || null,
|
|
36
|
+
width: meta.width ?? null,
|
|
37
|
+
height: meta.height ?? null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAsB,MAAM,gBAAgB,CAAC;AAI5D;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,MAAqB,EAAE,IAAuB;IACpE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IACzB,OAAO,sBAAsB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC;AACpG,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAyB,EAAE,KAAa;IACpE,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,IAAI,IAAI,IAAI,CAAC;AAC3C,CAAC;AAUD;;;;GAIG;AACH,MAAM,UAAU,KAAK,CAAC,MAAqB,EAAE,KAAc;IACzD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACzD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,IAAI,GAAG,KAAwB,CAAC;IACtC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,IAAI,GAAuB,IAAI,CAAC,gBAAgB,IAAI,EAAE,CAAC;IAC7D,OAAO;QACL,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC;QAC1B,GAAG,EAAE,IAAI,CAAC,GAAG,IAAI,EAAE;QACnB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI;QACzB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI;QACzB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI;KAC5B,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
|
package/dist/menu.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { JsonApiClient, ResourceBase, ResourceRequestOptions } from '@jsondrop/core';
|
|
2
|
+
/**
|
|
3
|
+
* A menu link from the jsonapi_menu_items module. Unlike core's
|
|
4
|
+
* `menu_link_content` resources, it covers every link in the menu (links
|
|
5
|
+
* defined by modules too) and comes with a resolved `url`.
|
|
6
|
+
*/
|
|
7
|
+
export interface MenuItem extends ResourceBase {
|
|
8
|
+
/** Plugin ID: `menu_link_content:{uuid}`, `standard.front_page`, … */
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string | null;
|
|
12
|
+
url: string;
|
|
13
|
+
menu_name: string;
|
|
14
|
+
/** Parent plugin ID, or empty at the root. */
|
|
15
|
+
parent: string;
|
|
16
|
+
weight: number;
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
expanded: boolean;
|
|
19
|
+
provider: string;
|
|
20
|
+
route?: {
|
|
21
|
+
name: string;
|
|
22
|
+
parameters: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
options?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
export type MenuTreeItem<T> = T & {
|
|
27
|
+
children: MenuTreeItem<T>[];
|
|
28
|
+
};
|
|
29
|
+
/** A menu's enabled links, flat (jsonapi_menu_items module). */
|
|
30
|
+
export declare function getMenuItems(client: JsonApiClient, menu: string, options?: ResourceRequestOptions): Promise<MenuItem[]>;
|
|
31
|
+
/** A menu as a tree, sorted by weight then title (jsonapi_menu_items module). */
|
|
32
|
+
export declare function getMenu(client: JsonApiClient, menu: string, options?: ResourceRequestOptions): Promise<MenuTreeItem<MenuItem>[]>;
|
|
33
|
+
interface TreeInput {
|
|
34
|
+
id: string;
|
|
35
|
+
type?: string;
|
|
36
|
+
parent: string | null;
|
|
37
|
+
weight: number;
|
|
38
|
+
title: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Nests flat menu links by `parent`, sorted by weight then title. Takes
|
|
42
|
+
* jsonapi_menu_items links or core `menu_link_content` resources (whose
|
|
43
|
+
* `parent` names the plugin ID `menu_link_content:{uuid}`). A link whose
|
|
44
|
+
* parent is not in the list becomes a root.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildMenuTree<T extends TreeInput>(items: T[]): MenuTreeItem<T>[];
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=menu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAE1F;;;;GAIG;AACH,MAAM,WAAW,QAAS,SAAQ,YAAY;IAC5C,sEAAsE;IACtE,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA;CAAE,CAAC;AAElE,gEAAgE;AAChE,wBAAsB,YAAY,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAG7H;AAED,iFAAiF;AACjF,wBAAsB,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC,CAEtI;AAED,UAAU,SAAS;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAID;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,YAAY,CAAC,CAAC,CAAC,EAAE,CAiBhF"}
|
package/dist/menu.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** A menu's enabled links, flat (jsonapi_menu_items module). */
|
|
2
|
+
export async function getMenuItems(client, menu, options) {
|
|
3
|
+
const response = await client.get(`/menu_items/${encodeURIComponent(menu)}`, options);
|
|
4
|
+
return response.data ?? [];
|
|
5
|
+
}
|
|
6
|
+
/** A menu as a tree, sorted by weight then title (jsonapi_menu_items module). */
|
|
7
|
+
export async function getMenu(client, menu, options) {
|
|
8
|
+
return buildMenuTree(await getMenuItems(client, menu, options));
|
|
9
|
+
}
|
|
10
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
/**
|
|
12
|
+
* Nests flat menu links by `parent`, sorted by weight then title. Takes
|
|
13
|
+
* jsonapi_menu_items links or core `menu_link_content` resources (whose
|
|
14
|
+
* `parent` names the plugin ID `menu_link_content:{uuid}`). A link whose
|
|
15
|
+
* parent is not in the list becomes a root.
|
|
16
|
+
*/
|
|
17
|
+
export function buildMenuTree(items) {
|
|
18
|
+
const pluginId = (item) => (item.type === 'menu_link_content--menu_link_content' && UUID.test(item.id) ? `menu_link_content:${item.id}` : item.id);
|
|
19
|
+
const nodes = new Map();
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
nodes.set(pluginId(item), { ...item, children: [] });
|
|
22
|
+
}
|
|
23
|
+
const roots = [];
|
|
24
|
+
for (const node of nodes.values()) {
|
|
25
|
+
const parent = node.parent ? nodes.get(node.parent) : undefined;
|
|
26
|
+
(parent && parent !== node ? parent.children : roots).push(node);
|
|
27
|
+
}
|
|
28
|
+
const sort = (list) => {
|
|
29
|
+
list.sort((a, b) => a.weight - b.weight || a.title.localeCompare(b.title));
|
|
30
|
+
list.forEach((node) => sort(node.children));
|
|
31
|
+
};
|
|
32
|
+
sort(roots);
|
|
33
|
+
return roots;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=menu.js.map
|
package/dist/menu.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu.js","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"AA0BA,gEAAgE;AAChE,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAqB,EAAE,IAAY,EAAE,OAAgC;IACtG,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,GAAG,CAAa,eAAe,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IAClG,OAAO,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC;AAC7B,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,MAAqB,EAAE,IAAY,EAAE,OAAgC;IACjG,OAAO,aAAa,CAAC,MAAM,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;AAClE,CAAC;AAUD,MAAM,IAAI,GAAG,iEAAiE,CAAC;AAE/E;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAsB,KAAU;IAC3D,MAAM,QAAQ,GAAG,CAAC,IAAO,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,sCAAsC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,qBAAqB,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtJ,MAAM,KAAK,GAAG,IAAI,GAAG,EAA2B,CAAC;IACjD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,KAAK,GAAsB,EAAE,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAChE,CAAC,MAAM,IAAI,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnE,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,IAAuB,EAAE,EAAE;QACvC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3E,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC;IACF,IAAI,CAAC,KAAK,CAAC,CAAC;IACZ,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type JsonApiClient, type Resource, type ResourceRequestOptions } from '@jsondrop/core';
|
|
2
|
+
/** What decoupled_router answers for a path. */
|
|
3
|
+
export interface TranslatedPath {
|
|
4
|
+
/** Absolute URL the path resolves to. */
|
|
5
|
+
resolved: string;
|
|
6
|
+
isHomePath: boolean;
|
|
7
|
+
entity?: {
|
|
8
|
+
canonical: string;
|
|
9
|
+
type: string;
|
|
10
|
+
bundle: string;
|
|
11
|
+
/** Drupal's numeric ID, as a string. */
|
|
12
|
+
id: string;
|
|
13
|
+
uuid: string;
|
|
14
|
+
};
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Present when the path is an entity JSON:API exposes. */
|
|
17
|
+
jsonapi?: {
|
|
18
|
+
individual: string;
|
|
19
|
+
resourceName: string;
|
|
20
|
+
pathPrefix: string;
|
|
21
|
+
basePath: string;
|
|
22
|
+
entryPoint: string;
|
|
23
|
+
};
|
|
24
|
+
/** Redirects (redirect module) that led here, oldest first. */
|
|
25
|
+
redirect?: Array<{
|
|
26
|
+
from: string;
|
|
27
|
+
to: string;
|
|
28
|
+
status: string;
|
|
29
|
+
}>;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
export interface ResolvedPath<T> {
|
|
33
|
+
route: TranslatedPath;
|
|
34
|
+
/** The entity at the path; NULL when the path is not a JSON:API entity. */
|
|
35
|
+
resource: T | null;
|
|
36
|
+
}
|
|
37
|
+
type PathOptions = Pick<ResourceRequestOptions, 'locale' | 'auth' | 'headers' | 'fetchOptions' | 'signal'>;
|
|
38
|
+
/**
|
|
39
|
+
* Resolves a site path (`/about-us`, `/node/1`) with the decoupled_router
|
|
40
|
+
* module. NULL when nothing is there; a 403 (unpublished, no access) throws.
|
|
41
|
+
*/
|
|
42
|
+
export declare function translatePath(client: JsonApiClient, path: string, options?: PathOptions): Promise<TranslatedPath | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Resolves a path and fetches the entity there, in two requests. The entity
|
|
45
|
+
* is fetched from the URL decoupled_router gives, which respects resource
|
|
46
|
+
* paths renamed with JSON:API Extras and the path's language.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* const page = await getResourceByPath<Node>(drupal, params.slug, { query: { include: ['uid'] } });
|
|
50
|
+
* if (!page) notFound();
|
|
51
|
+
* if (page.route.redirect?.length) redirect(page.route.redirect.at(-1)!.to);
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function getResourceByPath<T = Resource>(client: JsonApiClient, path: string, options?: ResourceRequestOptions): Promise<ResolvedPath<T> | null>;
|
|
55
|
+
export {};
|
|
56
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,KAAK,QAAQ,EAAE,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAEhH,gDAAgD;AAChD,MAAM,WAAW,cAAc;IAC7B,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE;QACP,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,wCAAwC;QACxC,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,OAAO,CAAC,EAAE;QACR,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/D,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,KAAK,EAAE,cAAc,CAAC;IACtB,2EAA2E;IAC3E,QAAQ,EAAE,CAAC,GAAG,IAAI,CAAC;CACpB;AAED,KAAK,WAAW,GAAG,IAAI,CAAC,sBAAsB,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,cAAc,GAAG,QAAQ,CAAC,CAAC;AAE3G;;;GAGG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAgBlI;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAUhK"}
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isJsonApiError } from '@jsondrop/core';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a site path (`/about-us`, `/node/1`) with the decoupled_router
|
|
4
|
+
* module. NULL when nothing is there; a 403 (unpublished, no access) throws.
|
|
5
|
+
*/
|
|
6
|
+
export async function translatePath(client, path, options = {}) {
|
|
7
|
+
try {
|
|
8
|
+
const response = await client.get(client.siteUrl('/router/translate-path', undefined, options.locale), {
|
|
9
|
+
...options,
|
|
10
|
+
query: { params: { path, _format: 'json' } },
|
|
11
|
+
headers: { Accept: 'application/json', ...headersObject(options.headers) },
|
|
12
|
+
parse: 'json',
|
|
13
|
+
});
|
|
14
|
+
return response.data;
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (isJsonApiError(error) && error.status === 404) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolves a path and fetches the entity there, in two requests. The entity
|
|
25
|
+
* is fetched from the URL decoupled_router gives, which respects resource
|
|
26
|
+
* paths renamed with JSON:API Extras and the path's language.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* const page = await getResourceByPath<Node>(drupal, params.slug, { query: { include: ['uid'] } });
|
|
30
|
+
* if (!page) notFound();
|
|
31
|
+
* if (page.route.redirect?.length) redirect(page.route.redirect.at(-1)!.to);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export async function getResourceByPath(client, path, options = {}) {
|
|
35
|
+
const route = await translatePath(client, path, options);
|
|
36
|
+
if (!route) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (!route.jsonapi?.individual) {
|
|
40
|
+
return { route, resource: null };
|
|
41
|
+
}
|
|
42
|
+
const response = await client.get(route.jsonapi.individual, options);
|
|
43
|
+
return { route, resource: response.data };
|
|
44
|
+
}
|
|
45
|
+
function headersObject(headers) {
|
|
46
|
+
const result = {};
|
|
47
|
+
new Headers(headers).forEach((value, name) => {
|
|
48
|
+
result[name] = value;
|
|
49
|
+
});
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAkE,MAAM,gBAAgB,CAAC;AAqChH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAqB,EAAE,IAAY,EAAE,UAAuB,EAAE;IAChG,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,GAAG,CAAiB,MAAM,CAAC,OAAO,CAAC,wBAAwB,EAAE,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE;YACrH,GAAG,OAAO;YACV,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;YAC5C,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;YAC1E,KAAK,EAAE,MAAM;SACd,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IACD,OAAO,KAAK,EAAE,CAAC;QACb,IAAI,cAAc,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAClD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAe,MAAqB,EAAE,IAAY,EAAE,UAAkC,EAAE;IAC7H,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACzD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;QAC/B,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACnC,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,GAAG,CAAI,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACxE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;AAC5C,CAAC;AAED,SAAS,aAAa,CAAC,OAAgC;IACrD,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QAC3C,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IACvB,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/views.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { JsonApiClient, Links, Meta, Resource, ResourceRequestOptions } from '@jsondrop/core';
|
|
2
|
+
export interface ViewOptions extends Omit<ResourceRequestOptions, 'query'> {
|
|
3
|
+
/** Exposed filters, by identifier. */
|
|
4
|
+
filters?: Record<string, string | string[]>;
|
|
5
|
+
/** Contextual filter values, in order. */
|
|
6
|
+
arguments?: string[];
|
|
7
|
+
/** Exposed sort. */
|
|
8
|
+
sort?: {
|
|
9
|
+
by: string;
|
|
10
|
+
order?: 'ASC' | 'DESC';
|
|
11
|
+
};
|
|
12
|
+
/** Zero-based page of the view's pager. */
|
|
13
|
+
page?: number;
|
|
14
|
+
/** Other parameters (`include` works). */
|
|
15
|
+
params?: Record<string, string | string[]>;
|
|
16
|
+
}
|
|
17
|
+
export interface ViewResult<T> {
|
|
18
|
+
data: T[];
|
|
19
|
+
/** Total result count, when the view has a pager. */
|
|
20
|
+
count: number | null;
|
|
21
|
+
meta: Meta;
|
|
22
|
+
links: Links;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Results of a view display (jsonapi_views module): the display's
|
|
26
|
+
* filters, sorts and pager, as JSON:API resources.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getView<T = Resource>(client: JsonApiClient, view: string, display: string, options?: ViewOptions): Promise<ViewResult<T>>;
|
|
29
|
+
//# sourceMappingURL=views.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"views.d.ts","sourceRoot":"","sources":["../src/views.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAEnG,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,sBAAsB,EAAE,OAAO,CAAC;IACxE,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC5C,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,oBAAoB;IACpB,IAAI,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAC;IAC9C,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,qDAAqD;IACrD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,KAAK,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAAC,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAsBnJ"}
|
package/dist/views.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Results of a view display (jsonapi_views module): the display's
|
|
3
|
+
* filters, sorts and pager, as JSON:API resources.
|
|
4
|
+
*/
|
|
5
|
+
export async function getView(client, view, display, options = {}) {
|
|
6
|
+
const { filters, arguments: args, sort, page, params, ...rest } = options;
|
|
7
|
+
const query = { ...params };
|
|
8
|
+
for (const [name, value] of Object.entries(filters ?? {})) {
|
|
9
|
+
if (Array.isArray(value))
|
|
10
|
+
query[`views-filter[${name}][]`] = value;
|
|
11
|
+
else
|
|
12
|
+
query[`views-filter[${name}]`] = value;
|
|
13
|
+
}
|
|
14
|
+
if (args?.length)
|
|
15
|
+
query['views-argument[]'] = args;
|
|
16
|
+
if (sort) {
|
|
17
|
+
query['views-sort[sort_by]'] = sort.by;
|
|
18
|
+
if (sort.order)
|
|
19
|
+
query['views-sort[sort_order]'] = sort.order;
|
|
20
|
+
}
|
|
21
|
+
if (page !== undefined)
|
|
22
|
+
query.page = String(page);
|
|
23
|
+
const response = await client.get(`/views/${encodeURIComponent(view)}/${encodeURIComponent(display)}`, { ...rest, query: { params: query } });
|
|
24
|
+
const count = response.meta.count;
|
|
25
|
+
return {
|
|
26
|
+
data: response.data ?? [],
|
|
27
|
+
count: count === undefined || count === null ? null : Number(count),
|
|
28
|
+
meta: response.meta,
|
|
29
|
+
links: response.links,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=views.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"views.js","sourceRoot":"","sources":["../src/views.ts"],"names":[],"mappings":"AAuBA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAe,MAAqB,EAAE,IAAY,EAAE,OAAe,EAAE,UAAuB,EAAE;IACzH,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAC1E,MAAM,KAAK,GAAsC,EAAE,GAAG,MAAM,EAAE,CAAC;IAC/D,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,CAAC;QAC1D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,KAAK,CAAC,gBAAgB,IAAI,KAAK,CAAC,GAAG,KAAK,CAAC;;YAC9D,KAAK,CAAC,gBAAgB,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC;IAC9C,CAAC;IACD,IAAI,IAAI,EAAE,MAAM;QAAE,KAAK,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAAC;IACnD,IAAI,IAAI,EAAE,CAAC;QACT,KAAK,CAAC,qBAAqB,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,KAAK;YAAE,KAAK,CAAC,wBAAwB,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC;IAC/D,CAAC;IACD,IAAI,IAAI,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAElD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,GAAG,CAAM,UAAU,kBAAkB,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IACnJ,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;IAClC,OAAO;QACL,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAI,EAAE;QACzB,KAAK,EAAE,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QACnE,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,KAAK,EAAE,QAAQ,CAAC,KAAK;KACtB,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsondrop/drupal",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Drupal core entity types and helpers (decoupled_router, menus, views, images) for @jsondrop/core.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"drupal",
|
|
7
|
+
"jsonapi",
|
|
8
|
+
"decoupled",
|
|
9
|
+
"headless",
|
|
10
|
+
"typescript",
|
|
11
|
+
"decoupled_router"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Valentino Međimorec",
|
|
16
|
+
"url": "https://github.com/valicm"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/valicm/jsondrop.git",
|
|
21
|
+
"directory": "packages/drupal"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/valicm/jsondrop/tree/main/packages/drupal#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/valicm/jsondrop/issues"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"@jsondrop/source": "./src/index.ts",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"main": "./dist/index.js",
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"src"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -p tsconfig.build.json"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@jsondrop/core": "^0.1.1"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@jsondrop/core": "*"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=20.19"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/account.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { sessionCookie, type JsonApiClient, type ResourceRequestOptions } from '@jsondrop/core';
|
|
2
|
+
import type { User } from './entities.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The current user, from Drupal core alone: an authenticated request to the
|
|
6
|
+
* JSON:API entry point carries `meta.links.me` with the user's UUID. Works
|
|
7
|
+
* with any auth (OAuth bearer token, session cookie). NULL when anonymous.
|
|
8
|
+
*
|
|
9
|
+
* Two requests: the entry point, then the user (with `options`).
|
|
10
|
+
*/
|
|
11
|
+
export async function getCurrentUser<T = User>(client: JsonApiClient, options: ResourceRequestOptions = {}): Promise<T | null> {
|
|
12
|
+
const id = await getCurrentUserId(client, options);
|
|
13
|
+
return id ? client.getResource('user--user', id, options) as Promise<T> : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The current user's UUID, or NULL when anonymous (one request). */
|
|
17
|
+
export async function getCurrentUserId(client: JsonApiClient, options: Pick<ResourceRequestOptions, 'auth' | 'headers' | 'fetchOptions' | 'signal'> = {}): Promise<string | null> {
|
|
18
|
+
const { auth, headers, fetchOptions, signal } = options;
|
|
19
|
+
const response = await client.request('GET', '', { auth, headers, fetchOptions, signal });
|
|
20
|
+
const me = (response.meta.links as { me?: { meta?: { id?: string } } } | undefined)?.me;
|
|
21
|
+
return me?.meta?.id ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cookie session login through Drupal core's `/user/login?_format=json`,
|
|
26
|
+
* for sites without simple_oauth. Server-side only: it reads the session
|
|
27
|
+
* cookie from `Set-Cookie`, which browsers hide from scripts.
|
|
28
|
+
*
|
|
29
|
+
* Keep `cookie` in an httpOnly cookie of your app (never hand Drupal's own
|
|
30
|
+
* cookie to the browser) and send it with `sessionCookie(cookie)` as auth.
|
|
31
|
+
* Keep `logoutToken` too: logging out needs it.
|
|
32
|
+
*/
|
|
33
|
+
export interface SessionLogin {
|
|
34
|
+
/** The session cookie as a `Cookie` header value (`SSESS…=…`). */
|
|
35
|
+
cookie: string;
|
|
36
|
+
/** The user as Drupal reports them; fields the user cannot view are absent. */
|
|
37
|
+
user: { uid?: string; name?: string; roles?: string[] };
|
|
38
|
+
/** For requests that need Drupal's CSRF token (`X-CSRF-Token`), e.g. core REST writes. */
|
|
39
|
+
csrfToken: string;
|
|
40
|
+
/** Pass to {@link logout}. */
|
|
41
|
+
logoutToken: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const JSON_HEADERS = { 'Content-Type': 'application/json', Accept: 'application/json' };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Logs in with a username and password. Wrong credentials and blocked
|
|
48
|
+
* accounts throw a 400; too many attempts (flood control) a 403 or 429.
|
|
49
|
+
* The error's `message` is Drupal's explanation.
|
|
50
|
+
*/
|
|
51
|
+
export async function login(client: JsonApiClient, credentials: { name: string; pass: string }): Promise<SessionLogin> {
|
|
52
|
+
const response = await client.post<{ current_user?: SessionLogin['user']; csrf_token?: string; logout_token?: string } | null>(client.siteUrl('/user/login'), credentials, {
|
|
53
|
+
auth: false,
|
|
54
|
+
query: { params: { _format: 'json' } },
|
|
55
|
+
headers: JSON_HEADERS,
|
|
56
|
+
parse: 'json',
|
|
57
|
+
});
|
|
58
|
+
const body = response.data ?? {};
|
|
59
|
+
const cookie = sessionCookieOf(response.headers);
|
|
60
|
+
if (!cookie) {
|
|
61
|
+
throw new Error('Drupal answered the login without a session cookie. Log in from the server, not the browser.');
|
|
62
|
+
}
|
|
63
|
+
return { cookie, user: body.current_user ?? {}, csrfToken: body.csrf_token ?? '', logoutToken: body.logout_token ?? '' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Ends the Drupal session (`/user/logout`, with the logout token from {@link login}). */
|
|
67
|
+
export async function logout(client: JsonApiClient, session: Pick<SessionLogin, 'cookie' | 'logoutToken'>): Promise<void> {
|
|
68
|
+
await client.post(client.siteUrl('/user/logout'), undefined, {
|
|
69
|
+
auth: sessionCookie(session.cookie),
|
|
70
|
+
query: { params: { _format: 'json', token: session.logoutToken } },
|
|
71
|
+
headers: JSON_HEADERS,
|
|
72
|
+
parse: 'json',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Whether the session is still logged in (`/user/login_status`). */
|
|
77
|
+
export async function isLoggedIn(client: JsonApiClient, cookie: string): Promise<boolean> {
|
|
78
|
+
const response = await client.get<number | string | null>(client.siteUrl('/user/login_status'), {
|
|
79
|
+
auth: sessionCookie(cookie),
|
|
80
|
+
query: { params: { _format: 'json' } },
|
|
81
|
+
headers: JSON_HEADERS,
|
|
82
|
+
parse: 'json',
|
|
83
|
+
});
|
|
84
|
+
// Drupal answers a bare `1` or `0`.
|
|
85
|
+
return Number(response.data) === 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A CSRF token (`/session/token`), for the `X-CSRF-Token` header that
|
|
90
|
+
* Drupal requires on writes to core REST resources (webform_rest, for
|
|
91
|
+
* one) when a session cookie authenticates them. Requests with a bearer
|
|
92
|
+
* token, or anonymous ones, do not need it.
|
|
93
|
+
*/
|
|
94
|
+
export async function getCsrfToken(client: JsonApiClient, cookie?: string): Promise<string> {
|
|
95
|
+
const response = await client.get<string>(client.siteUrl('/session/token'), {
|
|
96
|
+
auth: cookie ? sessionCookie(cookie) : false,
|
|
97
|
+
headers: { Accept: 'text/plain' },
|
|
98
|
+
parse: 'text',
|
|
99
|
+
});
|
|
100
|
+
return response.data.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Mails a password reset link (`/user/password`). Answers the same whether
|
|
105
|
+
* or not the account exists, so it does not reveal accounts.
|
|
106
|
+
*/
|
|
107
|
+
export async function requestPasswordReset(client: JsonApiClient, account: { name: string } | { mail: string }): Promise<void> {
|
|
108
|
+
await client.post(client.siteUrl('/user/password'), account, {
|
|
109
|
+
auth: false,
|
|
110
|
+
query: { params: { _format: 'json' } },
|
|
111
|
+
headers: JSON_HEADERS,
|
|
112
|
+
parse: 'json',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Drupal's session cookie(s) from a response, as a `Cookie` header value. */
|
|
117
|
+
function sessionCookieOf(headers: Headers): string | null {
|
|
118
|
+
const setCookies = typeof headers.getSetCookie === 'function'
|
|
119
|
+
? headers.getSetCookie()
|
|
120
|
+
: (headers.get('set-cookie') ?? '').split(/,(?=\s*[^;,]+=)/);
|
|
121
|
+
const pairs = setCookies
|
|
122
|
+
.map((line) => line.split(';')[0]?.trim() ?? '')
|
|
123
|
+
.filter((pair) => /^S?SESS[^=]*=./.test(pair));
|
|
124
|
+
return pairs.length ? pairs.join('; ') : null;
|
|
125
|
+
}
|