@codewheel/jsonapi-frontend-client 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,6 +61,26 @@ await resolvePath("/about-us", {
61
61
  })
62
62
  ```
63
63
 
64
+ ### Caching note (Next.js / SSR)
65
+
66
+ If you pass `Authorization` (or `Cookie`) headers, this client defaults to `cache: "no-store"` unless you explicitly set `options.init.cache`.
67
+
68
+ ## Static builds (SSG) routes feed (optional)
69
+
70
+ If you enable the secret-protected routes feed in Drupal (`/jsonapi/routes`), you can fetch a complete build-time list of headless paths by following `links.next`:
71
+
72
+ ```ts
73
+ import { collectRoutes } from "@codewheel/jsonapi-frontend-client"
74
+
75
+ const baseUrl = process.env.DRUPAL_BASE_URL!
76
+ const secret = process.env.ROUTES_FEED_SECRET!
77
+
78
+ const routes = await collectRoutes({ baseUrl, secret })
79
+ const paths = routes.map((r) => r.path)
80
+ ```
81
+
82
+ Keep `ROUTES_FEED_SECRET` server-side only (build environment variables). Do not expose it to browsers.
83
+
64
84
  ## Query building (optional)
65
85
 
66
86
  This client doesn’t require a query builder, but `drupal-jsonapi-params` works well:
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAuD,MAAM,gBAAgB,CAAA;AAC1G,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAmBlE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAa5D;AAoBD,wBAAsB,YAAY,CAAC,CAAC,GAAG,eAAe,EACpD,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,GACA,OAAO,CAAC,CAAC,CAAC,CAsCZ;AAED,wBAAsB,SAAS,CAAC,CAAC,GAAG,eAAe,EACjD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACnD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,GACA,OAAO,CAAC,CAAC,CAAC,CAyCZ"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAuD,MAAM,gBAAgB,CAAA;AAC1G,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAmBlE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAa5D;AAwBD,wBAAsB,YAAY,CAAC,CAAC,GAAG,eAAe,EACpD,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,GACA,OAAO,CAAC,CAAC,CAAC,CAuDZ;AAED,wBAAsB,SAAS,CAAC,CAAC,GAAG,eAAe,EACjD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACnD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,GACA,OAAO,CAAC,CAAC,CAAC,CAyDZ"}
package/dist/fetch.js CHANGED
@@ -46,6 +46,9 @@ function buildSafeUrl(input, base, options) {
46
46
  }
47
47
  return url;
48
48
  }
49
+ function hasAuthLikeHeaders(headers) {
50
+ return headers.has("authorization") || headers.has("cookie");
51
+ }
49
52
  export async function fetchJsonApi(jsonapiPath, options) {
50
53
  const base = getDrupalBaseUrlFromOptions({ baseUrl: options?.baseUrl, envKey: options?.envKey });
51
54
  const fetcher = getFetch(options?.fetch);
@@ -63,15 +66,28 @@ export async function fetchJsonApi(jsonapiPath, options) {
63
66
  if (!headers.has("Accept")) {
64
67
  headers.set("Accept", "application/vnd.api+json");
65
68
  }
66
- const res = await fetcher(url.toString(), {
69
+ const hasExplicitCache = options?.init?.cache !== undefined;
70
+ const disableCaching = hasAuthLikeHeaders(headers) && !hasExplicitCache;
71
+ const init = {
67
72
  ...options?.init,
68
73
  headers,
69
- next: {
70
- ...(options?.init?.next ?? {}),
71
- revalidate: options?.revalidate,
72
- tags: Array.from(new Set([...(options?.init?.next?.tags ?? []), ...tags])),
73
- },
74
- });
74
+ };
75
+ if (disableCaching) {
76
+ init.cache = "no-store";
77
+ // Avoid mixing Next.js cache options with no-store.
78
+ delete init.next;
79
+ }
80
+ const fetchInit = disableCaching
81
+ ? init
82
+ : {
83
+ ...init,
84
+ next: {
85
+ ...(options?.init?.next ?? {}),
86
+ revalidate: options?.revalidate,
87
+ tags: Array.from(new Set([...(options?.init?.next?.tags ?? []), ...tags])),
88
+ },
89
+ };
90
+ const res = await fetcher(url.toString(), fetchInit);
75
91
  if (!res.ok) {
76
92
  throw new Error(`JSON:API fetch failed: ${res.status} ${res.statusText}`);
77
93
  }
@@ -99,15 +115,27 @@ export async function fetchView(dataUrl, options) {
99
115
  if (!headers.has("Accept")) {
100
116
  headers.set("Accept", "application/vnd.api+json");
101
117
  }
102
- const res = await fetcher(url.toString(), {
118
+ const hasExplicitCache = options?.init?.cache !== undefined;
119
+ const disableCaching = hasAuthLikeHeaders(headers) && !hasExplicitCache;
120
+ const init = {
103
121
  ...options?.init,
104
122
  headers,
105
- next: {
106
- ...(options?.init?.next ?? {}),
107
- revalidate: options?.revalidate,
108
- tags: Array.from(new Set([...(options?.init?.next?.tags ?? []), ...tags])),
109
- },
110
- });
123
+ };
124
+ if (disableCaching) {
125
+ init.cache = "no-store";
126
+ delete init.next;
127
+ }
128
+ const fetchInit = disableCaching
129
+ ? init
130
+ : {
131
+ ...init,
132
+ next: {
133
+ ...(options?.init?.next ?? {}),
134
+ revalidate: options?.revalidate,
135
+ tags: Array.from(new Set([...(options?.init?.next?.tags ?? []), ...tags])),
136
+ },
137
+ };
138
+ const res = await fetcher(url.toString(), fetchInit);
111
139
  if (!res.ok) {
112
140
  throw new Error(`View fetch failed: ${res.status} ${res.statusText}`);
113
141
  }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export { resolvePath } from "./resolve.js";
2
2
  export { fetchJsonApi, fetchView } from "./fetch.js";
3
+ export { fetchRoutesPage, iterateRoutes, collectRoutes } from "./routes.js";
3
4
  export { getDrupalBaseUrl, resolveFileUrl, getFileUrl, getImageStyleUrl } from "./url.js";
4
5
  export { findIncluded, findIncludedByRelationship, findIncludedByRelationshipMultiple, extractImageFromFile, extractMedia, extractMediaField, extractPrimaryImage, extractEmbeddedMediaUuids, parseDrupalMediaTag, } from "./media.js";
5
- export type { ResolveResponse, JsonApiDocument, JsonApiResource, JsonApiRelationship, JsonApiLinks, NodeAttributes, } from "./types.js";
6
+ export type { ResolveResponse, RoutesFeedItem, RoutesFeedResponse, JsonApiDocument, JsonApiResource, JsonApiRelationship, JsonApiLinks, NodeAttributes, } from "./types.js";
6
7
  export type { DrupalImageData, DrupalMediaData } from "./media.js";
7
8
  export type { FetchInit, FetchLike } from "./transport.js";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEpD,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAEzF,OAAO,EACL,YAAY,EACZ,0BAA0B,EAC1B,kCAAkC,EAClC,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,YAAY,CAAA;AAEnB,YAAY,EACV,eAAe,EACf,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,cAAc,GACf,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAElE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3E,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAEzF,OAAO,EACL,YAAY,EACZ,0BAA0B,EAC1B,kCAAkC,EAClC,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,YAAY,CAAA;AAEnB,YAAY,EACV,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,cAAc,GACf,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAElE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { resolvePath } from "./resolve.js";
2
2
  export { fetchJsonApi, fetchView } from "./fetch.js";
3
+ export { fetchRoutesPage, iterateRoutes, collectRoutes } from "./routes.js";
3
4
  export { getDrupalBaseUrl, resolveFileUrl, getFileUrl, getImageStyleUrl } from "./url.js";
4
5
  export { findIncluded, findIncludedByRelationship, findIncludedByRelationshipMultiple, extractImageFromFile, extractMedia, extractMediaField, extractPrimaryImage, extractEmbeddedMediaUuids, parseDrupalMediaTag, } from "./media.js";
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAuD,MAAM,gBAAgB,CAAA;AAE1G;;GAEG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,GACA,OAAO,CAAC,eAAe,CAAC,CA8B1B"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAuD,MAAM,gBAAgB,CAAA;AAE1G;;GAEG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,GACA,OAAO,CAAC,eAAe,CAAC,CA8C1B"}
package/dist/resolve.js CHANGED
@@ -15,14 +15,26 @@ export async function resolvePath(path, options) {
15
15
  if (!headers.has("Accept")) {
16
16
  headers.set("Accept", "application/vnd.api+json");
17
17
  }
18
- const res = await fetcher(url.toString(), {
18
+ const hasExplicitCache = options?.init?.cache !== undefined;
19
+ const disableCaching = (headers.has("authorization") || headers.has("cookie")) && !hasExplicitCache;
20
+ const init = {
19
21
  ...options?.init,
20
22
  headers,
21
- next: {
22
- ...(options?.init?.next ?? {}),
23
- revalidate: options?.revalidate,
24
- },
25
- });
23
+ };
24
+ if (disableCaching) {
25
+ init.cache = "no-store";
26
+ delete init.next;
27
+ }
28
+ const fetchInit = disableCaching
29
+ ? init
30
+ : {
31
+ ...init,
32
+ next: {
33
+ ...(options?.init?.next ?? {}),
34
+ revalidate: options?.revalidate,
35
+ },
36
+ };
37
+ const res = await fetcher(url.toString(), fetchInit);
26
38
  if (!res.ok) {
27
39
  throw new Error(`Resolver failed: ${res.status} ${res.statusText}`);
28
40
  }
@@ -0,0 +1,40 @@
1
+ import { FetchInit, FetchLike } from "./transport.js";
2
+ import type { RoutesFeedItem, RoutesFeedResponse } from "./types.js";
3
+ /**
4
+ * Fetch a single page from the build-time routes feed (/jsonapi/routes).
5
+ *
6
+ * This endpoint is typically protected by X-Routes-Secret. Keep secrets
7
+ * server-side only.
8
+ */
9
+ export declare function fetchRoutesPage(options?: {
10
+ baseUrl?: string;
11
+ envKey?: string;
12
+ langcode?: string;
13
+ /**
14
+ * Page size (default: 50). This maps to page[limit] for the first request.
15
+ *
16
+ * Subsequent pages follow links.next from the server.
17
+ */
18
+ limit?: number;
19
+ /**
20
+ * Provide a pagination URL (usually links.next from a previous response).
21
+ * If provided, it takes precedence over limit/langcode and is fetched as-is.
22
+ */
23
+ url?: string;
24
+ /** Routes feed secret (sent as X-Routes-Secret). */
25
+ secret?: string;
26
+ fetch?: FetchLike;
27
+ headers?: HeadersInit;
28
+ init?: FetchInit;
29
+ }): Promise<RoutesFeedResponse>;
30
+ /**
31
+ * Iterate all routes by following links.next until it is null.
32
+ */
33
+ export declare function iterateRoutes(options?: Omit<Parameters<typeof fetchRoutesPage>[0], "url"> & {
34
+ maxPages?: number;
35
+ }): AsyncGenerator<RoutesFeedItem>;
36
+ /**
37
+ * Collect all routes into an array (convenience wrapper around iterateRoutes).
38
+ */
39
+ export declare function collectRoutes(options?: Parameters<typeof iterateRoutes>[0]): Promise<RoutesFeedItem[]>;
40
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAuD,MAAM,gBAAgB,CAAA;AAC1G,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAqDpE;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,OAAO,GAAE;IACP,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;CACZ,GACL,OAAO,CAAC,kBAAkB,CAAC,CA8D7B;AAED;;GAEG;AACH,wBAAuB,aAAa,CAClC,OAAO,GAAE,IAAI,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GACvF,cAAc,CAAC,cAAc,CAAC,CAiBhC;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,GAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC,CAAM,GAChD,OAAO,CAAC,cAAc,EAAE,CAAC,CAM3B"}
package/dist/routes.js ADDED
@@ -0,0 +1,133 @@
1
+ import { getDrupalBaseUrlFromOptions, getFetch, mergeHeaders } from "./transport.js";
2
+ function buildRoutesUrl(input, base) {
3
+ const baseUrl = new URL(base);
4
+ const url = new URL(input, baseUrl);
5
+ if (url.origin !== baseUrl.origin) {
6
+ throw new Error(`Refusing to fetch a URL from a different origin (${url.origin}) than base (${baseUrl.origin}). ` +
7
+ "This should not happen for /jsonapi/routes pagination.");
8
+ }
9
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
10
+ throw new Error(`Unsupported URL protocol "${url.protocol}" (expected http/https)`);
11
+ }
12
+ return url;
13
+ }
14
+ function normalizeRoutesItem(value) {
15
+ if (!value || typeof value !== "object")
16
+ return null;
17
+ const item = value;
18
+ const path = item.path;
19
+ const kind = item.kind;
20
+ const jsonapi_url = item.jsonapi_url;
21
+ const data_url = item.data_url;
22
+ if (typeof path !== "string" || path.trim() === "" || !path.startsWith("/"))
23
+ return null;
24
+ if (kind !== "entity" && kind !== "view")
25
+ return null;
26
+ if (kind === "entity") {
27
+ if (typeof jsonapi_url !== "string" || jsonapi_url.trim() === "")
28
+ return null;
29
+ if (data_url !== null)
30
+ return null;
31
+ return { path, kind, jsonapi_url, data_url: null };
32
+ }
33
+ if (kind === "view") {
34
+ if (typeof data_url !== "string" || data_url.trim() === "")
35
+ return null;
36
+ if (jsonapi_url !== null)
37
+ return null;
38
+ return { path, kind, jsonapi_url: null, data_url };
39
+ }
40
+ return null;
41
+ }
42
+ function getNextLink(links) {
43
+ if (!links || typeof links !== "object")
44
+ return null;
45
+ const next = links.next;
46
+ return typeof next === "string" && next.trim() !== "" ? next : null;
47
+ }
48
+ /**
49
+ * Fetch a single page from the build-time routes feed (/jsonapi/routes).
50
+ *
51
+ * This endpoint is typically protected by X-Routes-Secret. Keep secrets
52
+ * server-side only.
53
+ */
54
+ export async function fetchRoutesPage(options = {}) {
55
+ const base = getDrupalBaseUrlFromOptions({ baseUrl: options.baseUrl, envKey: options.envKey });
56
+ const fetcher = getFetch(options.fetch);
57
+ const url = options.url
58
+ ? buildRoutesUrl(options.url, base)
59
+ : (() => {
60
+ const u = buildRoutesUrl("/jsonapi/routes", base);
61
+ u.searchParams.set("_format", "json");
62
+ u.searchParams.set("page[limit]", String(options.limit ?? 50));
63
+ if (options.langcode) {
64
+ u.searchParams.set("langcode", options.langcode);
65
+ }
66
+ return u;
67
+ })();
68
+ const headers = mergeHeaders(options.init?.headers, options.headers);
69
+ if (!headers.has("Accept")) {
70
+ headers.set("Accept", "application/vnd.api+json");
71
+ }
72
+ if (options.secret && typeof options.secret === "string" && options.secret.trim() !== "") {
73
+ headers.set("X-Routes-Secret", options.secret.trim());
74
+ }
75
+ const init = {
76
+ ...options.init,
77
+ headers,
78
+ };
79
+ // This endpoint is secret-protected; disable caching by default.
80
+ if (options.init?.cache === undefined) {
81
+ init.cache = "no-store";
82
+ delete init.next;
83
+ }
84
+ const res = await fetcher(url.toString(), init);
85
+ if (!res.ok) {
86
+ throw new Error(`Routes feed failed: ${res.status} ${res.statusText}`);
87
+ }
88
+ const doc = (await res.json());
89
+ if (!doc || typeof doc !== "object") {
90
+ throw new Error("Routes feed returned invalid JSON");
91
+ }
92
+ const data = doc.data;
93
+ const rawItems = Array.isArray(data) ? data : [];
94
+ const items = rawItems.map(normalizeRoutesItem).filter((v) => v !== null);
95
+ const links = doc.links;
96
+ const meta = doc.meta;
97
+ return {
98
+ data: items,
99
+ links: {
100
+ self: typeof links?.self === "string" ? links.self : undefined,
101
+ next: getNextLink(links),
102
+ },
103
+ meta: (meta && typeof meta === "object" ? meta : undefined),
104
+ };
105
+ }
106
+ /**
107
+ * Iterate all routes by following links.next until it is null.
108
+ */
109
+ export async function* iterateRoutes(options = {}) {
110
+ const maxPages = options.maxPages ?? 10_000;
111
+ let pageUrl = undefined;
112
+ for (let i = 0; i < maxPages; i++) {
113
+ const page = await fetchRoutesPage({ ...options, url: pageUrl });
114
+ for (const item of page.data) {
115
+ yield item;
116
+ }
117
+ const next = page.links?.next;
118
+ if (!next)
119
+ return;
120
+ pageUrl = next;
121
+ }
122
+ throw new Error(`Routes feed exceeded maxPages=${maxPages}; aborting pagination`);
123
+ }
124
+ /**
125
+ * Collect all routes into an array (convenience wrapper around iterateRoutes).
126
+ */
127
+ export async function collectRoutes(options = {}) {
128
+ const items = [];
129
+ for await (const item of iterateRoutes(options)) {
130
+ items.push(item);
131
+ }
132
+ return items;
133
+ }
package/dist/types.d.ts CHANGED
@@ -42,6 +42,25 @@ export type ResolveResponse = {
42
42
  headless: false;
43
43
  drupal_url: null;
44
44
  };
45
+ export type RoutesFeedItem = {
46
+ path: string;
47
+ kind: "entity";
48
+ jsonapi_url: string;
49
+ data_url: null;
50
+ } | {
51
+ path: string;
52
+ kind: "view";
53
+ jsonapi_url: null;
54
+ data_url: string;
55
+ };
56
+ export interface RoutesFeedResponse {
57
+ data: RoutesFeedItem[];
58
+ links?: {
59
+ self?: string;
60
+ next?: string | null;
61
+ };
62
+ meta?: Record<string, unknown>;
63
+ }
45
64
  export interface JsonApiDocument<T = JsonApiResource> {
46
65
  data: T | T[];
47
66
  included?: JsonApiResource[];
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB;IACE,QAAQ,EAAE,IAAI,CAAA;IACd,IAAI,EAAE,QAAQ,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,EAAE,EAAE,MAAM,CAAA;QACV,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;IACD,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAChD,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,IAAI,CAAA;IACd,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,GACD;IACE,QAAQ,EAAE,IAAI,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,IAAI,CAAA;IACZ,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAChD,WAAW,EAAE,IAAI,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,GACD;IACE,QAAQ,EAAE,KAAK,CAAA;IACf,IAAI,EAAE,IAAI,CAAA;IACV,SAAS,EAAE,IAAI,CAAA;IACf,MAAM,EAAE,IAAI,CAAA;IACZ,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAChD,WAAW,EAAE,IAAI,CAAA;IACjB,QAAQ,EAAE,IAAI,CAAA;IACd,QAAQ,EAAE,KAAK,CAAA;IACf,UAAU,EAAE,IAAI,CAAA;CACjB,CAAA;AAEL,MAAM,MAAM,cAAc,GACtB;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,QAAQ,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,IAAI,CAAA;CACf,GACD;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,IAAI,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAEL,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,cAAc,EAAE,CAAA;IACtB,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KACrB,CAAA;IACD,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B;AAED,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,eAAe;IAClD,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAA;IACb,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAA;IAC5B,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;IACnD,KAAK,CAAC,EAAE,YAAY,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,IAAI,CAAA;IAC1E,KAAK,CAAC,EAAE,YAAY,CAAA;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAChC,OAAO,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACnC,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAChC,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,OAAO,CAAA;IACf,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;QACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;QAClB,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;IACD,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;QACd,SAAS,EAAE,MAAM,CAAA;QACjB,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codewheel/jsonapi-frontend-client",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "TypeScript client helpers for Drupal drupal/jsonapi_frontend",
5
5
  "homepage": "https://github.com/code-wheel/jsonapi-frontend-client#readme",
6
6
  "repository": {