@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/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"}
@@ -0,0 +1,8 @@
1
+ export * from './account.js';
2
+ export * from './entities.js';
3
+ export * from './fields.js';
4
+ export * from './files.js';
5
+ export * from './menu.js';
6
+ export * from './router.js';
7
+ export * from './views.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -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,8 @@
1
+ export * from './account.js';
2
+ export * from './entities.js';
3
+ export * from './fields.js';
4
+ export * from './files.js';
5
+ export * from './menu.js';
6
+ export * from './router.js';
7
+ export * from './views.js';
8
+ //# sourceMappingURL=index.js.map
@@ -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
@@ -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"}
@@ -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"}
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }