@obelism/content 0.28.4 → 0.29.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.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { TokenSource } from './oidc.mjs';
2
- import { R as RichTextFormat, E as Entry, a as RedirectList, b as Redirect, c as EntryList, M as ModelFile, A as Asset, B as Block, I as ImproveBlock, S as ShopifyItem, L as LinkTarget, d as Link, e as EntryStub, f as RichTextNode } from './types-b6jbj3g6.js';
3
- export { g as AssetProduct, C as ContentTypeAccent, h as ContentTypeDefinition, i as ContentTypeKind, j as Environment, F as FieldCondition, k as FieldDefinition, l as FieldGroup, m as FieldType, n as RichTextDocument, o as RichTextMark } from './types-b6jbj3g6.js';
2
+ import { R as RichTextFormat, E as Entry, a as RedirectList, b as Redirect, c as EntryList, M as ModelFile, A as Asset, B as Block, I as ImproveBlock, S as ShopifyItem, L as LinkTarget, d as Link, e as EntryStub, f as RichTextNode } from './types-mqo1osjh.js';
3
+ export { g as AssetProduct, C as ContentTypeAccent, h as ContentTypeDefinition, i as ContentTypeKind, j as Environment, F as FieldCondition, k as FieldDefinition, l as FieldGroup, m as FieldType, n as RichTextDocument, o as RichTextMark } from './types-mqo1osjh.js';
4
4
 
5
5
  type ContentClientOptions = {
6
6
  token: TokenSource;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { TokenSource } from './oidc.js';
2
- import { R as RichTextFormat, E as Entry, a as RedirectList, b as Redirect, c as EntryList, M as ModelFile, A as Asset, B as Block, I as ImproveBlock, S as ShopifyItem, L as LinkTarget, d as Link, e as EntryStub, f as RichTextNode } from './types-b6jbj3g6.js';
3
- export { g as AssetProduct, C as ContentTypeAccent, h as ContentTypeDefinition, i as ContentTypeKind, j as Environment, F as FieldCondition, k as FieldDefinition, l as FieldGroup, m as FieldType, n as RichTextDocument, o as RichTextMark } from './types-b6jbj3g6.js';
2
+ import { R as RichTextFormat, E as Entry, a as RedirectList, b as Redirect, c as EntryList, M as ModelFile, A as Asset, B as Block, I as ImproveBlock, S as ShopifyItem, L as LinkTarget, d as Link, e as EntryStub, f as RichTextNode } from './types-mqo1osjh.js';
3
+ export { g as AssetProduct, C as ContentTypeAccent, h as ContentTypeDefinition, i as ContentTypeKind, j as Environment, F as FieldCondition, k as FieldDefinition, l as FieldGroup, m as FieldType, n as RichTextDocument, o as RichTextMark } from './types-mqo1osjh.js';
4
4
 
5
5
  type ContentClientOptions = {
6
6
  token: TokenSource;
package/dist/index.js CHANGED
@@ -33,6 +33,9 @@ const fieldParams = (fields)=>Object.fromEntries(Object.entries(fields ?? {}).ma
33
33
  ];
34
34
  }));
35
35
  const trimSlashes = (path)=>path.replace(/^\/+|\/+$/g, '');
36
+ // `/space_…/<environment>`: the part of a scoped delivery path between
37
+ // DELIVERY_PATH and the operation.
38
+ const SCOPE = /^\/space_[^/]+\/[^/]+$/;
36
39
  // Reads one space environment's content through the delivery API. The token
37
40
  // decides the space, the environment and whether drafts are visible (for a
38
41
  // host token, with `drafts`).
@@ -42,11 +45,17 @@ const trimSlashes = (path)=>path.replace(/^\/+|\/+$/g, '');
42
45
  class ContentClient {
43
46
  #token;
44
47
  #place;
48
+ // Where the API answers this client's token: it sends a token that reads
49
+ // published content to `/api/delivery/v1/{space}/{environment}/…` with a
50
+ // 308, so the client asks there directly after the first one. Never used
51
+ // for a request that reads drafts, which the scoped path refuses.
52
+ #scope;
45
53
  #defaults;
46
54
  #timeout;
47
55
  #init;
48
56
  #fetch;
49
57
  constructor({ token, drafts, space, environment, url, locale, depth, richText, timeout, init, fetch: fetchImpl }){
58
+ this.#scope = null;
50
59
  // The page at a path (`blog/hello`), or null when there is none. A site's
51
60
  // catch-all route asks for this. The path is looked up in `locale`'s
52
61
  // paths (the default locale's when left out or `*`): pages can have a
@@ -171,7 +180,7 @@ class ContentClient {
171
180
  }
172
181
  // For a 404, null when `notFound` is set, or the body as is for 'body';
173
182
  // throws ContentApiError otherwise.
174
- async #request(path, query, { init, ...options }, { notFound = false, defaults = true } = {}) {
183
+ async #request(path, query, { init, ...options }, { notFound = false, defaults = true, scoped = true } = {}) {
175
184
  // Checked here rather than in the constructor, so a module creating a
176
185
  // client still loads (and a build still runs) without the variable.
177
186
  if (!this.#token) throw new Error('ContentClient needs a token');
@@ -190,7 +199,8 @@ class ContentClient {
190
199
  if (value !== undefined) params.set(key, String(value));
191
200
  }
192
201
  const search = params.size > 0 ? `?${params}` : '';
193
- const url = `${this.url}${config.DELIVERY_PATH}${path}${search}`;
202
+ const scope = drafts || !scoped ? null : this.#scope;
203
+ const url = `${this.url}${config.DELIVERY_PATH}${scope ?? ''}${path}${search}`;
194
204
  const merged = {
195
205
  ...this.#init,
196
206
  ...init
@@ -215,6 +225,21 @@ class ContentClient {
215
225
  timeout
216
226
  ]) : timeout
217
227
  });
228
+ // A token that changed, or now reads drafts, is refused on the path
229
+ // learned for the one before it: ask unscoped once, and learn again.
230
+ if (scope && (response.status === 401 || response.status === 403)) {
231
+ if (this.#scope === scope) this.#scope = null;
232
+ await response.body?.cancel();
233
+ return this.#request(path, query, {
234
+ init,
235
+ ...options
236
+ }, {
237
+ notFound,
238
+ defaults,
239
+ scoped: false
240
+ });
241
+ }
242
+ if (!scope && !drafts && response.redirected) this.#learnScope(response, path);
218
243
  if (notFound === true && response.status === 404) return null;
219
244
  const text = await response.text();
220
245
  let body = text;
@@ -229,6 +254,18 @@ class ContentClient {
229
254
  if (!response.ok) throw new ContentApiError(response.status, url, body);
230
255
  return body;
231
256
  }
257
+ #learnScope(response, path) {
258
+ let redirected;
259
+ try {
260
+ redirected = new URL(response.url);
261
+ } catch {
262
+ return;
263
+ }
264
+ const { origin, pathname } = redirected;
265
+ if (origin !== new URL(this.url).origin || !pathname.startsWith(`${config.DELIVERY_PATH}/`) || !pathname.endsWith(path)) return;
266
+ const scope = pathname.slice(config.DELIVERY_PATH.length, pathname.length - path.length);
267
+ if (SCOPE.test(scope)) this.#scope = scope;
268
+ }
232
269
  }
233
270
  const withoutUndefined = (value)=>Object.fromEntries(Object.entries(value).filter(([, entry])=>entry !== undefined));
234
271
  const headersToObject = (headers)=>headers ? Object.fromEntries(new Headers(headers)) : {};
@@ -277,11 +314,14 @@ const isImproveBlock = (block)=>block._type === IMPROVE_BLOCK;
277
314
  // improve's SDK returned for the visitor (getTestValue / getFlagValue). It
278
315
  // is null when the test or flag isn't active or the visitor isn't in it;
279
316
  // then, or for an option the block has no variant for, the `fallback`
280
- // option's variant, else the first one (usually the control).
317
+ // option's variant, else the first one.
281
318
  const pickImproveVariant = (block, option, fallback)=>{
282
319
  const variants = block.variants ?? {};
283
320
  if (option && variants[option]) return variants[option];
284
- if (fallback && variants[fallback]) return variants[fallback];
321
+ // The arm Improve gives a visitor outside the test, when the caller names
322
+ // none: it is not always the first variant.
323
+ const behind = fallback ?? block.fallback;
324
+ if (behind && variants[behind]) return variants[behind];
285
325
  return Object.values(variants)[0] ?? [];
286
326
  };
287
327
 
package/dist/index.mjs CHANGED
@@ -31,6 +31,9 @@ const fieldParams = (fields)=>Object.fromEntries(Object.entries(fields ?? {}).ma
31
31
  ];
32
32
  }));
33
33
  const trimSlashes = (path)=>path.replace(/^\/+|\/+$/g, '');
34
+ // `/space_…/<environment>`: the part of a scoped delivery path between
35
+ // DELIVERY_PATH and the operation.
36
+ const SCOPE = /^\/space_[^/]+\/[^/]+$/;
34
37
  // Reads one space environment's content through the delivery API. The token
35
38
  // decides the space, the environment and whether drafts are visible (for a
36
39
  // host token, with `drafts`).
@@ -40,11 +43,17 @@ const trimSlashes = (path)=>path.replace(/^\/+|\/+$/g, '');
40
43
  class ContentClient {
41
44
  #token;
42
45
  #place;
46
+ // Where the API answers this client's token: it sends a token that reads
47
+ // published content to `/api/delivery/v1/{space}/{environment}/…` with a
48
+ // 308, so the client asks there directly after the first one. Never used
49
+ // for a request that reads drafts, which the scoped path refuses.
50
+ #scope;
43
51
  #defaults;
44
52
  #timeout;
45
53
  #init;
46
54
  #fetch;
47
55
  constructor({ token, drafts, space, environment, url, locale, depth, richText, timeout, init, fetch: fetchImpl }){
56
+ this.#scope = null;
48
57
  // The page at a path (`blog/hello`), or null when there is none. A site's
49
58
  // catch-all route asks for this. The path is looked up in `locale`'s
50
59
  // paths (the default locale's when left out or `*`): pages can have a
@@ -169,7 +178,7 @@ class ContentClient {
169
178
  }
170
179
  // For a 404, null when `notFound` is set, or the body as is for 'body';
171
180
  // throws ContentApiError otherwise.
172
- async #request(path, query, { init, ...options }, { notFound = false, defaults = true } = {}) {
181
+ async #request(path, query, { init, ...options }, { notFound = false, defaults = true, scoped = true } = {}) {
173
182
  // Checked here rather than in the constructor, so a module creating a
174
183
  // client still loads (and a build still runs) without the variable.
175
184
  if (!this.#token) throw new Error('ContentClient needs a token');
@@ -188,7 +197,8 @@ class ContentClient {
188
197
  if (value !== undefined) params.set(key, String(value));
189
198
  }
190
199
  const search = params.size > 0 ? `?${params}` : '';
191
- const url = `${this.url}${DELIVERY_PATH}${path}${search}`;
200
+ const scope = drafts || !scoped ? null : this.#scope;
201
+ const url = `${this.url}${DELIVERY_PATH}${scope ?? ''}${path}${search}`;
192
202
  const merged = {
193
203
  ...this.#init,
194
204
  ...init
@@ -213,6 +223,21 @@ class ContentClient {
213
223
  timeout
214
224
  ]) : timeout
215
225
  });
226
+ // A token that changed, or now reads drafts, is refused on the path
227
+ // learned for the one before it: ask unscoped once, and learn again.
228
+ if (scope && (response.status === 401 || response.status === 403)) {
229
+ if (this.#scope === scope) this.#scope = null;
230
+ await response.body?.cancel();
231
+ return this.#request(path, query, {
232
+ init,
233
+ ...options
234
+ }, {
235
+ notFound,
236
+ defaults,
237
+ scoped: false
238
+ });
239
+ }
240
+ if (!scope && !drafts && response.redirected) this.#learnScope(response, path);
216
241
  if (notFound === true && response.status === 404) return null;
217
242
  const text = await response.text();
218
243
  let body = text;
@@ -227,6 +252,18 @@ class ContentClient {
227
252
  if (!response.ok) throw new ContentApiError(response.status, url, body);
228
253
  return body;
229
254
  }
255
+ #learnScope(response, path) {
256
+ let redirected;
257
+ try {
258
+ redirected = new URL(response.url);
259
+ } catch {
260
+ return;
261
+ }
262
+ const { origin, pathname } = redirected;
263
+ if (origin !== new URL(this.url).origin || !pathname.startsWith(`${DELIVERY_PATH}/`) || !pathname.endsWith(path)) return;
264
+ const scope = pathname.slice(DELIVERY_PATH.length, pathname.length - path.length);
265
+ if (SCOPE.test(scope)) this.#scope = scope;
266
+ }
230
267
  }
231
268
  const withoutUndefined = (value)=>Object.fromEntries(Object.entries(value).filter(([, entry])=>entry !== undefined));
232
269
  const headersToObject = (headers)=>headers ? Object.fromEntries(new Headers(headers)) : {};
@@ -275,11 +312,14 @@ const isImproveBlock = (block)=>block._type === IMPROVE_BLOCK;
275
312
  // improve's SDK returned for the visitor (getTestValue / getFlagValue). It
276
313
  // is null when the test or flag isn't active or the visitor isn't in it;
277
314
  // then, or for an option the block has no variant for, the `fallback`
278
- // option's variant, else the first one (usually the control).
315
+ // option's variant, else the first one.
279
316
  const pickImproveVariant = (block, option, fallback)=>{
280
317
  const variants = block.variants ?? {};
281
318
  if (option && variants[option]) return variants[option];
282
- if (fallback && variants[fallback]) return variants[fallback];
319
+ // The arm Improve gives a visitor outside the test, when the caller names
320
+ // none: it is not always the first variant.
321
+ const behind = fallback ?? block.fallback;
322
+ if (behind && variants[behind]) return variants[behind];
283
323
  return Object.values(variants)[0] ?? [];
284
324
  };
285
325
 
@@ -76,6 +76,7 @@ type Block<TFields extends Record<string, unknown> = Record<string, unknown>, TT
76
76
  type ImproveBlock<TBlock extends Block = Block> = Block<{
77
77
  kind: 'test' | 'flag';
78
78
  slug: string;
79
+ fallback?: string;
79
80
  variants: Record<string, TBlock[]>;
80
81
  }, '_improve'>;
81
82
  type RichTextMark = {
@@ -226,4 +227,4 @@ type ModelFile = {
226
227
  };
227
228
 
228
229
  export type { Asset as A, Block as B, ContentTypeAccent as C, Entry as E, FieldCondition as F, ImproveBlock as I, LinkTarget as L, ModelFile as M, RichTextFormat as R, ShopifyItem as S, RedirectList as a, Redirect as b, EntryList as c, Link as d, EntryStub as e, RichTextNode as f, AssetProduct as g, ContentTypeDefinition as h, ContentTypeKind as i, Environment as j, FieldDefinition as k, FieldGroup as l, FieldType as m, RichTextDocument as n, RichTextMark as o };
229
- //# sourceMappingURL=types-b6jbj3g6.d.ts.map
230
+ //# sourceMappingURL=types-mqo1osjh.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types-b6jbj3g6.d.ts","sources":["../src/types.ts"],"mappings":"AAAA,KAAK,WAAW;AAChB,KAAK,cAAc;AACnB,KAAK,KAAK;AACV;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oBAAoB,MAAM;AAC1B,kBAAkB,MAAM;AACxB;AACA;AACA;AACA;AACA,eAAe,YAAY;AAC3B;AACA,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,KAAK,iBAAiB,MAAM,oBAAoB,MAAM;AAC3D;AACA;AACA;AACA;AACA;AACA,WAAW,MAAM;AACjB;AACA;AACA;AACA;AACA;AACA,KAAK,SAAS;AACd;AACA;AACA;AACA,KAAK,UAAU;AACf;AACA,oBAAoB,MAAM;AAC1B;AACA;AACA;AACA,KAAK,IAAI;AACT;AACA,WAAW,UAAU;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,WAAW;AACxB,WAAW,UAAU;AACrB;AACA;AACA;AACA;AACA,gBAAgB,WAAW;AAC3B,WAAW,UAAU;AACrB;AACA;AACA;AACA,KAAK,KAAK,iBAAiB,MAAM,oBAAoB,MAAM;AAC3D;AACA;AACA;AACA,KAAK,YAAY,gBAAgB,KAAK,GAAG,KAAK,IAAI,KAAK;AACvD;AACA;AACA,cAAc,MAAM;AACpB;AACA,KAAK,YAAY;AACjB;AACA,YAAY,MAAM;AAClB;AACA,KAAK,YAAY;AACjB;AACA,YAAY,MAAM;AAClB,cAAc,YAAY;AAC1B;AACA,YAAY,YAAY;AACxB;AACA,KAAK,gBAAgB,GAAG,YAAY;AACpC;AACA;AACA,KAAK,SAAS,gBAAgB,KAAK,GAAG,KAAK;AAC3C;AACA;AACA;AACA;AACA;AACA,KAAK,QAAQ;AACb;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB,WAAW,QAAQ;AACnB;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA,KAAK,eAAe;AACpB,KAAK,SAAS;AACd;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB,MAAM;AACvB;AACA,WAAW,cAAc;AACzB;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA;AACA;AACA,KAAK,UAAU;AACf;AACA;AACA;AACA;AACA,KAAK,iBAAiB;AACtB,KAAK,eAAe,GAAG,SAAS;AAChC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,SAAS,GAAG,eAAe;AAChC,KAAK,qBAAqB;AAC1B;AACA;AACA,UAAU,eAAe;AACzB;AACA;AACA,aAAa,iBAAiB;AAC9B;AACA,aAAa,UAAU;AACvB,YAAY,eAAe;AAC3B;AACA,KAAK,SAAS;AACd;AACA;AACA,kBAAkB,qBAAqB;AACvC;;","names":[]}
1
+ {"version":3,"file":"types-mqo1osjh.d.ts","sources":["../src/types.ts"],"mappings":"AAAA,KAAK,WAAW;AAChB,KAAK,cAAc;AACnB,KAAK,KAAK;AACV;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oBAAoB,MAAM;AAC1B,kBAAkB,MAAM;AACxB;AACA;AACA;AACA;AACA,eAAe,YAAY;AAC3B;AACA,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,KAAK,iBAAiB,MAAM,oBAAoB,MAAM;AAC3D;AACA;AACA;AACA;AACA;AACA,WAAW,MAAM;AACjB;AACA;AACA;AACA;AACA;AACA,KAAK,SAAS;AACd;AACA;AACA;AACA,KAAK,UAAU;AACf;AACA,oBAAoB,MAAM;AAC1B;AACA;AACA;AACA,KAAK,IAAI;AACT;AACA,WAAW,UAAU;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,WAAW;AACxB,WAAW,UAAU;AACrB;AACA;AACA;AACA;AACA,gBAAgB,WAAW;AAC3B,WAAW,UAAU;AACrB;AACA;AACA;AACA,KAAK,KAAK,iBAAiB,MAAM,oBAAoB,MAAM;AAC3D;AACA;AACA;AACA,KAAK,YAAY,gBAAgB,KAAK,GAAG,KAAK,IAAI,KAAK;AACvD;AACA;AACA;AACA,cAAc,MAAM;AACpB;AACA,KAAK,YAAY;AACjB;AACA,YAAY,MAAM;AAClB;AACA,KAAK,YAAY;AACjB;AACA,YAAY,MAAM;AAClB,cAAc,YAAY;AAC1B;AACA,YAAY,YAAY;AACxB;AACA,KAAK,gBAAgB,GAAG,YAAY;AACpC;AACA;AACA,KAAK,SAAS,gBAAgB,KAAK,GAAG,KAAK;AAC3C;AACA;AACA;AACA;AACA;AACA,KAAK,QAAQ;AACb;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB,WAAW,QAAQ;AACnB;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA,KAAK,eAAe;AACpB,KAAK,SAAS;AACd;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB,MAAM;AACvB;AACA,WAAW,cAAc;AACzB;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA;AACA;AACA,KAAK,UAAU;AACf;AACA;AACA;AACA;AACA,KAAK,iBAAiB;AACtB,KAAK,eAAe,GAAG,SAAS;AAChC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,SAAS,GAAG,eAAe;AAChC,KAAK,qBAAqB;AAC1B;AACA;AACA,UAAU,eAAe;AACzB;AACA;AACA,aAAa,iBAAiB;AAC9B;AACA,aAAa,UAAU;AACvB,YAAY,eAAe;AAC3B;AACA,KAAK,SAAS;AACd;AACA;AACA,kBAAkB,qBAAqB;AACvC;;","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@obelism/content",
3
3
  "description": "Obelism Content SDK: the delivery API client, the visual editor bridge with prompt editing, editing on your own site, webhook verification, and the content model CLI",
4
- "version": "0.28.4",
4
+ "version": "0.29.0",
5
5
  "keywords": [
6
6
  "cms",
7
7
  "headless-cms",