@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,225 @@
|
|
|
1
|
+
import type { Client } from './query-client.js';
|
|
2
|
+
import type { DocumentLocaleRouting } from './locale-routing.js';
|
|
3
|
+
import type { DeliveryDocumentV1 } from '@storystick/types/delivery-contract';
|
|
4
|
+
import { type CompiledQueryV2, type KernelFilterExprV1, type QueryPageInfo, type QueryResult as QueryResultV2 } from '@storystick/types/query-contract';
|
|
5
|
+
import type { QueryFieldMap, QueryFieldType, QuerySchemaDefinition, QueryStoryMap, QueryStoryType, QueryStoriesOf } from './query-schema.js';
|
|
6
|
+
export interface QueryExecuteOptions {
|
|
7
|
+
readonly deliveryCache?: 'default' | 'bypass';
|
|
8
|
+
readonly signal?: AbortSignal;
|
|
9
|
+
readonly cache?: 'force-cache' | 'no-store';
|
|
10
|
+
readonly next?: {
|
|
11
|
+
readonly revalidate?: number | false;
|
|
12
|
+
readonly tags?: readonly string[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface QueryExecutionHost {
|
|
16
|
+
readonly client?: Client;
|
|
17
|
+
readonly schema: QuerySchemaDefinition;
|
|
18
|
+
readonly defaultLocale?: string;
|
|
19
|
+
executeCompiled<TData, TPageInfo extends QueryPageInfo | null>(compiled: CompiledQueryV2, canonical: string, options?: QueryExecuteOptions): Promise<QueryResultV2<TData, TPageInfo>>;
|
|
20
|
+
}
|
|
21
|
+
export interface InternalExecutableQueryDescriptor {
|
|
22
|
+
readonly host: QueryExecutionHost;
|
|
23
|
+
}
|
|
24
|
+
/** Internal framework seam. Executable queries retain their owning Client. */
|
|
25
|
+
export declare function getExecutableQueryDescriptor(query: object): InternalExecutableQueryDescriptor;
|
|
26
|
+
type FieldsOf<TStory> = TStory extends QueryStoryType<unknown, infer TFields> ? TFields : never;
|
|
27
|
+
type DocumentOf<TStory> = TStory extends QueryStoryType<infer TDocument, object> ? TDocument : never;
|
|
28
|
+
type ValueOf<TField> = TField extends {
|
|
29
|
+
readonly value: infer TValue;
|
|
30
|
+
} ? TValue : never;
|
|
31
|
+
type ChildrenOf<TField> = TField extends QueryFieldType<unknown, never, false, false, false, infer TChildren> ? TChildren : TField extends {
|
|
32
|
+
readonly children: infer TChildren;
|
|
33
|
+
} ? TChildren : never;
|
|
34
|
+
type TargetsOf<TField> = TField extends {
|
|
35
|
+
readonly targets: infer TTargets;
|
|
36
|
+
} ? TTargets : never;
|
|
37
|
+
type IsArray<TField> = TField extends {
|
|
38
|
+
readonly array: infer TArray;
|
|
39
|
+
} ? TArray : false;
|
|
40
|
+
type IsOptional<TField> = TField extends {
|
|
41
|
+
readonly optional: infer TOptional;
|
|
42
|
+
} ? TOptional : false;
|
|
43
|
+
type TargetSelection<TTargets> = TTargets extends object ? QuerySelectionInput<FieldsOf<TTargets[keyof TTargets]>> | {
|
|
44
|
+
readonly on: {
|
|
45
|
+
readonly [K in keyof TTargets]?: QuerySelectionInput<FieldsOf<TTargets[K]>>;
|
|
46
|
+
};
|
|
47
|
+
} : never;
|
|
48
|
+
export type QuerySelectionInput<TFields extends object> = {
|
|
49
|
+
readonly [K in keyof TFields]?: true | (ChildrenOf<TFields[K]> extends QueryFieldMap ? QuerySelectionInput<ChildrenOf<TFields[K]>> : never) | (TargetsOf<TFields[K]> extends QueryStoryMap ? {
|
|
50
|
+
readonly resolve: TargetSelection<TargetsOf<TFields[K]>>;
|
|
51
|
+
} : never);
|
|
52
|
+
};
|
|
53
|
+
type SelectedTarget<TTargets, TSelection> = TTargets extends object ? TSelection extends {
|
|
54
|
+
readonly on: infer TOn;
|
|
55
|
+
} ? {
|
|
56
|
+
[K in keyof TTargets & keyof TOn]: TOn[K] extends QuerySelectionInput<FieldsOf<TTargets[K]>> ? SelectedDocument<TTargets[K], TOn[K]> : never;
|
|
57
|
+
}[keyof TTargets & keyof TOn] : TSelection extends QuerySelectionInput<FieldsOf<TTargets[keyof TTargets]>> ? SelectedDocument<TTargets[keyof TTargets], TSelection> : never : never;
|
|
58
|
+
type SelectedFieldValue<TField, TSelection> = TSelection extends true ? ValueOf<TField> : TSelection extends {
|
|
59
|
+
readonly resolve: infer TResolve;
|
|
60
|
+
} ? IsArray<TField> extends true ? readonly (SelectedTarget<TargetsOf<TField>, TResolve> | null)[] : SelectedTarget<TargetsOf<TField>, TResolve> | null : TSelection extends QuerySelectionInput<Extract<ChildrenOf<TField>, QueryFieldMap>> ? IsArray<TField> extends true ? readonly SelectedFields<Extract<ChildrenOf<TField>, QueryFieldMap>, TSelection>[] : SelectedFields<Extract<ChildrenOf<TField>, QueryFieldMap>, TSelection> : never;
|
|
61
|
+
type RequiredSelectedFields<TFields extends object, TSelection> = {
|
|
62
|
+
readonly [K in keyof TFields & keyof TSelection as IsOptional<TFields[K]> extends true ? never : K]: SelectedFieldValue<TFields[K], TSelection[K]>;
|
|
63
|
+
};
|
|
64
|
+
type OptionalSelectedFields<TFields extends object, TSelection> = {
|
|
65
|
+
readonly [K in keyof TFields & keyof TSelection as IsOptional<TFields[K]> extends true ? K : never]?: SelectedFieldValue<TFields[K], TSelection[K]>;
|
|
66
|
+
};
|
|
67
|
+
export type SelectedFields<TFields extends object, TSelection> = RequiredSelectedFields<TFields, TSelection> & OptionalSelectedFields<TFields, TSelection>;
|
|
68
|
+
export type SelectedDocument<TStory, TSelection> = Omit<DeliveryDocumentV1, 'fields'> & {
|
|
69
|
+
readonly _localeRouting?: DocumentLocaleRouting;
|
|
70
|
+
readonly fields: TSelection extends 'all' ? DocumentOf<TStory> : SelectedFields<FieldsOf<TStory>, TSelection>;
|
|
71
|
+
};
|
|
72
|
+
type OperatorNames<TField> = TField extends {
|
|
73
|
+
readonly operators: infer TOperators;
|
|
74
|
+
} ? TOperators : never;
|
|
75
|
+
type OperatorValue<TField> = Exclude<ValueOf<TField>, undefined> extends readonly (infer TElement)[] ? TElement : Exclude<ValueOf<TField>, undefined>;
|
|
76
|
+
type OperatorMethods<TField> = ('is' extends OperatorNames<TField> ? {
|
|
77
|
+
is(value: OperatorValue<TField>): KernelFilterExprV1;
|
|
78
|
+
} : {}) & ('isNot' extends OperatorNames<TField> ? {
|
|
79
|
+
isNot(value: OperatorValue<TField>): KernelFilterExprV1;
|
|
80
|
+
} : {}) & ('isAnyOf' extends OperatorNames<TField> ? {
|
|
81
|
+
isAnyOf(values: readonly OperatorValue<TField>[]): KernelFilterExprV1;
|
|
82
|
+
} : {}) & ('startsWith' extends OperatorNames<TField> ? {
|
|
83
|
+
startsWith(value: string): KernelFilterExprV1;
|
|
84
|
+
} & ('contains' extends OperatorNames<TField> ? {
|
|
85
|
+
contains(value: string): KernelFilterExprV1;
|
|
86
|
+
} : {}) : {}) & ('gt' extends OperatorNames<TField> ? {
|
|
87
|
+
gt(value: number): KernelFilterExprV1;
|
|
88
|
+
} : {}) & ('lt' extends OperatorNames<TField> ? {
|
|
89
|
+
lt(value: number): KernelFilterExprV1;
|
|
90
|
+
} : {}) & ('after' extends OperatorNames<TField> ? {
|
|
91
|
+
after(value: string | Date): KernelFilterExprV1;
|
|
92
|
+
} : {}) & ('before' extends OperatorNames<TField> ? {
|
|
93
|
+
before(value: string | Date): KernelFilterExprV1;
|
|
94
|
+
} : {}) & ('between' extends OperatorNames<TField> ? {
|
|
95
|
+
between(min: number, max: number): KernelFilterExprV1;
|
|
96
|
+
} : {}) & ('isEmpty' extends OperatorNames<TField> ? {
|
|
97
|
+
isEmpty(): KernelFilterExprV1;
|
|
98
|
+
} : {}) & ('isNotEmpty' extends OperatorNames<TField> ? {
|
|
99
|
+
isNotEmpty(): KernelFilterExprV1;
|
|
100
|
+
} : {}) & ('hasAny' extends OperatorNames<TField> ? {
|
|
101
|
+
hasAny(values: readonly OperatorValue<TField>[]): KernelFilterExprV1;
|
|
102
|
+
} : {}) & ('hasAll' extends OperatorNames<TField> ? {
|
|
103
|
+
hasAll(values: readonly OperatorValue<TField>[]): KernelFilterExprV1;
|
|
104
|
+
} : {}) & ('doesNotInclude' extends OperatorNames<TField> ? {
|
|
105
|
+
doesNotInclude(values: readonly OperatorValue<TField>[]): KernelFilterExprV1;
|
|
106
|
+
} : {});
|
|
107
|
+
export interface QueryRoutePathAccessor {
|
|
108
|
+
is(value: string): KernelFilterExprV1;
|
|
109
|
+
isNot(value: string): KernelFilterExprV1;
|
|
110
|
+
isAnyOf(values: readonly string[]): KernelFilterExprV1;
|
|
111
|
+
isEmpty(): KernelFilterExprV1;
|
|
112
|
+
isNotEmpty(): KernelFilterExprV1;
|
|
113
|
+
}
|
|
114
|
+
export interface QueryRouteAccessors {
|
|
115
|
+
readonly pathname: QueryRoutePathAccessor & {
|
|
116
|
+
startsWith(value: string): KernelFilterExprV1;
|
|
117
|
+
inSection(value: string): KernelFilterExprV1;
|
|
118
|
+
notInSection(value: string): KernelFilterExprV1;
|
|
119
|
+
};
|
|
120
|
+
readonly parent: QueryRoutePathAccessor;
|
|
121
|
+
readonly depth: {
|
|
122
|
+
is(value: number): KernelFilterExprV1;
|
|
123
|
+
isNot(value: number): KernelFilterExprV1;
|
|
124
|
+
gt(value: number): KernelFilterExprV1;
|
|
125
|
+
lt(value: number): KernelFilterExprV1;
|
|
126
|
+
between(min: number, max: number): KernelFilterExprV1;
|
|
127
|
+
isEmpty(): KernelFilterExprV1;
|
|
128
|
+
isNotEmpty(): KernelFilterExprV1;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
type ChildAccessors<TField> = ChildrenOf<TField> extends object ? {
|
|
132
|
+
readonly [K in keyof ChildrenOf<TField>]: FieldAccessor<ChildrenOf<TField>[K]>;
|
|
133
|
+
} : {};
|
|
134
|
+
export type FieldAccessor<TField> = OperatorMethods<TField> & ChildAccessors<TField> & (IsArray<TField> extends true ? {
|
|
135
|
+
readonly elements: OperatorMethods<TField> & ChildAccessors<TField>;
|
|
136
|
+
} : {});
|
|
137
|
+
export type QueryFieldAccessors<TFields extends object> = {
|
|
138
|
+
readonly [K in keyof TFields]: FieldAccessor<TFields[K]>;
|
|
139
|
+
};
|
|
140
|
+
declare const sortFieldBrand: unique symbol;
|
|
141
|
+
export interface QuerySortField<TField> {
|
|
142
|
+
readonly descriptorId: string;
|
|
143
|
+
readonly [sortFieldBrand]: TField;
|
|
144
|
+
}
|
|
145
|
+
export type QuerySortAccessors<TFields extends object> = {
|
|
146
|
+
readonly [K in keyof TFields as TFields[K] extends {
|
|
147
|
+
readonly sortable: true;
|
|
148
|
+
} ? K : never]: QuerySortField<TFields[K]>;
|
|
149
|
+
};
|
|
150
|
+
export interface QuerySortClause {
|
|
151
|
+
readonly descriptorId: string;
|
|
152
|
+
readonly direction: 'asc' | 'desc';
|
|
153
|
+
}
|
|
154
|
+
export type QuerySortTuple = readonly [
|
|
155
|
+
QuerySortClause,
|
|
156
|
+
QuerySortClause?,
|
|
157
|
+
QuerySortClause?,
|
|
158
|
+
QuerySortClause?
|
|
159
|
+
];
|
|
160
|
+
type QueryAddress<TStory> = {
|
|
161
|
+
readonly id: string;
|
|
162
|
+
readonly slug?: never;
|
|
163
|
+
readonly pathname?: never;
|
|
164
|
+
} | (TStory extends {
|
|
165
|
+
readonly slug: true;
|
|
166
|
+
} ? {
|
|
167
|
+
readonly id?: never;
|
|
168
|
+
readonly slug: string;
|
|
169
|
+
readonly pathname?: never;
|
|
170
|
+
} : never) | (TStory extends {
|
|
171
|
+
readonly pathname: true;
|
|
172
|
+
} ? {
|
|
173
|
+
readonly id?: never;
|
|
174
|
+
readonly slug?: never;
|
|
175
|
+
readonly pathname: string | readonly string[] | undefined;
|
|
176
|
+
} : never);
|
|
177
|
+
export interface QueryCompileResult {
|
|
178
|
+
readonly query: CompiledQueryV2;
|
|
179
|
+
readonly canonical: string;
|
|
180
|
+
}
|
|
181
|
+
interface SelectedQueryCommon<TStory, TSelection> {
|
|
182
|
+
locale(code: string): this;
|
|
183
|
+
compile(): QueryCompileResult;
|
|
184
|
+
}
|
|
185
|
+
export interface AddressedQuery<TStory, TSelection> extends SelectedQueryCommon<TStory, TSelection> {
|
|
186
|
+
fetch(options?: QueryExecuteOptions): Promise<SelectedDocument<TStory, TSelection> | null>;
|
|
187
|
+
execute(options?: QueryExecuteOptions): Promise<QueryResultV2<SelectedDocument<TStory, TSelection> | null, null>>;
|
|
188
|
+
}
|
|
189
|
+
export interface SingletonQuery<TStory, TSelection> extends AddressedQuery<TStory, TSelection> {
|
|
190
|
+
}
|
|
191
|
+
export interface PagedQuery<TStory, TSelection> extends SelectedQueryCommon<TStory, TSelection> {
|
|
192
|
+
publicationFallback(policy: 'none' | 'configured'): this;
|
|
193
|
+
fetch(options?: QueryExecuteOptions): Promise<readonly SelectedDocument<TStory, TSelection>[]>;
|
|
194
|
+
execute(options?: QueryExecuteOptions): Promise<QueryResultV2<readonly SelectedDocument<TStory, TSelection>[], QueryPageInfo>>;
|
|
195
|
+
}
|
|
196
|
+
export interface CollectionQuery<TStory, TSelection, TOrdered extends boolean = false> extends SelectedQueryCommon<TStory, TSelection> {
|
|
197
|
+
/** Allow published fallback languages for this collection only. */
|
|
198
|
+
publicationFallback(policy: 'none' | 'configured'): this;
|
|
199
|
+
where(callback: (fields: QueryFieldAccessors<FieldsOf<TStory>>, helpers: {
|
|
200
|
+
readonly route: TStory extends {
|
|
201
|
+
readonly pathname: true;
|
|
202
|
+
} ? QueryRouteAccessors : never;
|
|
203
|
+
and(...children: readonly KernelFilterExprV1[]): KernelFilterExprV1;
|
|
204
|
+
or(...children: readonly KernelFilterExprV1[]): KernelFilterExprV1;
|
|
205
|
+
}) => KernelFilterExprV1): CollectionQuery<TStory, TSelection, TOrdered>;
|
|
206
|
+
orderBy: TOrdered extends true ? never : (callback: (fields: QuerySortAccessors<FieldsOf<TStory>>, helpers: {
|
|
207
|
+
asc(field: QuerySortField<unknown>): QuerySortClause;
|
|
208
|
+
desc(field: QuerySortField<unknown>): QuerySortClause;
|
|
209
|
+
}) => QuerySortTuple) => CollectionQuery<TStory, TSelection, true>;
|
|
210
|
+
page(input: {
|
|
211
|
+
readonly first: number;
|
|
212
|
+
readonly after?: string;
|
|
213
|
+
}): PagedQuery<TStory, TSelection>;
|
|
214
|
+
by(address: QueryAddress<TStory>): AddressedQuery<TStory, TSelection>;
|
|
215
|
+
}
|
|
216
|
+
type SelectedQuery<TStory, TSelection> = TStory extends {
|
|
217
|
+
readonly singleton: true;
|
|
218
|
+
} ? SingletonQuery<TStory, TSelection> : CollectionQuery<TStory, TSelection>;
|
|
219
|
+
export interface UnselectedQuery<TStory> {
|
|
220
|
+
select<const TSelection extends QuerySelectionInput<FieldsOf<TStory>>>(selection: TSelection): SelectedQuery<TStory, TSelection>;
|
|
221
|
+
selectAll(): SelectedQuery<TStory, 'all'>;
|
|
222
|
+
}
|
|
223
|
+
export type QueryForSchema<TSchema extends QuerySchemaDefinition, TStoryName extends keyof QueryStoriesOf<TSchema>> = UnselectedQuery<QueryStoriesOf<TSchema>[TStoryName]>;
|
|
224
|
+
export declare function createUnselectedQuery<TStory>(host: QueryExecutionHost, storyType: string): UnselectedQuery<TStory>;
|
|
225
|
+
export {};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { PATHNAME_QUERY_OPERATORS, isValidPathnameQueryClause, normalizeQueryPathname, } from '@storystick/types/pathname-query';
|
|
2
|
+
import { decodeQueryCursorV2, } from '@storystick/types/query-contract';
|
|
3
|
+
import { assertExactQueryResult } from './errors.js';
|
|
4
|
+
import { normalizePathname } from './normalize-pathname.js';
|
|
5
|
+
import { compileQueryState } from './query-compiler.js';
|
|
6
|
+
const EXECUTABLE_QUERY_HOSTS = new WeakMap();
|
|
7
|
+
/** Internal framework seam. Executable queries retain their owning Client. */
|
|
8
|
+
export function getExecutableQueryDescriptor(query) {
|
|
9
|
+
const host = EXECUTABLE_QUERY_HOSTS.get(query);
|
|
10
|
+
if (!host) {
|
|
11
|
+
throw new TypeError('storystick/QUERY_INVALID: Expected a query created by createClient().');
|
|
12
|
+
}
|
|
13
|
+
return { host };
|
|
14
|
+
}
|
|
15
|
+
export function createUnselectedQuery(host, storyType) {
|
|
16
|
+
return new RuntimeQuery(host, {
|
|
17
|
+
storyType,
|
|
18
|
+
cardinality: 'many',
|
|
19
|
+
locale: {
|
|
20
|
+
code: host.defaultLocale ?? 'default',
|
|
21
|
+
fallback: 'configured',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
class RuntimeQuery {
|
|
26
|
+
host;
|
|
27
|
+
state;
|
|
28
|
+
constructor(host, state) {
|
|
29
|
+
this.host = host;
|
|
30
|
+
this.state = state;
|
|
31
|
+
EXECUTABLE_QUERY_HOSTS.set(this, host);
|
|
32
|
+
}
|
|
33
|
+
select(selection) {
|
|
34
|
+
if (!selection || Object.keys(selection).length === 0)
|
|
35
|
+
throw queryInvalid('select requires at least one field.');
|
|
36
|
+
return this.with({ selection: compileSelection(selection) });
|
|
37
|
+
}
|
|
38
|
+
selectAll() {
|
|
39
|
+
return this.with({ selection: { mode: 'all' } });
|
|
40
|
+
}
|
|
41
|
+
where(callback) {
|
|
42
|
+
this.requireSelection();
|
|
43
|
+
const filter = callback(fieldProxy(this.state.storyType, ''), { and, or, route: routeAccessors() });
|
|
44
|
+
return this.with({ filter: this.state.filter ? and(this.state.filter, filter) : filter });
|
|
45
|
+
}
|
|
46
|
+
orderBy(callback) {
|
|
47
|
+
this.requireSelection();
|
|
48
|
+
if (this.state.sort)
|
|
49
|
+
throw queryInvalid('orderBy can be called only once.');
|
|
50
|
+
const sort = callback(sortProxy(this.state.storyType), { asc, desc });
|
|
51
|
+
if (!Array.isArray(sort) || sort.length < 1 || sort.length > 4)
|
|
52
|
+
throw queryInvalid('orderBy requires one to four clauses.');
|
|
53
|
+
return this.with({ sort: [...sort] });
|
|
54
|
+
}
|
|
55
|
+
page(input) {
|
|
56
|
+
this.requireSelection();
|
|
57
|
+
if (!Number.isSafeInteger(input.first) || input.first < 1 || input.first > 100) {
|
|
58
|
+
throw queryInvalid('page.first must be an integer between 1 and 100.');
|
|
59
|
+
}
|
|
60
|
+
if (input.after !== undefined)
|
|
61
|
+
decodeQueryCursorV2(input.after);
|
|
62
|
+
return this.with({ page: { first: input.first, ...(input.after ? { after: input.after } : {}) } });
|
|
63
|
+
}
|
|
64
|
+
by(address) {
|
|
65
|
+
this.requireSelection();
|
|
66
|
+
const entries = Object.entries(address).filter(([, value]) => value !== undefined);
|
|
67
|
+
if (entries.length !== 1)
|
|
68
|
+
throw queryInvalid('by requires exactly one address.');
|
|
69
|
+
const [kind, raw] = entries[0];
|
|
70
|
+
if (kind !== 'id' && kind !== 'slug' && kind !== 'pathname')
|
|
71
|
+
throw queryInvalid('Unknown query address.');
|
|
72
|
+
if (kind === 'pathname' && typeof raw !== 'string' && !(Array.isArray(raw) && raw.every(part => typeof part === 'string'))) {
|
|
73
|
+
throw queryInvalid('by.pathname requires a string or an array of string segments.');
|
|
74
|
+
}
|
|
75
|
+
const value = kind === 'pathname'
|
|
76
|
+
? normalizePathname(raw)
|
|
77
|
+
: raw;
|
|
78
|
+
if (typeof value !== 'string' || (kind !== 'pathname' && value.length === 0))
|
|
79
|
+
throw queryInvalid(`by.${kind} requires a value.`);
|
|
80
|
+
const runtime = this.host.schema.stories[this.state.storyType];
|
|
81
|
+
if (!runtime?.addresses.includes(kind))
|
|
82
|
+
throw queryInvalid(`Address ${kind} is not available for ${this.state.storyType}.`);
|
|
83
|
+
return this.with({ cardinality: 'one', address: { kind, value } });
|
|
84
|
+
}
|
|
85
|
+
locale(code) {
|
|
86
|
+
this.requireSelection();
|
|
87
|
+
const normalized = code.trim();
|
|
88
|
+
if (!normalized)
|
|
89
|
+
throw queryInvalid('locale requires a non-empty code.');
|
|
90
|
+
return this.with({ locale: { code: normalized, fallback: 'configured' } });
|
|
91
|
+
}
|
|
92
|
+
publicationFallback(policy) {
|
|
93
|
+
this.requireSelection();
|
|
94
|
+
if (policy !== 'none' && policy !== 'configured')
|
|
95
|
+
throw queryInvalid('Invalid publication fallback policy.');
|
|
96
|
+
return this.with({ publicationFallback: policy });
|
|
97
|
+
}
|
|
98
|
+
compile() {
|
|
99
|
+
this.requireSelection();
|
|
100
|
+
const runtime = this.host.schema.stories[this.state.storyType];
|
|
101
|
+
const singleton = runtime?.singleton === true;
|
|
102
|
+
const cardinality = singleton ? 'one' : this.state.cardinality;
|
|
103
|
+
if (cardinality !== 'many' && this.state.publicationFallback !== undefined) {
|
|
104
|
+
throw queryInvalid('Publication fallback applies only to a collection.');
|
|
105
|
+
}
|
|
106
|
+
if (cardinality === 'many' && !this.state.page)
|
|
107
|
+
throw queryInvalid('Collection queries require page().');
|
|
108
|
+
const query = {
|
|
109
|
+
version: 2,
|
|
110
|
+
target: { kind: 'documents', storyType: this.state.storyType },
|
|
111
|
+
selection: this.state.selection,
|
|
112
|
+
...(this.state.filter ? { filter: this.state.filter } : {}),
|
|
113
|
+
...(this.state.sort ? { sort: this.state.sort } : {}),
|
|
114
|
+
cardinality,
|
|
115
|
+
...(singleton ? { address: { kind: 'singleton' } } : this.state.address ? { address: this.state.address } : {}),
|
|
116
|
+
...(this.state.page ? { page: this.state.page } : {}),
|
|
117
|
+
...(this.state.publicationFallback ? { publicationFallback: this.state.publicationFallback } : {}),
|
|
118
|
+
locale: this.state.locale,
|
|
119
|
+
generatedSchemaFingerprint: this.host.schema.fingerprint,
|
|
120
|
+
};
|
|
121
|
+
return compileQueryState(this, query);
|
|
122
|
+
}
|
|
123
|
+
async fetch(options) {
|
|
124
|
+
const result = await this.execute(options);
|
|
125
|
+
assertExactQueryResult(result);
|
|
126
|
+
return result.data;
|
|
127
|
+
}
|
|
128
|
+
execute(options) {
|
|
129
|
+
const { query, canonical } = this.compile();
|
|
130
|
+
return this.host.executeCompiled(query, canonical, options);
|
|
131
|
+
}
|
|
132
|
+
with(patch) {
|
|
133
|
+
return new RuntimeQuery(this.host, Object.freeze({ ...this.state, ...patch }));
|
|
134
|
+
}
|
|
135
|
+
requireSelection() {
|
|
136
|
+
if (!this.state.selection)
|
|
137
|
+
throw queryInvalid('select() or selectAll() is required.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function compileSelection(input, base = '') {
|
|
141
|
+
const fields = [];
|
|
142
|
+
for (const [key, value] of Object.entries(input).sort(([a], [b]) => a.localeCompare(b))) {
|
|
143
|
+
const path = base ? `${base}.${key}` : key;
|
|
144
|
+
if (value === true) {
|
|
145
|
+
fields.push({ kind: 'field', path });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
149
|
+
throw queryInvalid(`Invalid selection at ${path}.`);
|
|
150
|
+
if ('resolve' in value) {
|
|
151
|
+
const resolve = value.resolve;
|
|
152
|
+
if (!resolve || typeof resolve !== 'object' || Array.isArray(resolve))
|
|
153
|
+
throw queryInvalid(`Invalid relation selection at ${path}.`);
|
|
154
|
+
if ('on' in resolve) {
|
|
155
|
+
const rawOn = resolve.on;
|
|
156
|
+
if (!rawOn || typeof rawOn !== 'object' || Array.isArray(rawOn))
|
|
157
|
+
throw queryInvalid(`Invalid polymorphic selection at ${path}.`);
|
|
158
|
+
const on = {};
|
|
159
|
+
for (const [story, selection] of Object.entries(rawOn)) {
|
|
160
|
+
if (!selection || typeof selection !== 'object' || Array.isArray(selection))
|
|
161
|
+
throw queryInvalid(`Invalid polymorphic selection at ${path}.`);
|
|
162
|
+
on[story] = compileSelection(selection);
|
|
163
|
+
}
|
|
164
|
+
fields.push({ kind: 'relation', path, on });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
fields.push({ kind: 'relation', path, selection: compileSelection(resolve) });
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const nested = compileSelection(value, path);
|
|
172
|
+
fields.push(...(nested.mode === 'select' ? nested.fields : []));
|
|
173
|
+
}
|
|
174
|
+
if (fields.length === 0)
|
|
175
|
+
throw queryInvalid('select requires at least one field.');
|
|
176
|
+
return { mode: 'select', fields };
|
|
177
|
+
}
|
|
178
|
+
function fieldProxy(storyType, base) {
|
|
179
|
+
const callable = () => undefined;
|
|
180
|
+
return new Proxy(callable, {
|
|
181
|
+
get(_target, property) {
|
|
182
|
+
if (typeof property !== 'string')
|
|
183
|
+
return undefined;
|
|
184
|
+
if (property === 'elements')
|
|
185
|
+
return fieldProxy(storyType, `${base}[]`);
|
|
186
|
+
if (OPERATOR_MAP[property]) {
|
|
187
|
+
return (...values) => clause(storyType, base, OPERATOR_MAP[property], normalizeOperatorValue(property, values));
|
|
188
|
+
}
|
|
189
|
+
return fieldProxy(storyType, base ? `${base}.${property}` : property);
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const OPERATOR_MAP = {
|
|
194
|
+
is: 'is', isNot: 'is_not', isAnyOf: 'is_any_of', startsWith: 'starts_with', contains: 'contains',
|
|
195
|
+
inSection: 'in_section', notInSection: 'not_in_section',
|
|
196
|
+
gt: 'gt', lt: 'lt', after: 'after', before: 'before', between: 'between',
|
|
197
|
+
isEmpty: 'empty', isNotEmpty: 'not_empty', hasAny: 'has_any', hasAll: 'has_all',
|
|
198
|
+
doesNotInclude: 'does_not_include',
|
|
199
|
+
};
|
|
200
|
+
function routeAccessors() {
|
|
201
|
+
function accessor(field) {
|
|
202
|
+
return Object.fromEntries(Object.entries(OPERATOR_MAP)
|
|
203
|
+
.filter(([, op]) => PATHNAME_QUERY_OPERATORS[field].includes(op))
|
|
204
|
+
.map(([name, op]) => [
|
|
205
|
+
name,
|
|
206
|
+
(...values) => {
|
|
207
|
+
const value = normalizeOperatorValue(name, values);
|
|
208
|
+
if (!isValidPathnameQueryClause(field, op, value))
|
|
209
|
+
throw queryInvalid(`Invalid ${field} filter.`);
|
|
210
|
+
const canonical = typeof value === 'string'
|
|
211
|
+
? normalizeQueryPathname(value)
|
|
212
|
+
: Array.isArray(value)
|
|
213
|
+
? value.map((entry) => typeof entry === 'string' ? normalizeQueryPathname(entry) : entry)
|
|
214
|
+
: value;
|
|
215
|
+
return {
|
|
216
|
+
kind: 'clause',
|
|
217
|
+
clause: {
|
|
218
|
+
descriptorId: `system.documents.${field}`,
|
|
219
|
+
op,
|
|
220
|
+
...(canonical === undefined ? {} : { value: canonical }),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
]));
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
pathname: accessor('resolvedPathname'),
|
|
228
|
+
parent: accessor('pathnameParent'),
|
|
229
|
+
depth: accessor('pathnameDepth'),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function normalizeOperatorValue(operator, values) {
|
|
233
|
+
if (operator === 'isEmpty' || operator === 'isNotEmpty')
|
|
234
|
+
return undefined;
|
|
235
|
+
if (operator === 'between')
|
|
236
|
+
return values.map(normalizeScalar);
|
|
237
|
+
return normalizeScalar(values[0]);
|
|
238
|
+
}
|
|
239
|
+
function normalizeScalar(value) {
|
|
240
|
+
if (value instanceof Date) {
|
|
241
|
+
if (!Number.isFinite(value.getTime()))
|
|
242
|
+
throw queryInvalid('Invalid date value.');
|
|
243
|
+
return value.toISOString();
|
|
244
|
+
}
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
if (value.length === 0)
|
|
247
|
+
throw queryInvalid('Operator arrays cannot be empty.');
|
|
248
|
+
return value.map(normalizeScalar);
|
|
249
|
+
}
|
|
250
|
+
if (typeof value === 'number' && !Number.isFinite(value))
|
|
251
|
+
throw queryInvalid('Query numbers must be finite.');
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
function clause(storyType, path, op, value) {
|
|
255
|
+
if (!path)
|
|
256
|
+
throw queryInvalid('A filter operator must target a field.');
|
|
257
|
+
const booleanOp = op === 'is' && typeof value === 'boolean' ? (value ? 'is_true' : 'is_false') : op;
|
|
258
|
+
return {
|
|
259
|
+
kind: 'clause',
|
|
260
|
+
clause: {
|
|
261
|
+
descriptorId: `field.${storyType}.${path}`,
|
|
262
|
+
op: booleanOp,
|
|
263
|
+
...(booleanOp === 'is_true' || booleanOp === 'is_false' || value === undefined ? {} : { value }),
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function and(...children) {
|
|
268
|
+
if (children.length === 0)
|
|
269
|
+
throw queryInvalid('and requires at least one child.');
|
|
270
|
+
return { kind: 'group', op: 'and', children: [...children] };
|
|
271
|
+
}
|
|
272
|
+
function or(...children) {
|
|
273
|
+
if (children.length === 0)
|
|
274
|
+
throw queryInvalid('or requires at least one child.');
|
|
275
|
+
return { kind: 'group', op: 'or', children: [...children] };
|
|
276
|
+
}
|
|
277
|
+
function sortProxy(storyType) {
|
|
278
|
+
return new Proxy({}, {
|
|
279
|
+
get(_target, property) {
|
|
280
|
+
return typeof property === 'string'
|
|
281
|
+
? { descriptorId: `field.${storyType}.${property}` }
|
|
282
|
+
: undefined;
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function asc(field) {
|
|
287
|
+
return { descriptorId: field.descriptorId, direction: 'asc' };
|
|
288
|
+
}
|
|
289
|
+
function desc(field) {
|
|
290
|
+
return { descriptorId: field.descriptorId, direction: 'desc' };
|
|
291
|
+
}
|
|
292
|
+
function queryInvalid(message) {
|
|
293
|
+
return new TypeError(`storystick/QUERY_INVALID: ${message}`);
|
|
294
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PreviewReadContext } from '@storystick/types/preview-query-targeting';
|
|
2
|
+
import type { ClientAuth, DefaultRequestOptions, RequestContext, Transport, PublicTransport } from './types.js';
|
|
3
|
+
import { type QueryExecutionHost, type QueryForSchema } from './query-builder.js';
|
|
4
|
+
import type { QuerySchemaDefinition, QueryStoriesOf } from './query-schema.js';
|
|
5
|
+
import { type LocaleRoutingSettings } from './locale-routing.js';
|
|
6
|
+
export type ClientOptions<TSchema extends QuerySchemaDefinition> = ClientAuth & {
|
|
7
|
+
readonly schema: TSchema;
|
|
8
|
+
readonly projectId: string;
|
|
9
|
+
readonly endpoint?: string;
|
|
10
|
+
/** Explicit public Query V2 gateway; control and Preview keep endpoint. */
|
|
11
|
+
readonly deliveryEndpoint?: string;
|
|
12
|
+
/** Customer application build identity for generation-paired reads. */
|
|
13
|
+
readonly applicationBuildId?: string;
|
|
14
|
+
readonly transport?: PublicTransport;
|
|
15
|
+
readonly defaults?: DefaultRequestOptions;
|
|
16
|
+
/** Routing metadata carried by returned documents across rendering boundaries. */
|
|
17
|
+
readonly localeRouting?: LocaleRoutingSettings;
|
|
18
|
+
};
|
|
19
|
+
export interface Client<TSchema extends QuerySchemaDefinition = QuerySchemaDefinition> {
|
|
20
|
+
readonly schema: TSchema;
|
|
21
|
+
readonly defaultLocale?: string;
|
|
22
|
+
readonly projectId: string;
|
|
23
|
+
query<TStoryName extends keyof QueryStoriesOf<TSchema> & string>(storyType: TStoryName): QueryForSchema<TSchema, TStoryName>;
|
|
24
|
+
}
|
|
25
|
+
export type InternalClientOptions<TSchema extends QuerySchemaDefinition> = ClientOptions<TSchema> & {
|
|
26
|
+
readonly requestContext?: RequestContext;
|
|
27
|
+
};
|
|
28
|
+
export interface InternalClientRuntime {
|
|
29
|
+
readonly contextEpoch: string;
|
|
30
|
+
readonly executionHost: QueryExecutionHost;
|
|
31
|
+
readonly transport: Transport;
|
|
32
|
+
readonly requestContext: RequestContext | undefined;
|
|
33
|
+
readonly defaults: DefaultRequestOptions | undefined;
|
|
34
|
+
readonly localeRouting: LocaleRoutingSettings | undefined;
|
|
35
|
+
}
|
|
36
|
+
export interface InternalPreviewAuthorityScope {
|
|
37
|
+
readonly readContext?: PreviewReadContext | null;
|
|
38
|
+
readonly projectId: string;
|
|
39
|
+
readonly previewSessionId: string;
|
|
40
|
+
readonly previewObservationId: string;
|
|
41
|
+
readonly previewToken: string;
|
|
42
|
+
readonly variantId: string | null;
|
|
43
|
+
readonly releaseId: string | null;
|
|
44
|
+
}
|
|
45
|
+
export declare function getInternalClientRuntime(client: Client): InternalClientRuntime;
|
|
46
|
+
/** Compare private Preview authority in memory without returning or serializing it. */
|
|
47
|
+
export declare function clientMatchesPreviewAuthority(authority: Client, expected: InternalPreviewAuthorityScope): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Internal adapter primitive: keep a trusted client's transport authority and
|
|
50
|
+
* request scope while replacing only its generated schema surface.
|
|
51
|
+
*/
|
|
52
|
+
export declare function bindClientSchemaToAuthority<const TSchema extends QuerySchemaDefinition>(authority: Client, schema: TSchema): Client<TSchema>;
|
|
53
|
+
export declare function createClient<const TSchema extends QuerySchemaDefinition>(options: ClientOptions<TSchema>): Client<TSchema>;
|
|
54
|
+
export declare function createInternalClient<const TSchema extends QuerySchemaDefinition>(options: InternalClientOptions<TSchema>): Client<TSchema>;
|