@crombolo/delivery-client 0.36.2 → 0.37.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.d.ts +79 -0
- package/dist/index.js +78 -3
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export type ApiContentReference<Alias extends string = string, Properties = Reco
|
|
|
33
33
|
export interface ApiContentResponse<Alias extends string = string, Properties = Record<string, unknown>> extends ApiContent<Alias, Properties> {
|
|
34
34
|
/** Its route in each culture; empty for a page that does not vary. */
|
|
35
35
|
cultures: Record<string, ApiRoute>;
|
|
36
|
+
/** Search and sharing details, when asked for with `seo: true`. */
|
|
37
|
+
seo?: ApiSeo;
|
|
36
38
|
}
|
|
37
39
|
/** A block or picked element: its properties are always there. */
|
|
38
40
|
export interface ApiElement<Alias extends string = string, Properties = Record<string, unknown>> {
|
|
@@ -205,6 +207,62 @@ type Narrowed<Item> = Item extends {
|
|
|
205
207
|
type Fielded<Item, Options> = Options extends {
|
|
206
208
|
fields: string;
|
|
207
209
|
} ? Narrowed<Item> : Item;
|
|
210
|
+
/** A page's search and sharing details, with `seo: true` (story 055). */
|
|
211
|
+
export interface ApiSeo {
|
|
212
|
+
title: string;
|
|
213
|
+
description: string | null;
|
|
214
|
+
image: {
|
|
215
|
+
url: string;
|
|
216
|
+
width: number | null;
|
|
217
|
+
height: number | null;
|
|
218
|
+
} | null;
|
|
219
|
+
canonical: string | null;
|
|
220
|
+
robots: string | null;
|
|
221
|
+
alternates: Array<{
|
|
222
|
+
culture: string;
|
|
223
|
+
url: string;
|
|
224
|
+
}>;
|
|
225
|
+
xDefault: string | null;
|
|
226
|
+
siteName: string;
|
|
227
|
+
}
|
|
228
|
+
/** One page `sitemap(host)` lists. */
|
|
229
|
+
export interface ApiSitemapItem {
|
|
230
|
+
id: string;
|
|
231
|
+
url: string;
|
|
232
|
+
lastmod: string;
|
|
233
|
+
alternates: Array<{
|
|
234
|
+
culture: string;
|
|
235
|
+
url: string;
|
|
236
|
+
}>;
|
|
237
|
+
}
|
|
238
|
+
/** What a request learned, for a cache that tags what it keeps. */
|
|
239
|
+
export interface DeliveryAnswer {
|
|
240
|
+
url: string;
|
|
241
|
+
status: number;
|
|
242
|
+
/** Every page and media key the answer holds, lower case. */
|
|
243
|
+
keys: string[];
|
|
244
|
+
/** The purge tags the site sent, when it sends them. */
|
|
245
|
+
tags: string[];
|
|
246
|
+
etag: string | null;
|
|
247
|
+
cacheControl: string | null;
|
|
248
|
+
/** The answer came from a kept copy after a `304`. */
|
|
249
|
+
revalidated: boolean;
|
|
250
|
+
}
|
|
251
|
+
/** Copies kept for `If-None-Match`, by address. */
|
|
252
|
+
export interface ConditionalStore {
|
|
253
|
+
get(url: string): {
|
|
254
|
+
etag: string;
|
|
255
|
+
body: string;
|
|
256
|
+
} | undefined;
|
|
257
|
+
set(url: string, copy: {
|
|
258
|
+
etag: string;
|
|
259
|
+
body: string;
|
|
260
|
+
}): void;
|
|
261
|
+
}
|
|
262
|
+
/** A store of the most recent `limit` copies, in memory. */
|
|
263
|
+
export declare function memoryConditionalStore(limit?: number): ConditionalStore;
|
|
264
|
+
/** Every page and media key in an answer, lower case: items, pickers, links, list entries. */
|
|
265
|
+
export declare function contentKeysIn(body: unknown): string[];
|
|
208
266
|
export interface RequestOptions {
|
|
209
267
|
/** A culture name, such as `en-US`; otherwise the client's, then the site's default. */
|
|
210
268
|
culture?: string;
|
|
@@ -214,6 +272,8 @@ export interface RequestOptions {
|
|
|
214
272
|
expand?: string;
|
|
215
273
|
/** Which properties to include: `properties[title,image]`. */
|
|
216
274
|
fields?: string;
|
|
275
|
+
/** Adds each page's search and sharing details as `seo`. */
|
|
276
|
+
seo?: boolean;
|
|
217
277
|
signal?: AbortSignal;
|
|
218
278
|
}
|
|
219
279
|
export interface ContentQuery extends RequestOptions {
|
|
@@ -261,6 +321,21 @@ export interface DeliveryClientOptions {
|
|
|
261
321
|
memberToken?: () => string | undefined | Promise<string | undefined>;
|
|
262
322
|
/** Headers sent with every request. */
|
|
263
323
|
headers?: Record<string, string>;
|
|
324
|
+
/**
|
|
325
|
+
* Drafts instead of what is published (story 053): `Preview: true`, with the
|
|
326
|
+
* editor's preview token when there is one. Asked for on every request.
|
|
327
|
+
*/
|
|
328
|
+
preview?: () => boolean | {
|
|
329
|
+
token?: string;
|
|
330
|
+
} | undefined | Promise<boolean | {
|
|
331
|
+
token?: string;
|
|
332
|
+
} | undefined>;
|
|
333
|
+
/** Told about every answer: its keys and tags, for a cache to tag what it keeps. */
|
|
334
|
+
onAnswer?: (answer: DeliveryAnswer) => void;
|
|
335
|
+
/** Keeps copies and revalidates them with `If-None-Match`; a `304` reuses the copy. */
|
|
336
|
+
conditional?: ConditionalStore;
|
|
337
|
+
/** Where the site sends purge tags; `Surrogate-Key` and `Cache-Tag` are read when unset. */
|
|
338
|
+
tagHeader?: string;
|
|
264
339
|
/** Defaults to the global `fetch`. */
|
|
265
340
|
fetch?: (input: string, init: RequestInit) => Promise<Response>;
|
|
266
341
|
}
|
|
@@ -294,6 +369,10 @@ export interface DeliveryClient<Schema extends DeliverySchema = DeliverySchema>
|
|
|
294
369
|
route(path: string, options?: Omit<RequestOptions, 'culture' | 'fields'> & {
|
|
295
370
|
host?: string;
|
|
296
371
|
}): Promise<RouteAnswer<Schema>>;
|
|
372
|
+
/** The pages `/sitemap.xml` lists for a hostname; undefined when the site serves none. */
|
|
373
|
+
sitemap(host?: string, options?: Pick<RequestOptions, 'signal'>): Promise<Paged<ApiSitemapItem> | undefined>;
|
|
374
|
+
/** A form to draw, by key or alias (story 056); undefined when there is none. */
|
|
375
|
+
form(keyOrAlias: string, options?: Pick<RequestOptions, 'culture' | 'signal'>): Promise<Record<string, unknown> | undefined>;
|
|
297
376
|
}
|
|
298
377
|
/** Where a crombolo site serves it; Umbraco's own path, `/umbraco/delivery/api/v2`, answers too. */
|
|
299
378
|
export declare const DELIVERY_API_PATH = "/crombolo/delivery/api/v2";
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
function isExpanded(reference) {
|
|
3
3
|
return Object.keys(reference.properties).length > 0;
|
|
4
4
|
}
|
|
5
|
+
function memoryConditionalStore(limit = 500) {
|
|
6
|
+
const copies = new Map;
|
|
7
|
+
return {
|
|
8
|
+
get(url) {
|
|
9
|
+
const copy = copies.get(url);
|
|
10
|
+
if (copy) {
|
|
11
|
+
copies.delete(url);
|
|
12
|
+
copies.set(url, copy);
|
|
13
|
+
}
|
|
14
|
+
return copy;
|
|
15
|
+
},
|
|
16
|
+
set(url, copy) {
|
|
17
|
+
copies.delete(url);
|
|
18
|
+
copies.set(url, copy);
|
|
19
|
+
while (copies.size > limit)
|
|
20
|
+
copies.delete(copies.keys().next().value);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
25
|
+
function contentKeysIn(body) {
|
|
26
|
+
const keys = new Set;
|
|
27
|
+
const walk = (value) => {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
for (const item of value)
|
|
30
|
+
walk(item);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!value || typeof value !== "object")
|
|
34
|
+
return;
|
|
35
|
+
const record = value;
|
|
36
|
+
const id = record.id;
|
|
37
|
+
if (typeof id === "string" && UUID.test(id) && (("contentType" in record) || ("mediaType" in record)))
|
|
38
|
+
keys.add(id.toLowerCase());
|
|
39
|
+
if (typeof record.destinationId === "string" && UUID.test(record.destinationId))
|
|
40
|
+
keys.add(record.destinationId.toLowerCase());
|
|
41
|
+
for (const child of Object.values(record))
|
|
42
|
+
walk(child);
|
|
43
|
+
};
|
|
44
|
+
walk(body);
|
|
45
|
+
return [...keys];
|
|
46
|
+
}
|
|
5
47
|
|
|
6
48
|
class DeliveryError extends Error {
|
|
7
49
|
status;
|
|
@@ -34,11 +76,39 @@ function createDeliveryClient(options) {
|
|
|
34
76
|
const token = await options.memberToken?.();
|
|
35
77
|
if (token)
|
|
36
78
|
headers.authorization = `Bearer ${token}`;
|
|
79
|
+
const preview = await options.preview?.();
|
|
80
|
+
if (preview) {
|
|
81
|
+
headers.preview = "true";
|
|
82
|
+
if (typeof preview === "object" && preview.token)
|
|
83
|
+
headers["preview-token"] = preview.token;
|
|
84
|
+
}
|
|
85
|
+
const keeps = options.conditional && !token && !preview ? options.conditional : undefined;
|
|
86
|
+
const kept = keeps?.get(url);
|
|
87
|
+
if (kept)
|
|
88
|
+
headers["if-none-match"] = kept.etag;
|
|
37
89
|
const response = await send(url, { headers, signal: request.signal });
|
|
38
90
|
if (response.status === 404 && missing === "undefined")
|
|
39
91
|
return;
|
|
40
|
-
const
|
|
92
|
+
const revalidated = response.status === 304 && kept !== undefined;
|
|
93
|
+
const text = revalidated ? kept.body : await response.text();
|
|
41
94
|
const body = text ? JSON.parse(text) : null;
|
|
95
|
+
const etag = revalidated ? kept.etag : response.headers.get("etag");
|
|
96
|
+
if (keeps && etag && response.ok && !revalidated)
|
|
97
|
+
keeps.set(url, { etag, body: text });
|
|
98
|
+
if (options.onAnswer) {
|
|
99
|
+
const tagged = response.headers.get(options.tagHeader ?? "surrogate-key") ?? (options.tagHeader ? null : response.headers.get("cache-tag"));
|
|
100
|
+
options.onAnswer({
|
|
101
|
+
url,
|
|
102
|
+
status: revalidated ? 200 : response.status,
|
|
103
|
+
keys: contentKeysIn(body),
|
|
104
|
+
tags: tagged ? tagged.split(/[\s,]+/).filter(Boolean) : [],
|
|
105
|
+
etag,
|
|
106
|
+
cacheControl: response.headers.get("cache-control"),
|
|
107
|
+
revalidated
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (revalidated)
|
|
111
|
+
return body;
|
|
42
112
|
if (response.status === 404 && missing === "body")
|
|
43
113
|
return body;
|
|
44
114
|
if (!response.ok)
|
|
@@ -47,7 +117,8 @@ function createDeliveryClient(options) {
|
|
|
47
117
|
}
|
|
48
118
|
const shaping = (request) => [
|
|
49
119
|
...request.expand ? [["expand", request.expand]] : [],
|
|
50
|
-
...request.fields ? [["fields", request.fields]] : []
|
|
120
|
+
...request.fields ? [["fields", request.fields]] : [],
|
|
121
|
+
...request.seo ? [["seo", "true"]] : []
|
|
51
122
|
];
|
|
52
123
|
const paging = (query) => [
|
|
53
124
|
...list(query.filter).map((filter) => ["filter", filter]),
|
|
@@ -72,6 +143,8 @@ function createDeliveryClient(options) {
|
|
|
72
143
|
...request.host ? [["host", request.host]] : [],
|
|
73
144
|
...shaping(request)
|
|
74
145
|
], request, "body"),
|
|
146
|
+
sitemap: (host, request = {}) => get("sitemap", host ? [["host", host]] : [], request, "undefined"),
|
|
147
|
+
form: (keyOrAlias, request = {}) => get(`forms/${encodeURIComponent(keyOrAlias)}`, [], request, "undefined"),
|
|
75
148
|
search: (term, search = {}) => get("search", [
|
|
76
149
|
["term", term],
|
|
77
150
|
...list(search.contentType).map((type) => ["contentType", type]),
|
|
@@ -82,6 +155,8 @@ function createDeliveryClient(options) {
|
|
|
82
155
|
export {
|
|
83
156
|
DELIVERY_API_PATH,
|
|
84
157
|
DeliveryError,
|
|
158
|
+
contentKeysIn,
|
|
85
159
|
createDeliveryClient,
|
|
86
|
-
isExpanded
|
|
160
|
+
isExpanded,
|
|
161
|
+
memoryConditionalStore
|
|
87
162
|
};
|
package/package.json
CHANGED