@storystick/client 0.0.0-stage → 0.2.0-rc.1
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/LICENSE +112 -0
- package/PUBLIC-API.md +22 -0
- package/README.md +180 -2
- package/dist/.source-hash +1 -0
- package/dist/_fetchers.d.ts +51 -0
- package/dist/_fetchers.js +144 -0
- package/dist/_stateMachine.d.ts +102 -0
- package/dist/_stateMachine.js +168 -0
- package/dist/_wireAuth.d.ts +48 -0
- package/dist/_wireAuth.js +52 -0
- package/dist/adapter.d.ts +14 -0
- package/dist/adapter.js +10 -0
- package/dist/content-values.d.ts +11 -0
- package/dist/content-values.js +401 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +45 -0
- package/dist/hydrate-slice.d.ts +2 -0
- package/dist/hydrate-slice.js +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +9 -0
- package/dist/infra-resolver.d.ts +94 -0
- package/dist/infra-resolver.js +130 -0
- package/dist/internal.d.ts +33 -0
- package/dist/internal.js +20 -0
- package/dist/locale-routing.d.ts +5 -0
- package/dist/locale-routing.js +33 -0
- package/dist/normalize-pathname.d.ts +79 -0
- package/dist/normalize-pathname.js +103 -0
- package/dist/preview-frame-dependencies.d.ts +39 -0
- package/dist/preview-frame-dependencies.js +84 -0
- package/dist/preview-query-receipt.d.ts +3 -0
- package/dist/preview-query-receipt.js +16 -0
- package/dist/preview-read-lifecycle.d.ts +10 -0
- package/dist/preview-read-lifecycle.js +40 -0
- package/dist/preview-read-observation.d.ts +18 -0
- package/dist/preview-read-observation.js +81 -0
- package/dist/preview-scope.d.ts +3 -0
- package/dist/preview-scope.js +22 -0
- package/dist/preview-target.d.ts +73 -0
- package/dist/preview-target.js +86 -0
- package/dist/project-locales.d.ts +4 -0
- package/dist/project-locales.js +12 -0
- package/dist/public-delivery.d.ts +36 -0
- package/dist/public-delivery.js +55 -0
- package/dist/published-routes.d.ts +3 -0
- package/dist/published-routes.js +100 -0
- package/dist/query-builder.d.ts +225 -0
- package/dist/query-builder.js +294 -0
- package/dist/query-client.d.ts +54 -0
- package/dist/query-client.js +306 -0
- package/dist/query-compiler.d.ts +5 -0
- package/dist/query-compiler.js +12 -0
- package/dist/query-schema.d.ts +32 -0
- package/dist/query-schema.js +1 -0
- package/dist/redirect-search.d.ts +4 -0
- package/dist/redirect-search.js +14 -0
- package/dist/reference-resolver.d.ts +37 -0
- package/dist/reference-resolver.js +126 -0
- package/dist/resolve-route.d.ts +27 -0
- package/dist/resolve-route.js +145 -0
- package/dist/resolve.d.ts +43 -0
- package/dist/resolve.js +160 -0
- package/dist/schemas.d.ts +71 -0
- package/dist/schemas.js +75 -0
- package/dist/seo.d.ts +52 -0
- package/dist/seo.js +93 -0
- package/dist/signed-webhook.d.ts +13 -0
- package/dist/signed-webhook.js +54 -0
- package/dist/transport.d.ts +29 -0
- package/dist/transport.js +584 -0
- package/dist/typed-route.d.ts +22 -0
- package/dist/typed-route.js +1 -0
- package/dist/types.d.ts +147 -0
- package/dist/types.js +1 -0
- package/package.json +70 -3
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { STORYSTICK_GATEWAY_ORIGIN } from '@storystick/types/public-endpoints';
|
|
2
|
+
import { isExactPublicQueryResultV2 } from '@storystick/types/delivery-contract';
|
|
3
|
+
import { attachPublicReadIdentity } from './public-delivery.js';
|
|
4
|
+
import { createHttpTransportV2, assertResponseProject, assertProjectMatch, } from './transport.js';
|
|
5
|
+
import { createUnselectedQuery, } from './query-builder.js';
|
|
6
|
+
import { attachPreviewFrameDependencies, previewAssetDependenciesFromValue, } from './preview-frame-dependencies.js';
|
|
7
|
+
import { createPreviewProvenanceReadCacheKey, runPreviewProvenanceReadLifecycle, } from './preview-read-lifecycle.js';
|
|
8
|
+
import { StorystickError } from './errors.js';
|
|
9
|
+
import { attachDocumentLocaleRouting, normalizePublicLocaleOrigins } from './locale-routing.js';
|
|
10
|
+
import { attachPreviewQueryReceipt } from './preview-query-receipt.js';
|
|
11
|
+
const CLIENT_RUNTIMES = new WeakMap();
|
|
12
|
+
const AUTHORITY_SCHEMA_CLIENTS = new WeakMap();
|
|
13
|
+
export function getInternalClientRuntime(client) {
|
|
14
|
+
const runtime = CLIENT_RUNTIMES.get(client);
|
|
15
|
+
if (!runtime)
|
|
16
|
+
throw new TypeError('storystick/QUERY_INVALID: Expected a Client instance.');
|
|
17
|
+
return runtime;
|
|
18
|
+
}
|
|
19
|
+
/** Compare private Preview authority in memory without returning or serializing it. */
|
|
20
|
+
export function clientMatchesPreviewAuthority(authority, expected) {
|
|
21
|
+
const runtime = CLIENT_RUNTIMES.get(authority);
|
|
22
|
+
const context = runtime?.requestContext;
|
|
23
|
+
return authority.projectId === expected.projectId &&
|
|
24
|
+
context?.previewSessionId === expected.previewSessionId &&
|
|
25
|
+
context?.previewObservationId === expected.previewObservationId &&
|
|
26
|
+
context.previewToken === expected.previewToken &&
|
|
27
|
+
(context.previewReadContext?.contextId ?? null) === (expected.readContext?.contextId ?? null) &&
|
|
28
|
+
(context.previewReadContext?.revision ?? null) === (expected.readContext?.revision ?? null) &&
|
|
29
|
+
(context.variantId ?? null) === expected.variantId &&
|
|
30
|
+
(context.releaseId ?? null) === expected.releaseId;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Internal adapter primitive: keep a trusted client's transport authority and
|
|
34
|
+
* request scope while replacing only its generated schema surface.
|
|
35
|
+
*/
|
|
36
|
+
export function bindClientSchemaToAuthority(authority, schema) {
|
|
37
|
+
let schemas = AUTHORITY_SCHEMA_CLIENTS.get(authority);
|
|
38
|
+
if (!schemas) {
|
|
39
|
+
schemas = new WeakMap();
|
|
40
|
+
AUTHORITY_SCHEMA_CLIENTS.set(authority, schemas);
|
|
41
|
+
}
|
|
42
|
+
const existing = schemas.get(schema);
|
|
43
|
+
if (existing)
|
|
44
|
+
return existing;
|
|
45
|
+
const runtime = getInternalClientRuntime(authority);
|
|
46
|
+
const options = normalizeClientOptions({
|
|
47
|
+
schema,
|
|
48
|
+
projectId: authority.projectId,
|
|
49
|
+
publishableKey: 'internal-authority-binding',
|
|
50
|
+
requestContext: runtime.requestContext,
|
|
51
|
+
defaults: runtime.defaults,
|
|
52
|
+
localeRouting: runtime.localeRouting,
|
|
53
|
+
});
|
|
54
|
+
const bound = createClientWithTransport(options, runtime.transport);
|
|
55
|
+
schemas.set(schema, bound);
|
|
56
|
+
return bound;
|
|
57
|
+
}
|
|
58
|
+
const DEFAULT_GATEWAY_URL = STORYSTICK_GATEWAY_ORIGIN;
|
|
59
|
+
export function createClient(options) {
|
|
60
|
+
// An untyped caller cannot opt into internal Preview authority through this entry.
|
|
61
|
+
return createInternalClient({ ...options, requestContext: undefined });
|
|
62
|
+
}
|
|
63
|
+
export function createInternalClient(options) {
|
|
64
|
+
const normalized = normalizeClientOptions(options);
|
|
65
|
+
const sourceTransport = normalized.transport ?? createHttpTransportV2({
|
|
66
|
+
projectId: normalized.projectId,
|
|
67
|
+
endpoint: normalized.endpoint ?? DEFAULT_GATEWAY_URL,
|
|
68
|
+
deliveryEndpoint: normalized.deliveryEndpoint,
|
|
69
|
+
apiKey: normalized.apiKey ?? normalized.publishableKey,
|
|
70
|
+
applicationBuildId: normalized.applicationBuildId,
|
|
71
|
+
});
|
|
72
|
+
return createClientWithTransport(normalized, sourceTransport);
|
|
73
|
+
}
|
|
74
|
+
function normalizeClientOptions(options) {
|
|
75
|
+
validateSchema(options.schema);
|
|
76
|
+
if (options.localeRouting?.publicOrigins) {
|
|
77
|
+
for (const locale of options.localeRouting.locales) {
|
|
78
|
+
if (locale.enabled && !options.localeRouting.publicOrigins[locale.code]) {
|
|
79
|
+
throw new TypeError(`Missing public origin for enabled locale ${locale.code}.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
options = { ...options, localeRouting: { ...options.localeRouting,
|
|
83
|
+
publicOrigins: normalizePublicLocaleOrigins(options.localeRouting.publicOrigins),
|
|
84
|
+
} };
|
|
85
|
+
}
|
|
86
|
+
if (!options.projectId.trim())
|
|
87
|
+
throw new TypeError('storystick/QUERY_INVALID: projectId is required.');
|
|
88
|
+
return options;
|
|
89
|
+
}
|
|
90
|
+
/** Schema rebinding reuses authority; only the outer factory selects HTTP. */
|
|
91
|
+
function createClientWithTransport(options, sourceTransport) {
|
|
92
|
+
const transport = {
|
|
93
|
+
async request(input) {
|
|
94
|
+
assertProjectMatch(input.expectedProjectId ?? options.projectId, options.projectId);
|
|
95
|
+
const response = await sourceTransport.request({ ...input, expectedProjectId: options.projectId });
|
|
96
|
+
assertResponseProject(response, options.projectId);
|
|
97
|
+
return response;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const contextEpoch = contextEpochFor(options);
|
|
101
|
+
const client = {
|
|
102
|
+
schema: options.schema,
|
|
103
|
+
...(options.defaults?.locale ? { defaultLocale: options.defaults.locale } : {}),
|
|
104
|
+
projectId: options.projectId,
|
|
105
|
+
query(storyType) {
|
|
106
|
+
if (!Object.prototype.hasOwnProperty.call(options.schema.stories, storyType)) {
|
|
107
|
+
throw new TypeError(`storystick/QUERY_INVALID: Unknown document story ${JSON.stringify(storyType)}.`);
|
|
108
|
+
}
|
|
109
|
+
return createUnselectedQuery(executionHost, storyType);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const executionHost = {
|
|
113
|
+
client,
|
|
114
|
+
schema: client.schema,
|
|
115
|
+
defaultLocale: client.defaultLocale,
|
|
116
|
+
async executeCompiled(compiled, canonical, executeOptions) {
|
|
117
|
+
const requestOptions = mergeQueryExecuteOptions(options.defaults, executeOptions);
|
|
118
|
+
const kind = compiled.cardinality === 'one'
|
|
119
|
+
? 'query-single'
|
|
120
|
+
: 'query-many';
|
|
121
|
+
return runPreviewProvenanceReadLifecycle(options.requestContext, {
|
|
122
|
+
kind,
|
|
123
|
+
cacheKey: createPreviewProvenanceReadCacheKey(kind, {
|
|
124
|
+
projectId: options.projectId,
|
|
125
|
+
endpoint: options.endpoint ?? DEFAULT_GATEWAY_URL,
|
|
126
|
+
authority: previewReadCacheAuthority(options.requestContext),
|
|
127
|
+
query: canonical,
|
|
128
|
+
options: requestOptions,
|
|
129
|
+
localeRouting: options.localeRouting,
|
|
130
|
+
}),
|
|
131
|
+
signal: executeOptions?.signal,
|
|
132
|
+
executeRaw: async () => {
|
|
133
|
+
let response;
|
|
134
|
+
try {
|
|
135
|
+
response = await transport.request({
|
|
136
|
+
udfPath: 'queries:queryV2',
|
|
137
|
+
args: { requestVersion: 2, query: compiled },
|
|
138
|
+
options: requestOptions,
|
|
139
|
+
requestContext: options.requestContext,
|
|
140
|
+
signal: executeOptions?.signal,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof StorystickError)
|
|
145
|
+
throw error;
|
|
146
|
+
if (executeOptions?.signal?.aborted || (error instanceof DOMException && error.name === 'AbortError')) {
|
|
147
|
+
throw new StorystickError('The query was aborted.', {
|
|
148
|
+
code: 'storystick/QUERY_ABORTED', category: 'cancel', retryable: false,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (error instanceof SyntaxError) {
|
|
152
|
+
throw new StorystickError('The Query V2 gateway returned malformed JSON.', {
|
|
153
|
+
code: 'storystick/QUERY_PROTOCOL', category: 'system', retryable: false,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
throw new StorystickError('The Query V2 gateway is unreachable.', {
|
|
157
|
+
code: 'storystick/QUERY_NETWORK', category: 'system', retryable: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
assertAuthoritativeExecutionIdentity({
|
|
161
|
+
compiled,
|
|
162
|
+
requestContext: options.requestContext,
|
|
163
|
+
response,
|
|
164
|
+
});
|
|
165
|
+
if (!isExactPublicQueryResultV2(response.data, compiled.cardinality)) {
|
|
166
|
+
throw new StorystickError('The Query V2 gateway returned an invalid response.', {
|
|
167
|
+
code: 'storystick/PROTOCOL_ERROR', category: 'system', retryable: false,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const result = response.data;
|
|
171
|
+
if (!options.requestContext?.previewToken)
|
|
172
|
+
attachPublicReadIdentity(result, response.publicReadIdentity);
|
|
173
|
+
if (options.localeRouting)
|
|
174
|
+
attachDocumentLocaleRouting(result, options.localeRouting);
|
|
175
|
+
if (options.requestContext?.unstable_previewProvenanceReadLifecycle) {
|
|
176
|
+
attachQueryPreviewDependencies(result, compiled);
|
|
177
|
+
}
|
|
178
|
+
return attachPreviewQueryReceipt(result, response.previewQueryReceiptId);
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
CLIENT_RUNTIMES.set(client, {
|
|
184
|
+
contextEpoch,
|
|
185
|
+
executionHost,
|
|
186
|
+
transport,
|
|
187
|
+
requestContext: options.requestContext,
|
|
188
|
+
defaults: options.defaults,
|
|
189
|
+
localeRouting: options.localeRouting,
|
|
190
|
+
});
|
|
191
|
+
return Object.freeze(client);
|
|
192
|
+
}
|
|
193
|
+
function mergeQueryExecuteOptions(defaults, executeOptions) {
|
|
194
|
+
const defaultNext = defaults?.next;
|
|
195
|
+
const callNext = executeOptions?.next;
|
|
196
|
+
const tags = [
|
|
197
|
+
...(defaultNext?.tags ?? []),
|
|
198
|
+
...(callNext?.tags ?? []),
|
|
199
|
+
];
|
|
200
|
+
return {
|
|
201
|
+
signal: executeOptions?.signal,
|
|
202
|
+
cache: executeOptions?.cache ?? defaults?.cache,
|
|
203
|
+
deliveryCache: executeOptions?.deliveryCache ?? defaults?.deliveryCache,
|
|
204
|
+
next: {
|
|
205
|
+
revalidate: callNext?.revalidate ?? defaultNext?.revalidate,
|
|
206
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function previewReadCacheAuthority(context) {
|
|
211
|
+
if (!context?.previewToken)
|
|
212
|
+
return { mode: 'production' };
|
|
213
|
+
return {
|
|
214
|
+
mode: 'preview',
|
|
215
|
+
sessionId: context.previewSessionId ?? null,
|
|
216
|
+
observationId: context.previewObservationId ?? null,
|
|
217
|
+
variantId: context.variantId ?? null,
|
|
218
|
+
releaseId: context.releaseId ?? null,
|
|
219
|
+
codeVersion: context.previewCodeVersion ?? null,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function attachQueryPreviewDependencies(result, compiled) {
|
|
223
|
+
try {
|
|
224
|
+
attachPreviewFrameDependencies(result, [
|
|
225
|
+
...previewDependenciesForCompiledQuery(compiled),
|
|
226
|
+
...previewAssetDependenciesFromValue(result),
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Provenance bookkeeping is optional and must never fail a content read.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function previewDependenciesForCompiledQuery(compiled) {
|
|
234
|
+
const storyType = compiled.target.storyType;
|
|
235
|
+
if (compiled.cardinality === 'many') {
|
|
236
|
+
return [{
|
|
237
|
+
kind: 'list',
|
|
238
|
+
storyType,
|
|
239
|
+
...(compiled.filter === undefined ? {} : { where: compiled.filter }),
|
|
240
|
+
...(compiled.page === undefined ? {} : { limit: compiled.page.first }),
|
|
241
|
+
}];
|
|
242
|
+
}
|
|
243
|
+
const address = compiled.address;
|
|
244
|
+
if (address?.kind === 'id') {
|
|
245
|
+
return [{ kind: 'batch-get', documentIds: [address.value] }];
|
|
246
|
+
}
|
|
247
|
+
if (address?.kind === 'slug') {
|
|
248
|
+
return [{ kind: 'get-by-slug', storyType, slug: address.value }];
|
|
249
|
+
}
|
|
250
|
+
if (address?.kind === 'pathname') {
|
|
251
|
+
return [{ kind: 'get-by-pathname', storyType, pathname: address.value }];
|
|
252
|
+
}
|
|
253
|
+
return [{ kind: 'list', storyType, limit: 1 }];
|
|
254
|
+
}
|
|
255
|
+
function assertAuthoritativeExecutionIdentity(input) {
|
|
256
|
+
const identity = input.response.executionIdentity;
|
|
257
|
+
const publicQueryHash = input.response.data && typeof input.response.data === 'object'
|
|
258
|
+
? input.response.data.meta?.queryHash
|
|
259
|
+
: undefined;
|
|
260
|
+
if (!identity || typeof publicQueryHash !== 'string' || identity.queryHash !== publicQueryHash) {
|
|
261
|
+
throw new StorystickError('The Query V2 gateway omitted or mismatched its execution identity.', {
|
|
262
|
+
code: 'storystick/QUERY_PROTOCOL', category: 'system', retryable: false,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const context = input.requestContext;
|
|
266
|
+
const source = !context?.previewToken || context.previewReadContext?.perspective.kind === 'published'
|
|
267
|
+
? 'activePublication'
|
|
268
|
+
: context.releaseId
|
|
269
|
+
? 'release'
|
|
270
|
+
: context.variantId
|
|
271
|
+
? 'variant'
|
|
272
|
+
: 'living';
|
|
273
|
+
const expected = [
|
|
274
|
+
source,
|
|
275
|
+
'none',
|
|
276
|
+
input.compiled.locale.code,
|
|
277
|
+
input.compiled.locale.fallback === 'configured' ? 'explicit' : 'none',
|
|
278
|
+
...(source === 'variant' ? [`variant:${context.variantId}`] : []),
|
|
279
|
+
...(source === 'release' ? [`release:${context.releaseId}`] : []),
|
|
280
|
+
].join('|');
|
|
281
|
+
if (identity.readScopeIdentity !== expected) {
|
|
282
|
+
throw new StorystickError('The Query V2 response belongs to a different authoritative read scope.', {
|
|
283
|
+
code: 'storystick/QUERY_SCOPE_MISMATCH', category: 'auth', retryable: false,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function validateSchema(schema) {
|
|
288
|
+
if (!schema || schema.contractVersion !== 2 || typeof schema.fingerprint !== 'string' ||
|
|
289
|
+
!schema.stories || typeof schema.stories !== 'object') {
|
|
290
|
+
throw new TypeError('storystick/QUERY_INVALID: schema must be a generated QuerySchema V2.');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function contextEpochFor(options) {
|
|
294
|
+
const context = options.requestContext;
|
|
295
|
+
if (!context?.previewToken)
|
|
296
|
+
return 'production';
|
|
297
|
+
return [
|
|
298
|
+
'preview',
|
|
299
|
+
context.previewSessionId ?? '',
|
|
300
|
+
context.previewObservationId ?? '',
|
|
301
|
+
context.variantId ?? '',
|
|
302
|
+
context.releaseId ?? '',
|
|
303
|
+
context.previewReadContext?.contextId ?? '',
|
|
304
|
+
context.previewReadContext?.revision ?? '',
|
|
305
|
+
].join(':');
|
|
306
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { canonicalConversationJson } from '@storystick/types/internal/canonical-json';
|
|
2
|
+
const canonicalByQuery = new WeakMap();
|
|
3
|
+
export function compileQueryState(owner, state) {
|
|
4
|
+
const cached = canonicalByQuery.get(owner);
|
|
5
|
+
if (cached)
|
|
6
|
+
return cached;
|
|
7
|
+
const canonical = canonicalConversationJson(state);
|
|
8
|
+
const query = JSON.parse(canonical);
|
|
9
|
+
const result = Object.freeze({ query: Object.freeze(query), canonical });
|
|
10
|
+
canonicalByQuery.set(owner, result);
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type QueryDslOperator = 'is' | 'isNot' | 'isAnyOf' | 'startsWith' | 'contains' | 'gt' | 'lt' | 'after' | 'before' | 'between' | 'isEmpty' | 'isNotEmpty' | 'hasAny' | 'hasAll' | 'doesNotInclude';
|
|
2
|
+
export interface QueryFieldType<TValue = unknown, TOperators extends QueryDslOperator = never, TSortable extends boolean = false, TOptional extends boolean = false, TArray extends boolean = false, TChildren extends object | never = never, TTargets extends object | never = never> {
|
|
3
|
+
readonly value: TValue;
|
|
4
|
+
readonly operators: TOperators;
|
|
5
|
+
readonly sortable: TSortable;
|
|
6
|
+
readonly optional: TOptional;
|
|
7
|
+
readonly array: TArray;
|
|
8
|
+
readonly children: TChildren;
|
|
9
|
+
readonly targets: TTargets;
|
|
10
|
+
}
|
|
11
|
+
/** Structural marker: generated named interfaces deliberately need no index signature. */
|
|
12
|
+
export type QueryFieldMap = object;
|
|
13
|
+
export interface QueryStoryType<TDocument = unknown, TFields extends object = object, TSingleton extends boolean = boolean, TSlug extends boolean = boolean, TPathname extends boolean = boolean> {
|
|
14
|
+
readonly document: TDocument;
|
|
15
|
+
readonly fields: TFields;
|
|
16
|
+
readonly singleton: TSingleton;
|
|
17
|
+
readonly slug: TSlug;
|
|
18
|
+
readonly pathname: TPathname;
|
|
19
|
+
}
|
|
20
|
+
/** Structural marker: generated named interfaces deliberately need no index signature. */
|
|
21
|
+
export type QueryStoryMap = object;
|
|
22
|
+
export interface QuerySchemaDefinition<TStories extends object = object> {
|
|
23
|
+
readonly contractVersion: 2;
|
|
24
|
+
readonly fingerprint: string;
|
|
25
|
+
readonly stories: Readonly<Record<string, {
|
|
26
|
+
readonly singleton: boolean;
|
|
27
|
+
readonly addresses: readonly ('id' | 'slug' | 'pathname')[];
|
|
28
|
+
}>>;
|
|
29
|
+
/** Type-only carrier. Generated values never serialize this property. */
|
|
30
|
+
readonly __types?: TStories;
|
|
31
|
+
}
|
|
32
|
+
export type QueryStoriesOf<TSchema extends QuerySchemaDefinition> = NonNullable<TSchema['__types']>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
type SearchParamValue = string | readonly string[] | undefined;
|
|
2
|
+
/** Keep repeated customer query parameters when a CMS route redirects. */
|
|
3
|
+
export declare function appendSearchParamsToRedirect(redirectTo: string, searchParams: URLSearchParams | Readonly<Record<string, SearchParamValue>>): string;
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Keep repeated customer query parameters when a CMS route redirects. */
|
|
2
|
+
export function appendSearchParamsToRedirect(redirectTo, searchParams) {
|
|
3
|
+
const destination = new URL(redirectTo, 'http://redirect.invalid');
|
|
4
|
+
const entries = searchParams instanceof URLSearchParams ? searchParams.entries() : Object.entries(searchParams);
|
|
5
|
+
for (const [key, rawValue] of entries) {
|
|
6
|
+
const values = Array.isArray(rawValue) ? rawValue : [rawValue];
|
|
7
|
+
for (const value of values) {
|
|
8
|
+
if (typeof value === 'string')
|
|
9
|
+
destination.searchParams.append(key, value);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const path = `${destination.pathname}${destination.search}${destination.hash}`;
|
|
13
|
+
return /^[a-z][a-z\d+.-]*:/i.test(redirectTo) ? destination.href : redirectTo.startsWith('//') ? `//${destination.host}${path}` : path;
|
|
14
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReferencedDocumentFailureReason, type ReferencedDocumentSidecarEntry, type ReferencedDocumentSidecarFailure } from '@storystick/types';
|
|
2
|
+
import type { QuerySchemaDefinition, QueryStoriesOf } from './query-schema.js';
|
|
3
|
+
type StoryName<TSchema extends QuerySchemaDefinition> = keyof QueryStoriesOf<TSchema> & string;
|
|
4
|
+
/** Address metadata only. Read document fields through an explicit Query V2 selection. */
|
|
5
|
+
export type ReferenceDocument = Omit<ReferencedDocumentSidecarEntry, 'fields'>;
|
|
6
|
+
export interface ResolveReferenceOptions {
|
|
7
|
+
/** A delivery document or `_referencedDocs` sidecar. */
|
|
8
|
+
readonly source?: unknown;
|
|
9
|
+
/** Canonical source field path, for example `author` or `members[0]`. */
|
|
10
|
+
readonly path?: string;
|
|
11
|
+
}
|
|
12
|
+
export type ResolvedReference = {
|
|
13
|
+
readonly status: 'ok';
|
|
14
|
+
readonly document: ReferenceDocument;
|
|
15
|
+
} | {
|
|
16
|
+
readonly status: 'empty';
|
|
17
|
+
readonly documentId?: never;
|
|
18
|
+
} | {
|
|
19
|
+
readonly status: 'no-sidecar';
|
|
20
|
+
readonly documentId: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly status: ReferencedDocumentFailureReason;
|
|
23
|
+
readonly documentId: string;
|
|
24
|
+
readonly failure: ReferencedDocumentSidecarFailure;
|
|
25
|
+
} | {
|
|
26
|
+
readonly status: 'wrong-type';
|
|
27
|
+
readonly documentId: string;
|
|
28
|
+
readonly expectedStoryType: string;
|
|
29
|
+
readonly actualStoryType: string;
|
|
30
|
+
} | {
|
|
31
|
+
readonly status: 'ambiguous';
|
|
32
|
+
readonly documentId: string;
|
|
33
|
+
readonly paths: readonly string[];
|
|
34
|
+
};
|
|
35
|
+
export declare function resolveReference<const TSchema extends QuerySchemaDefinition, const TStoryName extends StoryName<TSchema>>(schema: TSchema, storyType: TStoryName, value: string | null | undefined, options?: ResolveReferenceOptions): ResolvedReference;
|
|
36
|
+
export declare function resolveReferences<const TSchema extends QuerySchemaDefinition, const TStoryName extends StoryName<TSchema>>(schema: TSchema, storyType: TStoryName, values: readonly (string | null | undefined)[] | null | undefined, options?: ResolveReferenceOptions): readonly ResolvedReference[];
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { getReferencedDocumentsSidecar, } from '@storystick/types';
|
|
2
|
+
export function resolveReference(schema, storyType, value, options = {}) {
|
|
3
|
+
assertStory(schema, storyType);
|
|
4
|
+
const documentId = nonEmptyString(value);
|
|
5
|
+
if (!documentId)
|
|
6
|
+
return { status: 'empty' };
|
|
7
|
+
const sidecar = getReferencedDocumentsSidecar(options.source);
|
|
8
|
+
if (!sidecar)
|
|
9
|
+
return { status: 'no-sidecar', documentId };
|
|
10
|
+
const exact = options.path
|
|
11
|
+
? exactOutcome(sidecar, options.path, documentId)
|
|
12
|
+
: undefined;
|
|
13
|
+
if (exact)
|
|
14
|
+
return finish(exact, storyType, documentId);
|
|
15
|
+
const candidates = outcomesForDocument(sidecar, documentId);
|
|
16
|
+
if (candidates.length === 0) {
|
|
17
|
+
const byId = sidecar.byId[documentId];
|
|
18
|
+
if (byId)
|
|
19
|
+
return finish({ path: '$byId', entry: byId }, storyType, documentId);
|
|
20
|
+
return finish({
|
|
21
|
+
path: options.path ?? '$unknown',
|
|
22
|
+
failure: {
|
|
23
|
+
documentId,
|
|
24
|
+
policy: 'activePublication',
|
|
25
|
+
reason: 'missing',
|
|
26
|
+
},
|
|
27
|
+
}, storyType, documentId);
|
|
28
|
+
}
|
|
29
|
+
const byId = sidecar.byId[documentId];
|
|
30
|
+
const comparable = byId
|
|
31
|
+
? [...candidates, { path: '$byId', entry: byId }]
|
|
32
|
+
: candidates;
|
|
33
|
+
if (!outcomesAgree(comparable)) {
|
|
34
|
+
return {
|
|
35
|
+
status: 'ambiguous',
|
|
36
|
+
documentId,
|
|
37
|
+
paths: comparable.map((candidate) => candidate.path).sort(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const candidate = byId && candidates[0]?.entry
|
|
41
|
+
? { path: '$byId', entry: byId }
|
|
42
|
+
: candidates[0];
|
|
43
|
+
return finish(candidate, storyType, documentId);
|
|
44
|
+
}
|
|
45
|
+
export function resolveReferences(schema, storyType, values, options = {}) {
|
|
46
|
+
assertStory(schema, storyType);
|
|
47
|
+
if (!values)
|
|
48
|
+
return [];
|
|
49
|
+
return values.map((value, index) => resolveReference(schema, storyType, value, {
|
|
50
|
+
...options,
|
|
51
|
+
...(options.path ? { path: `${options.path}[${index}]` } : {}),
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
function exactOutcome(sidecar, path, documentId) {
|
|
55
|
+
const entry = sidecar.byRef[path];
|
|
56
|
+
if (entry?.documentId === documentId)
|
|
57
|
+
return { path, entry };
|
|
58
|
+
const failure = sidecar.unresolved?.[path];
|
|
59
|
+
if (failure?.documentId === documentId)
|
|
60
|
+
return { path, failure };
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
function outcomesForDocument(sidecar, documentId) {
|
|
64
|
+
const outcomes = [];
|
|
65
|
+
for (const [path, entry] of Object.entries(sidecar.byRef)) {
|
|
66
|
+
if (entry.documentId === documentId)
|
|
67
|
+
outcomes.push({ path, entry });
|
|
68
|
+
}
|
|
69
|
+
for (const [path, failure] of Object.entries(sidecar.unresolved ?? {})) {
|
|
70
|
+
if (failure.documentId === documentId)
|
|
71
|
+
outcomes.push({ path, failure });
|
|
72
|
+
}
|
|
73
|
+
return outcomes;
|
|
74
|
+
}
|
|
75
|
+
function outcomesAgree(outcomes) {
|
|
76
|
+
const first = outcomeIdentity(outcomes[0]);
|
|
77
|
+
return outcomes.every((outcome) => outcomeIdentity(outcome) === first);
|
|
78
|
+
}
|
|
79
|
+
function outcomeIdentity(outcome) {
|
|
80
|
+
if (outcome.failure) {
|
|
81
|
+
return [
|
|
82
|
+
'failure',
|
|
83
|
+
outcome.failure.reason,
|
|
84
|
+
outcome.failure.policy,
|
|
85
|
+
outcome.failure.pinnedSnapshotId ?? '',
|
|
86
|
+
].join(':');
|
|
87
|
+
}
|
|
88
|
+
return [
|
|
89
|
+
'ok',
|
|
90
|
+
outcome.entry.storyType,
|
|
91
|
+
outcome.entry.policy,
|
|
92
|
+
outcome.entry.snapshotId ?? '',
|
|
93
|
+
outcome.entry.locale ?? '',
|
|
94
|
+
outcome.entry.resolvedPathname ?? '',
|
|
95
|
+
].join(':');
|
|
96
|
+
}
|
|
97
|
+
function finish(outcome, expectedStoryType, documentId) {
|
|
98
|
+
if (outcome.failure) {
|
|
99
|
+
return {
|
|
100
|
+
status: outcome.failure.reason,
|
|
101
|
+
documentId,
|
|
102
|
+
failure: outcome.failure,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (outcome.entry.storyType !== expectedStoryType) {
|
|
106
|
+
return {
|
|
107
|
+
status: 'wrong-type',
|
|
108
|
+
documentId,
|
|
109
|
+
expectedStoryType,
|
|
110
|
+
actualStoryType: outcome.entry.storyType,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const { fields: _fields, ...address } = outcome.entry;
|
|
114
|
+
return { status: 'ok', document: address };
|
|
115
|
+
}
|
|
116
|
+
function assertStory(schema, storyType) {
|
|
117
|
+
if (!Object.prototype.hasOwnProperty.call(schema.stories, storyType)) {
|
|
118
|
+
throw new TypeError(`storystick/QUERY_INVALID: Unknown reference target story ${JSON.stringify(storyType)}.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function nonEmptyString(value) {
|
|
122
|
+
if (typeof value !== 'string')
|
|
123
|
+
return undefined;
|
|
124
|
+
const trimmed = value.trim();
|
|
125
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
126
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `resolveStorystickPage` — core URL → document resolver (ADR-094 §1).
|
|
3
|
+
*
|
|
4
|
+
* Takes a configured client and a pathname (and optional storytype filter),
|
|
5
|
+
* dispatches a SINGLE call to the backend `queries:getRouteByPathname`
|
|
6
|
+
* cross-storytype endpoint (Lane A0), and returns a discriminated
|
|
7
|
+
* {@link ResolvedRoute}.
|
|
8
|
+
*
|
|
9
|
+
* Single-call semantics: the backend resolves cross-storytype via the
|
|
10
|
+
* `by_projectId_resolvedPathname` index (ADR-086 URL-uniqueness invariant);
|
|
11
|
+
* no SDK-side fan-out across storytypes. 1 indexed call per invocation,
|
|
12
|
+
* regardless of how many storytypes the project declares.
|
|
13
|
+
*
|
|
14
|
+
* Production vs preview parity is JWT-driven — the configured client's
|
|
15
|
+
* `requestContext.previewToken` (when set) routes the underlying transport
|
|
16
|
+
* to the preview JWT; otherwise the API-key mint cache supplies the
|
|
17
|
+
* production JWT (audit P1-1).
|
|
18
|
+
*/
|
|
19
|
+
import type { ResolvedRoute, ResolveRouteInput } from './types.js';
|
|
20
|
+
import type { Client } from './query-client.js';
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a developer-supplied pathname to a `ResolvedRoute` discriminated
|
|
23
|
+
* union. Throws on infrastructure failures (auth, network, gateway 5xx) —
|
|
24
|
+
* the return shape only models *resolution outcomes*, never failures
|
|
25
|
+
* (per ADR-094 §1 error strategy).
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveStorystickPage(client: Client, input: ResolveRouteInput): Promise<ResolvedRoute>;
|