@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/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,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"}
@@ -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.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
+ }