@cobrastyle/adapter-storyblok 1.0.3 → 2.0.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/client.d.ts CHANGED
@@ -1,7 +1,72 @@
1
+ import StoryblokClient from 'storyblok-js-client';
2
+ import type { ISbCustomFetch, ISbStoriesParams } from 'storyblok-js-client';
1
3
  import { type StoryblokVersion } from './config';
2
- import type { StoryblokStory } from './types';
4
+ import type { FetchStoriesParams, ResolveParams, StoryblokStoriesResult, StoryblokStory } from './types';
3
5
  import type { MapContext } from './mappers/blocks';
6
+ /**
7
+ * Storyblok caps `by_uuids` at 100 ids per request. Beyond that the query
8
+ * string overflows and the CDN errors, so lists longer than this are split
9
+ * across requests and merged. Consumers should not have to know this.
10
+ */
11
+ export declare const UUID_CHUNK_SIZE = 100;
12
+ /** Drop memoised clients. Needed when the config changes (tokens, region). */
13
+ export declare function resetClients(): void;
14
+ /** The version this request should read, unless the caller pins one. */
15
+ export declare function resolveVersion(version?: StoryblokVersion): Promise<StoryblokVersion>;
16
+ /**
17
+ * The configured client for a version — correct token, region and richtext
18
+ * resolver. Exported so consumers that render their own components never have
19
+ * to construct a client (and re-derive token selection) themselves.
20
+ */
21
+ export declare function getStoryblokClient(version?: StoryblokVersion): Promise<StoryblokClient>;
4
22
  /** Render context backed by the SDK's richtext resolver. */
5
23
  export declare function mapContext(version: StoryblokVersion): MapContext;
6
- /** Fetch a single story by slug. Returns null on 404. */
7
- export declare function fetchStory(slug: string, version: StoryblokVersion): Promise<StoryblokStory | null>;
24
+ /**
25
+ * Cache policy, in one place.
26
+ *
27
+ * Draft must never be cached — the Visual Editor would show stale content and
28
+ * the editor would think their change was lost. Published content is cached for
29
+ * `revalidate` seconds (from config, or per call).
30
+ */
31
+ export declare function fetchOptionsFor(version: StoryblokVersion, revalidate?: number): ISbCustomFetch;
32
+ export interface FetchOptions {
33
+ /** Pin the version instead of resolving it from config. */
34
+ version?: StoryblokVersion;
35
+ /** Cache lifetime in seconds. Published only — draft is always uncached. */
36
+ revalidate?: number;
37
+ }
38
+ export interface FetchStoryOptions extends FetchOptions {
39
+ /** Relation/link expansion for this story. */
40
+ params?: ResolveParams;
41
+ }
42
+ /**
43
+ * Fetch a single story by slug. Returns null on 404.
44
+ *
45
+ * Pass `params.resolve_relations` to have Storyblok substitute referenced
46
+ * stories rather than returning bare uuids — the alternative is batch-fetching
47
+ * them yourself afterwards.
48
+ */
49
+ export declare function fetchStory(slug: string, version?: StoryblokVersion, opts?: FetchStoryOptions): Promise<StoryblokStory | null>;
50
+ /** Translate the public params into the SDK's wire shape. */
51
+ export declare function toStoriesParams(params: FetchStoriesParams, version: StoryblokVersion): ISbStoriesParams;
52
+ /** Translate single-story params into the SDK's wire shape. */
53
+ export declare function toStoryParams(params: ResolveParams, version: StoryblokVersion): Record<string, unknown>;
54
+ /**
55
+ * Query the Content Delivery API for a list of stories.
56
+ *
57
+ * Returns raw stories — mapping into `CmsPage`/`CmsBlockNode` is opt-in via
58
+ * `mapStory`, so consumers rendering their own components keep the full blok
59
+ * tree (and `_editable`, which the Visual Editor needs).
60
+ *
61
+ * `by_uuids` / `by_uuids_ordered` longer than 100 are split and merged
62
+ * transparently; `by_uuids_ordered` keeps the caller's order across chunks.
63
+ */
64
+ export declare function fetchStories(params?: FetchStoriesParams, opts?: FetchOptions): Promise<StoryblokStoriesResult>;
65
+ /**
66
+ * Every story matching the query, paging until the CDN is exhausted.
67
+ *
68
+ * `fetchStories` returns one page — fine for a capped list, wrong for "all
69
+ * recipes" once a space grows past `per_page`. Uuid queries are already
70
+ * complete (they chunk rather than page), so those short-circuit.
71
+ */
72
+ export declare function fetchAllStories(params?: FetchStoriesParams, opts?: FetchOptions): Promise<StoryblokStoriesResult>;
package/dist/client.js CHANGED
@@ -1,30 +1,82 @@
1
1
  import StoryblokClient from 'storyblok-js-client';
2
2
  import { getConfig } from './config';
3
- let clientForToken = null;
4
- function getClient(token) {
5
- if (clientForToken?.token === token)
6
- return clientForToken.client;
3
+ /**
4
+ * Storyblok caps `by_uuids` at 100 ids per request. Beyond that the query
5
+ * string overflows and the CDN errors, so lists longer than this are split
6
+ * across requests and merged. Consumers should not have to know this.
7
+ */
8
+ export const UUID_CHUNK_SIZE = 100;
9
+ /**
10
+ * One client per token. Draft and published use different tokens, so a
11
+ * single-slot cache would rebuild the client on every alternation — which a
12
+ * page rendering both preview and published content does constantly.
13
+ */
14
+ const clients = new Map();
15
+ function clientFor(token) {
7
16
  const cfg = getConfig();
17
+ // Keyed on everything the client is constructed from, so re-configuring with
18
+ // a different token or region cannot hand back a stale client.
19
+ const key = `${token}|${cfg.region ?? 'eu'}`;
20
+ const cached = clients.get(key);
21
+ if (cached)
22
+ return cached;
8
23
  const client = new StoryblokClient({ accessToken: token, region: cfg.region });
9
- clientForToken = { token, client };
24
+ clients.set(key, client);
10
25
  return client;
11
26
  }
27
+ /** Drop memoised clients. Needed when the config changes (tokens, region). */
28
+ export function resetClients() {
29
+ clients.clear();
30
+ }
12
31
  function tokenFor(version) {
13
32
  const cfg = getConfig();
14
33
  return version === 'draft' ? cfg.previewToken : cfg.publicToken;
15
34
  }
35
+ /** The version this request should read, unless the caller pins one. */
36
+ export async function resolveVersion(version) {
37
+ return version ?? getConfig().resolveVersion();
38
+ }
39
+ /**
40
+ * The configured client for a version — correct token, region and richtext
41
+ * resolver. Exported so consumers that render their own components never have
42
+ * to construct a client (and re-derive token selection) themselves.
43
+ */
44
+ export async function getStoryblokClient(version) {
45
+ return clientFor(tokenFor(await resolveVersion(version)));
46
+ }
16
47
  /** Render context backed by the SDK's richtext resolver. */
17
48
  export function mapContext(version) {
18
- const client = getClient(tokenFor(version));
49
+ const client = clientFor(tokenFor(version));
19
50
  return {
20
- renderRichText: (doc) => doc ? client.richTextResolver.render(doc) : '',
51
+ renderRichText: (doc) => (doc ? client.richTextResolver.render(doc) : ''),
21
52
  };
22
53
  }
23
- /** Fetch a single story by slug. Returns null on 404. */
24
- export async function fetchStory(slug, version) {
25
- const client = getClient(tokenFor(version));
54
+ /**
55
+ * Cache policy, in one place.
56
+ *
57
+ * Draft must never be cached — the Visual Editor would show stale content and
58
+ * the editor would think their change was lost. Published content is cached for
59
+ * `revalidate` seconds (from config, or per call).
60
+ */
61
+ export function fetchOptionsFor(version, revalidate) {
62
+ if (version === 'draft')
63
+ return { cache: 'no-store' };
64
+ const seconds = revalidate ?? getConfig().revalidate;
65
+ // `next` is Next.js's RequestInit extension; harmless on other runtimes.
66
+ return seconds != null ? { next: { revalidate: seconds } } : {};
67
+ }
68
+ /**
69
+ * Fetch a single story by slug. Returns null on 404.
70
+ *
71
+ * Pass `params.resolve_relations` to have Storyblok substitute referenced
72
+ * stories rather than returning bare uuids — the alternative is batch-fetching
73
+ * them yourself afterwards.
74
+ */
75
+ export async function fetchStory(slug, version, opts = {}) {
76
+ const v = await resolveVersion(version ?? opts.version);
77
+ const client = clientFor(tokenFor(v));
26
78
  try {
27
- const res = await client.get(`cdn/stories/${slug}`, { version });
79
+ const res = await client.get(`cdn/stories/${slug}`, toStoryParams(opts.params ?? {}, v), fetchOptionsFor(v, opts.revalidate));
28
80
  return res.data.story;
29
81
  }
30
82
  catch (err) {
@@ -33,6 +85,124 @@ export async function fetchStory(slug, version) {
33
85
  throw err;
34
86
  }
35
87
  }
88
+ /** Relation/link expansion, shared by single-story and list fetches. */
89
+ function applyResolveParams(out, params) {
90
+ // The SDK accepts a comma-joined string or an array; join so the query string
91
+ // is identical either way and easier to assert on.
92
+ if (params.resolve_relations?.length)
93
+ out.resolve_relations = params.resolve_relations.join(',');
94
+ if (params.resolve_links)
95
+ out.resolve_links = params.resolve_links;
96
+ if (params.resolve_links_level)
97
+ out.resolve_links_level = params.resolve_links_level;
98
+ if (params.language)
99
+ out.language = params.language;
100
+ if (params.fallback_lang)
101
+ out.fallback_lang = params.fallback_lang;
102
+ }
103
+ /** Translate the public params into the SDK's wire shape. */
104
+ export function toStoriesParams(params, version) {
105
+ const out = { version };
106
+ if (params.content_type)
107
+ out.content_type = params.content_type;
108
+ if (params.starts_with)
109
+ out.starts_with = params.starts_with;
110
+ if (params.by_uuids?.length)
111
+ out.by_uuids = params.by_uuids.join(',');
112
+ if (params.by_uuids_ordered?.length)
113
+ out.by_uuids_ordered = params.by_uuids_ordered.join(',');
114
+ if (params.filter_query)
115
+ out.filter_query = params.filter_query;
116
+ if (params.sort_by)
117
+ out.sort_by = params.sort_by;
118
+ if (params.search_term)
119
+ out.search_term = params.search_term;
120
+ if (params.excluding_slugs)
121
+ out.excluding_slugs = params.excluding_slugs;
122
+ if (params.per_page != null)
123
+ out.per_page = params.per_page;
124
+ if (params.page != null)
125
+ out.page = params.page;
126
+ applyResolveParams(out, params);
127
+ return out;
128
+ }
129
+ /** Translate single-story params into the SDK's wire shape. */
130
+ export function toStoryParams(params, version) {
131
+ const out = { version };
132
+ applyResolveParams(out, params);
133
+ return out;
134
+ }
135
+ function chunk(xs, size) {
136
+ const out = [];
137
+ for (let i = 0; i < xs.length; i += size)
138
+ out.push(xs.slice(i, i + size));
139
+ return out;
140
+ }
141
+ /** Restore the caller's order; ids with no story (deleted, unpublished) drop out. */
142
+ function orderByUuids(stories, uuids) {
143
+ const byUuid = new Map(stories.map((s) => [s.uuid, s]));
144
+ return uuids.map((id) => byUuid.get(id)).filter((s) => Boolean(s));
145
+ }
146
+ async function fetchStoriesPage(params, version, revalidate) {
147
+ const client = clientFor(tokenFor(version));
148
+ const res = await client.get('cdn/stories', params, fetchOptionsFor(version, revalidate));
149
+ return {
150
+ stories: (res.data.stories ?? []),
151
+ total: res.total ?? 0,
152
+ };
153
+ }
154
+ /**
155
+ * Query the Content Delivery API for a list of stories.
156
+ *
157
+ * Returns raw stories — mapping into `CmsPage`/`CmsBlockNode` is opt-in via
158
+ * `mapStory`, so consumers rendering their own components keep the full blok
159
+ * tree (and `_editable`, which the Visual Editor needs).
160
+ *
161
+ * `by_uuids` / `by_uuids_ordered` longer than 100 are split and merged
162
+ * transparently; `by_uuids_ordered` keeps the caller's order across chunks.
163
+ */
164
+ export async function fetchStories(params = {}, opts = {}) {
165
+ const version = await resolveVersion(opts.version);
166
+ const ordered = params.by_uuids_ordered;
167
+ const ids = ordered ?? params.by_uuids;
168
+ if (!ids?.length || ids.length <= UUID_CHUNK_SIZE) {
169
+ return fetchStoriesPage(toStoriesParams(params, version), version, opts.revalidate);
170
+ }
171
+ const key = ordered ? 'by_uuids_ordered' : 'by_uuids';
172
+ const pages = await Promise.all(chunk(ids, UUID_CHUNK_SIZE).map((slice) => fetchStoriesPage(toStoriesParams({ ...params, [key]: slice }, version), version, opts.revalidate)));
173
+ const stories = pages.flatMap((p) => p.stories);
174
+ return {
175
+ stories: ordered ? orderByUuids(stories, ordered) : stories,
176
+ total: stories.length,
177
+ };
178
+ }
179
+ /** Storyblok's hard cap on a single page. */
180
+ const MAX_PER_PAGE = 100;
181
+ /**
182
+ * Every story matching the query, paging until the CDN is exhausted.
183
+ *
184
+ * `fetchStories` returns one page — fine for a capped list, wrong for "all
185
+ * recipes" once a space grows past `per_page`. Uuid queries are already
186
+ * complete (they chunk rather than page), so those short-circuit.
187
+ */
188
+ export async function fetchAllStories(params = {}, opts = {}) {
189
+ if (params.by_uuids?.length || params.by_uuids_ordered?.length) {
190
+ return fetchStories(params, opts);
191
+ }
192
+ const perPage = Math.min(params.per_page ?? MAX_PER_PAGE, MAX_PER_PAGE);
193
+ const stories = [];
194
+ let total = 0;
195
+ for (let page = params.page ?? 1;; page += 1) {
196
+ const res = await fetchStories({ ...params, per_page: perPage, page }, opts);
197
+ stories.push(...res.stories);
198
+ total = res.total;
199
+ // Stop on a short page rather than trusting `total` alone — a story
200
+ // unpublished mid-walk would otherwise loop forever.
201
+ if (res.stories.length < perPage || stories.length >= total)
202
+ break;
203
+ }
204
+ return { stories, total: total || stories.length };
205
+ }
36
206
  function isNotFound(err) {
37
207
  const status = err?.status ??
38
208
  err?.response?.status;
package/dist/config.d.ts CHANGED
@@ -10,6 +10,11 @@ export interface StoryblokConfig {
10
10
  region?: 'eu' | 'us' | 'ap' | 'ca' | 'cn';
11
11
  /** Fallback version when no versionGetter is supplied. Defaults to 'published'. */
12
12
  defaultVersion?: StoryblokVersion;
13
+ /**
14
+ * Cache lifetime in seconds for PUBLISHED reads. Draft is never cached.
15
+ * Omit to leave caching to the framework's default.
16
+ */
17
+ revalidate?: number;
13
18
  /**
14
19
  * Per-request version resolver supplied by the app (reads Next.js draftMode).
15
20
  * Kept here so the framework-agnostic package never imports next/headers.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { storyblokAdapter } from './adapter';
2
2
  export { setConfig, getConfig } from './config';
3
3
  export type { StoryblokConfig, StoryblokVersion } from './config';
4
- export { blockCatalog, type BlockDefinition } from './blocks/catalog';
5
4
  export { inertStoryblokAdapter } from './inert';
5
+ export { getStoryblokClient, resolveVersion, fetchStory, fetchStories, fetchAllStories, mapContext, fetchOptionsFor, toStoriesParams, toStoryParams, UUID_CHUNK_SIZE, resetClients, type FetchOptions, type FetchStoryOptions, } from './client';
6
+ export type { FetchStoriesParams, ResolveParams, StoryblokAsset, StoryblokBlok, StoryblokLink, StoryblokPageContent, StoryblokStoriesResult, StoryblokStory, } from './types';
7
+ export { mapStory } from './mappers/story';
package/dist/index.js CHANGED
@@ -1,4 +1,12 @@
1
1
  export { storyblokAdapter } from './adapter';
2
2
  export { setConfig, getConfig } from './config';
3
- export { blockCatalog } from './blocks/catalog';
4
3
  export { inertStoryblokAdapter } from './inert';
4
+ // ── Transport ─────────────────────────────────────────────────────────────
5
+ // The configured client and the raw fetches, for consumers that render their
6
+ // own components. Do NOT build your own StoryblokClient — token-per-version,
7
+ // region and cache policy live here.
8
+ export { getStoryblokClient, resolveVersion, fetchStory, fetchStories, fetchAllStories, mapContext, fetchOptionsFor, toStoriesParams, toStoryParams, UUID_CHUNK_SIZE, resetClients, } from './client';
9
+ // ── Normalisation (opt-in) ────────────────────────────────────────────────
10
+ // Map a raw story into cobrastyle's CmsPage/CmsBlockNode. Only needed by
11
+ // consumers using the shared block components.
12
+ export { mapStory } from './mappers/story';
package/dist/types.d.ts CHANGED
@@ -35,3 +35,49 @@ export interface StoryblokStory {
35
35
  export interface StoryblokStoryResponse {
36
36
  story: StoryblokStory;
37
37
  }
38
+ export interface StoryblokStoriesResponse {
39
+ stories: StoryblokStory[];
40
+ }
41
+ /** A page of stories plus the total the CDN reports. */
42
+ export interface StoryblokStoriesResult {
43
+ stories: StoryblokStory[];
44
+ total: number;
45
+ }
46
+ /**
47
+ * Relation and link expansion, shared by single-story and list fetches.
48
+ *
49
+ * `resolve_relations` is how you avoid hand-rolling a relation resolver:
50
+ * Storyblok substitutes the referenced stories into the response instead of
51
+ * leaving bare uuids, so a consumer does not have to batch-fetch them itself.
52
+ */
53
+ export interface ResolveParams {
54
+ /** Dot paths to expand, e.g. ['recipe.ingredients', 'page.author']. */
55
+ resolve_relations?: string[];
56
+ /** How story links in multilink/richtext fields are expanded. */
57
+ resolve_links?: 'url' | 'story' | 'link';
58
+ /** Depth for `resolve_links`. */
59
+ resolve_links_level?: 1 | 2;
60
+ /** Space language, for translated content. */
61
+ language?: string;
62
+ /** Fallback language when a translation is missing. */
63
+ fallback_lang?: string;
64
+ }
65
+ /**
66
+ * The Content Delivery query surface, in the shapes a consumer thinks in —
67
+ * uuid lists are arrays here and joined internally, and chunked past 100.
68
+ */
69
+ export interface FetchStoriesParams extends ResolveParams {
70
+ content_type?: string;
71
+ starts_with?: string;
72
+ by_uuids?: string[];
73
+ /** Same as `by_uuids`, but the response keeps this order. */
74
+ by_uuids_ordered?: string[];
75
+ /** e.g. `{ tags: { in_array: 'vegan' } }` */
76
+ filter_query?: Record<string, unknown>;
77
+ sort_by?: string;
78
+ search_term?: string;
79
+ excluding_slugs?: string;
80
+ /** Storyblok caps this at 100. */
81
+ per_page?: number;
82
+ page?: number;
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cobrastyle/adapter-storyblok",
3
- "version": "1.0.3",
3
+ "version": "2.0.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "exports": {