@jsondrop/drupal 0.0.0-stage → 0.1.0
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 +58 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +78 -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 +51 -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 +106 -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 +88 -0
- package/src/views.ts +50 -0
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,CAelI;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,51 @@
|
|
|
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
|
+
});
|
|
13
|
+
return response.document;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (isJsonApiError(error) && error.status === 404) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolves a path and fetches the entity there, in two requests. The entity
|
|
24
|
+
* is fetched from the URL decoupled_router gives, which respects resource
|
|
25
|
+
* paths renamed with JSON:API Extras and the path's language.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* const page = await getResourceByPath<Node>(drupal, params.slug, { query: { include: ['uid'] } });
|
|
29
|
+
* if (!page) notFound();
|
|
30
|
+
* if (page.route.redirect?.length) redirect(page.route.redirect.at(-1)!.to);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export async function getResourceByPath(client, path, options = {}) {
|
|
34
|
+
const route = await translatePath(client, path, options);
|
|
35
|
+
if (!route) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (!route.jsonapi?.individual) {
|
|
39
|
+
return { route, resource: null };
|
|
40
|
+
}
|
|
41
|
+
const response = await client.get(route.jsonapi.individual, options);
|
|
42
|
+
return { route, resource: response.data };
|
|
43
|
+
}
|
|
44
|
+
function headersObject(headers) {
|
|
45
|
+
const result = {};
|
|
46
|
+
new Headers(headers).forEach((value, name) => {
|
|
47
|
+
result[name] = value;
|
|
48
|
+
});
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
//# 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,CAAC,MAAM,CAAC,OAAO,CAAC,wBAAwB,EAAE,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE;YACrG,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;SAC3E,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,QAAqC,CAAC;IACxD,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.0",
|
|
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.0"
|
|
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,106 @@
|
|
|
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(client.siteUrl('/user/login'), credentials, {
|
|
53
|
+
auth: false,
|
|
54
|
+
query: { params: { _format: 'json' } },
|
|
55
|
+
headers: JSON_HEADERS,
|
|
56
|
+
});
|
|
57
|
+
const body = response.document as unknown as { current_user?: SessionLogin['user']; csrf_token?: string; logout_token?: string };
|
|
58
|
+
const cookie = sessionCookieOf(response.headers);
|
|
59
|
+
if (!cookie) {
|
|
60
|
+
throw new Error('Drupal answered the login without a session cookie. Log in from the server, not the browser.');
|
|
61
|
+
}
|
|
62
|
+
return { cookie, user: body.current_user ?? {}, csrfToken: body.csrf_token ?? '', logoutToken: body.logout_token ?? '' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Ends the Drupal session (`/user/logout`, with the logout token from {@link login}). */
|
|
66
|
+
export async function logout(client: JsonApiClient, session: Pick<SessionLogin, 'cookie' | 'logoutToken'>): Promise<void> {
|
|
67
|
+
await client.post(client.siteUrl('/user/logout'), undefined, {
|
|
68
|
+
auth: sessionCookie(session.cookie),
|
|
69
|
+
query: { params: { _format: 'json', token: session.logoutToken } },
|
|
70
|
+
headers: JSON_HEADERS,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Whether the session is still logged in (`/user/login_status`). */
|
|
75
|
+
export async function isLoggedIn(client: JsonApiClient, cookie: string): Promise<boolean> {
|
|
76
|
+
const response = await client.get(client.siteUrl('/user/login_status'), {
|
|
77
|
+
auth: sessionCookie(cookie),
|
|
78
|
+
query: { params: { _format: 'json' } },
|
|
79
|
+
headers: JSON_HEADERS,
|
|
80
|
+
});
|
|
81
|
+
// Drupal answers a bare `1` or `0`.
|
|
82
|
+
return Number(response.document) === 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Mails a password reset link (`/user/password`). Answers the same whether
|
|
87
|
+
* or not the account exists, so it does not reveal accounts.
|
|
88
|
+
*/
|
|
89
|
+
export async function requestPasswordReset(client: JsonApiClient, account: { name: string } | { mail: string }): Promise<void> {
|
|
90
|
+
await client.post(client.siteUrl('/user/password'), account, {
|
|
91
|
+
auth: false,
|
|
92
|
+
query: { params: { _format: 'json' } },
|
|
93
|
+
headers: JSON_HEADERS,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Drupal's session cookie(s) from a response, as a `Cookie` header value. */
|
|
98
|
+
function sessionCookieOf(headers: Headers): string | null {
|
|
99
|
+
const setCookies = typeof headers.getSetCookie === 'function'
|
|
100
|
+
? headers.getSetCookie()
|
|
101
|
+
: (headers.get('set-cookie') ?? '').split(/,(?=\s*[^;,]+=)/);
|
|
102
|
+
const pairs = setCookies
|
|
103
|
+
.map((line) => line.split(';')[0]?.trim() ?? '')
|
|
104
|
+
.filter((pair) => /^S?SESS[^=]*=./.test(pair));
|
|
105
|
+
return pairs.length ? pairs.join('; ') : null;
|
|
106
|
+
}
|