@soda-gql/colocation-tools 0.2.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/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # @soda-gql/colocation-tools
2
+
3
+ Utilities for colocating GraphQL fragments with components in soda-gql. This package provides tools for fragment composition and data masking patterns.
4
+
5
+ ## Features
6
+
7
+ - **Fragment colocation** - Keep GraphQL fragments close to components that use them
8
+ - **Data projection** - Create typed projections from fragment data
9
+ - **Type safety** - Full TypeScript support for fragment composition
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @soda-gql/colocation-tools
15
+ # or
16
+ bun add @soda-gql/colocation-tools
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Fragment Colocation Pattern
22
+
23
+ ```typescript
24
+ import { createProjection, createExecutionResultParser } from "@soda-gql/colocation-tools";
25
+ import { userFragment } from "./graphql-system";
26
+
27
+ // Create a projection with paths and handle function
28
+ const userProjection = createProjection(userFragment, {
29
+ paths: ["$.user.id", "$.user.name"],
30
+ handle: (result) => {
31
+ if (result.isError()) return { error: result.error, user: null };
32
+ if (result.isEmpty()) return { error: null, user: null };
33
+ const data = result.unwrap();
34
+ return { error: null, user: data };
35
+ },
36
+ });
37
+
38
+ // Use with execution result parser
39
+ const parser = createExecutionResultParser({
40
+ user: userProjection,
41
+ });
42
+ ```
43
+
44
+ ### Embedding Fragments
45
+
46
+ Fragments can be embedded in operations:
47
+
48
+ ```typescript
49
+ import { gql } from "./graphql-system";
50
+ import { userFragment } from "./UserCard";
51
+
52
+ export const getUserQuery = gql.default(({ query }) =>
53
+ query.operation({ name: "GetUser" }, ({ f }) => [
54
+ f.user({ id: "1" })(userFragment.embed()),
55
+ ]),
56
+ );
57
+ ```
58
+
59
+ ### Using with $colocate
60
+
61
+ When composing multiple fragments in a single operation, use `$colocate` to prefix field selections with labels. The `createExecutionResultParser` will use these same labels to extract the corresponding data.
62
+
63
+ #### Complete Workflow
64
+
65
+ **Step 1: Define component fragments**
66
+
67
+ ```typescript
68
+ // UserCard.tsx
69
+ export const userCardFragment = gql.default(({ fragment }, { $var }) =>
70
+ fragment.Query({ variables: [$var("userId").scalar("ID:!")] }, ({ f, $ }) => [
71
+ f.user({ id: $.userId })(({ f }) => [f.id(), f.name(), f.email()]),
72
+ ]),
73
+ );
74
+
75
+ export const userCardProjection = createProjection(userCardFragment, {
76
+ paths: ["$.user"],
77
+ handle: (result) => {
78
+ if (result.isError()) return { error: result.error, user: null };
79
+ if (result.isEmpty()) return { error: null, user: null };
80
+ return { error: null, user: result.unwrap().user };
81
+ },
82
+ });
83
+ ```
84
+
85
+ **Step 2: Compose operation with $colocate**
86
+
87
+ ```typescript
88
+ // UserPage.tsx
89
+ import { userCardFragment, userCardProjection } from "./UserCard";
90
+ import { postListFragment, postListProjection } from "./PostList";
91
+
92
+ export const userPageQuery = gql.default(({ query }, { $var, $colocate }) =>
93
+ query.operation(
94
+ { name: "UserPage", variables: [$var("userId").scalar("ID:!")] },
95
+ ({ $ }) => [
96
+ $colocate({
97
+ userCard: userCardFragment.embed({ userId: $.userId }),
98
+ postList: postListFragment.embed({ userId: $.userId }),
99
+ }),
100
+ ],
101
+ ),
102
+ );
103
+ ```
104
+
105
+ **Step 3: Create parser with matching labels**
106
+
107
+ ```typescript
108
+ const parseUserPageResult = createExecutionResultParser({
109
+ userCard: userCardProjection,
110
+ postList: postListProjection,
111
+ });
112
+ ```
113
+
114
+ **Step 4: Parse execution result**
115
+
116
+ ```typescript
117
+ const result = await executeQuery(userPageQuery);
118
+ const { userCard, postList } = parseUserPageResult(result);
119
+ // userCard and postList contain the projected data
120
+ ```
121
+
122
+ The labels in `$colocate` (`userCard`, `postList`) must match the labels in `createExecutionResultParser` for proper data routing.
123
+
124
+ ## API
125
+
126
+ ### createProjection
127
+
128
+ Creates a typed projection from a fragment definition with specified paths and handler.
129
+
130
+ ```typescript
131
+ import { createProjection } from "@soda-gql/colocation-tools";
132
+
133
+ const projection = createProjection(fragment, {
134
+ // Field paths to extract (must start with "$.")
135
+ paths: ["$.user.id", "$.user.name"],
136
+ // Handler to transform the sliced result
137
+ handle: (result) => {
138
+ if (result.isError()) return { error: result.error, data: null };
139
+ if (result.isEmpty()) return { error: null, data: null };
140
+ return { error: null, data: result.unwrap() };
141
+ },
142
+ });
143
+ ```
144
+
145
+ ### createProjectionAttachment
146
+
147
+ Combines fragment definition and projection into a single export using `attach()`. This eliminates the need for separate projection definitions.
148
+
149
+ ```typescript
150
+ import { createProjectionAttachment } from "@soda-gql/colocation-tools";
151
+ import { gql } from "./graphql-system";
152
+
153
+ export const postListFragment = gql
154
+ .default(({ fragment }, { $var }) =>
155
+ fragment.Query({ variables: [$var("userId").scalar("ID:!")] }, ({ f, $ }) => [
156
+ f.user({ id: $.userId })(({ f }) => [f.posts({})(({ f }) => [f.id(), f.title()])]),
157
+ ]),
158
+ )
159
+ .attach(
160
+ createProjectionAttachment({
161
+ paths: ["$.user.posts"],
162
+ handle: (result) => {
163
+ if (result.isError()) return { error: result.error, posts: null };
164
+ if (result.isEmpty()) return { error: null, posts: null };
165
+ return { error: null, posts: result.unwrap().user?.posts ?? [] };
166
+ },
167
+ }),
168
+ );
169
+
170
+ // The fragment now has a .projection property
171
+ postListFragment.projection;
172
+ ```
173
+
174
+ **Benefits**:
175
+ - Single export for both fragment and projection
176
+ - Fragment can be passed directly to `createExecutionResultParser`
177
+ - Reduces boilerplate when projection logic is simple
178
+
179
+ **Using with createExecutionResultParser**:
180
+
181
+ ```typescript
182
+ const parseResult = createExecutionResultParser({
183
+ userCard: { projection: userCardProjection }, // Explicit projection
184
+ postList: postListFragment, // Fragment with attached projection
185
+ });
186
+ ```
187
+
188
+ Both patterns work with the parser - it automatically detects fragments with attached projections.
189
+
190
+ ### createExecutionResultParser
191
+
192
+ Creates a parser from labeled projections to process GraphQL execution results.
193
+
194
+ ```typescript
195
+ import { createExecutionResultParser } from "@soda-gql/colocation-tools";
196
+
197
+ const parser = createExecutionResultParser({
198
+ userData: userProjection,
199
+ postsData: postsProjection,
200
+ });
201
+
202
+ const results = parser(executionResult);
203
+ // results.userData, results.postsData
204
+ ```
205
+
206
+ ## Related Packages
207
+
208
+ - [@soda-gql/core](../core) - Core types and fragment definitions
209
+ - [@soda-gql/runtime](../runtime) - Runtime operation handling
210
+
211
+ ## License
212
+
213
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,313 @@
1
+
2
+ //#region packages/colocation-tools/src/projection.ts
3
+ /**
4
+ * Nominal type representing any slice selection regardless of schema specifics.
5
+ * Encodes how individual slices map a concrete field path to a projection
6
+ * function. Multiple selections allow slices to expose several derived values.
7
+ */
8
+ var Projection = class {
9
+ constructor(paths, projector) {
10
+ this.projector = projector;
11
+ this.paths = paths.map((path) => createProjectionPath(path));
12
+ Object.defineProperty(this, "$infer", { get() {
13
+ throw new Error("This property is only for type meta. Do not access this property directly.");
14
+ } });
15
+ }
16
+ paths;
17
+ };
18
+ function createProjectionPath(path) {
19
+ const segments = path.split(".");
20
+ if (path === "$" || segments.length <= 1) throw new Error("Field path must not be only $ or empty");
21
+ return {
22
+ full: path,
23
+ segments: segments.slice(1)
24
+ };
25
+ }
26
+
27
+ //#endregion
28
+ //#region packages/colocation-tools/src/create-projection.ts
29
+ /**
30
+ * Creates a type-safe projection from a Fragment.
31
+ *
32
+ * The projection extracts and transforms data from GraphQL execution results,
33
+ * with full type inference from the Fragment's output type.
34
+ *
35
+ * Note: The Fragment parameter is used only for type inference.
36
+ * The actual paths must be specified explicitly.
37
+ *
38
+ * @param _fragment - The Fragment to infer types from (used for type inference only)
39
+ * @param options - Projection options including paths and handle function
40
+ * @returns A Projection that can be used with createExecutionResultParser
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * const userFragment = gql(({ fragment }) =>
45
+ * fragment.Query({ variables: [...] }, ({ f, $ }) => [
46
+ * f.user({ id: $.userId })(({ f }) => [f.id(), f.name()]),
47
+ * ])
48
+ * );
49
+ *
50
+ * const userProjection = createProjection(userFragment, {
51
+ * paths: ["$.user"],
52
+ * handle: (result) => {
53
+ * if (result.isError()) return { error: result.error, user: null };
54
+ * if (result.isEmpty()) return { error: null, user: null };
55
+ * const data = result.unwrap();
56
+ * return { error: null, user: data.user };
57
+ * },
58
+ * });
59
+ * ```
60
+ */
61
+ const createProjection = (_fragment, options) => {
62
+ return new Projection(options.paths, options.handle);
63
+ };
64
+ const createProjectionAttachment = (options) => {
65
+ return {
66
+ name: "projection",
67
+ createValue: (fragment) => createProjection(fragment, options)
68
+ };
69
+ };
70
+
71
+ //#endregion
72
+ //#region packages/colocation-tools/src/utils/map-values.ts
73
+ function mapValues(obj, fn) {
74
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value, key)]));
75
+ }
76
+
77
+ //#endregion
78
+ //#region packages/colocation-tools/src/projection-path-graph.ts
79
+ function createPathGraph(paths) {
80
+ const intermediate = paths.reduce((acc, { label, raw, segments: [segment, ...segments] }) => {
81
+ if (segment) (acc[segment] || (acc[segment] = [])).push({
82
+ label,
83
+ raw,
84
+ segments
85
+ });
86
+ return acc;
87
+ }, {});
88
+ return {
89
+ matches: paths.map(({ label, raw, segments }) => ({
90
+ label,
91
+ path: raw,
92
+ exact: segments.length === 0
93
+ })),
94
+ children: mapValues(intermediate, (paths$1) => createPathGraph(paths$1))
95
+ };
96
+ }
97
+ /**
98
+ * Creates a projection path graph from slice entries with field prefixing.
99
+ * Each slice's paths are prefixed with the slice label for disambiguation.
100
+ */
101
+ function createPathGraphFromSliceEntries(fragments) {
102
+ return createPathGraph(Object.entries(fragments).flatMap(([label, slice]) => Array.from(new Map(slice.projection.paths.map(({ full: raw, segments }) => {
103
+ const [first, ...rest] = segments;
104
+ return [raw, {
105
+ label,
106
+ raw,
107
+ segments: [`${label}_${first}`, ...rest]
108
+ }];
109
+ })).values())));
110
+ }
111
+
112
+ //#endregion
113
+ //#region packages/colocation-tools/src/sliced-execution-result.ts
114
+ /** Runtime guard interface shared by all slice result variants. */
115
+ var SlicedExecutionResultGuards = class {
116
+ isSuccess() {
117
+ return this.type === "success";
118
+ }
119
+ isError() {
120
+ return this.type === "error";
121
+ }
122
+ isEmpty() {
123
+ return this.type === "empty";
124
+ }
125
+ constructor(type) {
126
+ this.type = type;
127
+ }
128
+ };
129
+ /** Variant representing an empty payload (no data, no error). */
130
+ var SlicedExecutionResultEmpty = class extends SlicedExecutionResultGuards {
131
+ constructor() {
132
+ super("empty");
133
+ }
134
+ unwrap() {
135
+ return null;
136
+ }
137
+ safeUnwrap() {
138
+ return {
139
+ data: void 0,
140
+ error: void 0
141
+ };
142
+ }
143
+ };
144
+ /** Variant representing a successful payload. */
145
+ var SlicedExecutionResultSuccess = class extends SlicedExecutionResultGuards {
146
+ constructor(data, extensions) {
147
+ super("success");
148
+ this.data = data;
149
+ this.extensions = extensions;
150
+ }
151
+ unwrap() {
152
+ return this.data;
153
+ }
154
+ safeUnwrap(transform) {
155
+ return {
156
+ data: transform(this.data),
157
+ error: void 0
158
+ };
159
+ }
160
+ };
161
+ /** Variant representing an error payload. */
162
+ var SlicedExecutionResultError = class extends SlicedExecutionResultGuards {
163
+ constructor(error, extensions) {
164
+ super("error");
165
+ this.error = error;
166
+ this.extensions = extensions;
167
+ }
168
+ unwrap() {
169
+ throw this.error;
170
+ }
171
+ safeUnwrap() {
172
+ return {
173
+ data: void 0,
174
+ error: this.error
175
+ };
176
+ }
177
+ };
178
+
179
+ //#endregion
180
+ //#region packages/colocation-tools/src/parse-execution-result.ts
181
+ const createPathGraphFromSlices = createPathGraphFromSliceEntries;
182
+ function* generateErrorMapEntries(errors, projectionPathGraph) {
183
+ for (const error of errors) {
184
+ const errorPath = error.path ?? [];
185
+ let stack = projectionPathGraph;
186
+ for (let i = 0; i <= errorPath.length; i++) {
187
+ const segment = errorPath[i];
188
+ if (segment == null || typeof segment === "number") {
189
+ yield* stack.matches.map(({ label, path }) => ({
190
+ label,
191
+ path,
192
+ error
193
+ }));
194
+ break;
195
+ }
196
+ yield* stack.matches.filter(({ exact }) => exact).map(({ label, path }) => ({
197
+ label,
198
+ path,
199
+ error
200
+ }));
201
+ const next = stack.children[segment];
202
+ if (!next) break;
203
+ stack = next;
204
+ }
205
+ }
206
+ }
207
+ const createErrorMaps = (errors, projectionPathGraph) => {
208
+ const errorMaps = {};
209
+ for (const { label, path, error } of generateErrorMapEntries(errors ?? [], projectionPathGraph)) {
210
+ const mapPerLabel = errorMaps[label] || (errorMaps[label] = {});
211
+ (mapPerLabel[path] || (mapPerLabel[path] = [])).push({ error });
212
+ }
213
+ return errorMaps;
214
+ };
215
+ const accessDataByPathSegments = (data, pathSegments) => {
216
+ let current = data;
217
+ for (const segment of pathSegments) {
218
+ if (current == null) return { error: /* @__PURE__ */ new Error("No data") };
219
+ if (typeof current !== "object") return { error: /* @__PURE__ */ new Error("Incorrect data type") };
220
+ if (Array.isArray(current)) return { error: /* @__PURE__ */ new Error("Incorrect data type") };
221
+ current = current[segment];
222
+ }
223
+ return { data: current };
224
+ };
225
+ /**
226
+ * Creates an execution result parser for composed operations.
227
+ * The parser maps GraphQL errors and data to their corresponding slices
228
+ * based on the projection path graph.
229
+ *
230
+ * @param slices - Object mapping labels to projections
231
+ * @returns A parser function that takes a NormalizedExecutionResult and returns parsed slices
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * const parser = createExecutionResultParser({
236
+ * userCard: userCardProjection,
237
+ * posts: postsProjection,
238
+ * });
239
+ *
240
+ * const results = parser({
241
+ * type: "graphql",
242
+ * body: { data, errors },
243
+ * });
244
+ * ```
245
+ */
246
+ const createExecutionResultParser = (slices) => {
247
+ const projectionPathGraph = createPathGraphFromSlices(slices);
248
+ const fragments = slices;
249
+ const prepare = (result) => {
250
+ if (result.type === "graphql") {
251
+ const errorMaps = createErrorMaps(result.body.errors, projectionPathGraph);
252
+ return {
253
+ ...result,
254
+ errorMaps
255
+ };
256
+ }
257
+ if (result.type === "non-graphql-error") return {
258
+ ...result,
259
+ error: new SlicedExecutionResultError({
260
+ type: "non-graphql-error",
261
+ error: result.error
262
+ })
263
+ };
264
+ if (result.type === "empty") return {
265
+ ...result,
266
+ error: new SlicedExecutionResultEmpty()
267
+ };
268
+ throw new Error("Invalid result type", { cause: result });
269
+ };
270
+ return (result) => {
271
+ const prepared = prepare(result);
272
+ const entries = Object.entries(fragments).map(([label, fragment]) => {
273
+ const { projection } = fragment;
274
+ if (prepared.type === "graphql") {
275
+ const matchedErrors = projection.paths.flatMap(({ full: raw }) => prepared.errorMaps[label]?.[raw] ?? []);
276
+ const uniqueErrors = Array.from(new Set(matchedErrors.map(({ error }) => error)).values());
277
+ if (uniqueErrors.length > 0) return [label, projection.projector(new SlicedExecutionResultError({
278
+ type: "graphql-error",
279
+ errors: uniqueErrors
280
+ }))];
281
+ const dataResults = projection.paths.map(({ segments }) => {
282
+ const [first, ...rest] = segments;
283
+ const prefixedSegments = [`${label}_${first}`, ...rest];
284
+ return prepared.body.data ? accessDataByPathSegments(prepared.body.data, prefixedSegments) : { error: /* @__PURE__ */ new Error("No data") };
285
+ });
286
+ if (dataResults.some(({ error }) => error)) {
287
+ const errors = dataResults.flatMap(({ error }) => error ? [error] : []);
288
+ return [label, projection.projector(new SlicedExecutionResultError({
289
+ type: "parse-error",
290
+ errors
291
+ }))];
292
+ }
293
+ const dataList = dataResults.map(({ data }) => data);
294
+ return [label, projection.projector(new SlicedExecutionResultSuccess(dataList))];
295
+ }
296
+ if (prepared.type === "non-graphql-error") return [label, projection.projector(prepared.error)];
297
+ if (prepared.type === "empty") return [label, projection.projector(prepared.error)];
298
+ throw new Error("Invalid result type", { cause: prepared });
299
+ });
300
+ return Object.fromEntries(entries);
301
+ };
302
+ };
303
+
304
+ //#endregion
305
+ exports.Projection = Projection;
306
+ exports.SlicedExecutionResultEmpty = SlicedExecutionResultEmpty;
307
+ exports.SlicedExecutionResultError = SlicedExecutionResultError;
308
+ exports.SlicedExecutionResultSuccess = SlicedExecutionResultSuccess;
309
+ exports.createExecutionResultParser = createExecutionResultParser;
310
+ exports.createPathGraphFromSliceEntries = createPathGraphFromSliceEntries;
311
+ exports.createProjection = createProjection;
312
+ exports.createProjectionAttachment = createProjectionAttachment;
313
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["projector: (result: AnySlicedExecutionResult) => TProjected","paths","type: \"success\" | \"error\" | \"empty\"","data: TData","extensions?: unknown","error: NormalizedError","errorMaps: { [label: string]: { [path: string]: { error: GraphQLFormattedError }[] } }","current: unknown"],"sources":["../src/projection.ts","../src/create-projection.ts","../src/utils/map-values.ts","../src/projection-path-graph.ts","../src/sliced-execution-result.ts","../src/parse-execution-result.ts"],"sourcesContent":["import type { AnySlicedExecutionResult } from \"./sliced-execution-result\";\nimport type { Tuple } from \"./utils/type-utils\";\n\n/** Shape of a single selection slice projection. */\n// biome-ignore lint/suspicious/noExplicitAny: Type alias for any Projection regardless of projected type\nexport type AnyProjection = Projection<any>;\n\ndeclare const __PROJECTION_BRAND__: unique symbol;\n/**\n * Nominal type representing any slice selection regardless of schema specifics.\n * Encodes how individual slices map a concrete field path to a projection\n * function. Multiple selections allow slices to expose several derived values.\n */\nexport class Projection<TProjected> {\n declare readonly [__PROJECTION_BRAND__]: void;\n\n declare readonly $infer: { readonly output: TProjected };\n\n constructor(\n paths: Tuple<string>,\n public readonly projector: (result: AnySlicedExecutionResult) => TProjected,\n ) {\n this.paths = paths.map((path) => createProjectionPath(path));\n\n Object.defineProperty(this, \"$infer\", {\n get() {\n throw new Error(\"This property is only for type meta. Do not access this property directly.\");\n },\n });\n }\n\n public readonly paths: ProjectionPath[];\n}\n\nexport type ProjectionPath = {\n full: string;\n segments: Tuple<string>;\n};\n\nfunction createProjectionPath(path: string): ProjectionPath {\n const segments = path.split(\".\");\n if (path === \"$\" || segments.length <= 1) {\n throw new Error(\"Field path must not be only $ or empty\");\n }\n\n return {\n full: path,\n segments: segments.slice(1) as Tuple<string>,\n };\n}\n\nexport type InferExecutionResultProjection<TProjection extends AnyProjection> = ReturnType<TProjection[\"projector\"]>;\n","import type { Fragment, GqlElementAttachment } from \"@soda-gql/core\";\nimport { Projection } from \"./projection\";\nimport type { SlicedExecutionResult } from \"./sliced-execution-result\";\nimport type { Tuple } from \"./utils/type-utils\";\n\n// biome-ignore lint/suspicious/noExplicitAny: Type alias for any Fragment regardless of type parameters\ntype AnyFragment = Fragment<string, any, any, any>;\n\n/**\n * Options for creating a projection from a Fragment.\n */\nexport type CreateProjectionOptions<TOutput extends object, TProjected> = {\n /**\n * Field paths to extract from the execution result.\n * Each path starts with \"$.\" and follows the field selection structure.\n *\n * @example\n * ```typescript\n * paths: [\"$.user.id\", \"$.user.name\"]\n * ```\n */\n paths: Tuple<string>;\n\n /**\n * Handler function to transform the sliced execution result.\n * Receives a SlicedExecutionResult with the Fragment's output type.\n * Handles all cases: success, error, and empty.\n *\n * @example\n * ```typescript\n * handle: (result) => {\n * if (result.isError()) return { error: result.error, data: null };\n * if (result.isEmpty()) return { error: null, data: null };\n * const data = result.unwrap();\n * return { error: null, data: { userId: data.user.id } };\n * }\n * ```\n */\n handle: (result: SlicedExecutionResult<TOutput>) => TProjected;\n};\n\n/**\n * Creates a type-safe projection from a Fragment.\n *\n * The projection extracts and transforms data from GraphQL execution results,\n * with full type inference from the Fragment's output type.\n *\n * Note: The Fragment parameter is used only for type inference.\n * The actual paths must be specified explicitly.\n *\n * @param _fragment - The Fragment to infer types from (used for type inference only)\n * @param options - Projection options including paths and handle function\n * @returns A Projection that can be used with createExecutionResultParser\n *\n * @example\n * ```typescript\n * const userFragment = gql(({ fragment }) =>\n * fragment.Query({ variables: [...] }, ({ f, $ }) => [\n * f.user({ id: $.userId })(({ f }) => [f.id(), f.name()]),\n * ])\n * );\n *\n * const userProjection = createProjection(userFragment, {\n * paths: [\"$.user\"],\n * handle: (result) => {\n * if (result.isError()) return { error: result.error, user: null };\n * if (result.isEmpty()) return { error: null, user: null };\n * const data = result.unwrap();\n * return { error: null, user: data.user };\n * },\n * });\n * ```\n */\nexport const createProjection = <TFragment extends AnyFragment, TProjected>(\n _fragment: TFragment,\n options: CreateProjectionOptions<TFragment[\"$infer\"][\"output\"], TProjected>,\n): Projection<TProjected> => {\n return new Projection(options.paths, options.handle);\n};\n\nexport const createProjectionAttachment = <TFragment extends AnyFragment, TProjected>(\n options: CreateProjectionOptions<NoInfer<TFragment>[\"$infer\"][\"output\"], TProjected>,\n): GqlElementAttachment<TFragment, \"projection\", Projection<TProjected>> => {\n return {\n name: \"projection\",\n createValue: (fragment) => createProjection(fragment, options),\n };\n};\n","type ArgEntries<T extends object> = { [K in keyof T]-?: [value: T[K], key: K] }[keyof T];\ntype Entries<T extends object> = { [K in keyof T]: [key: K, value: T[K]] }[keyof T];\n\nexport function mapValues<TObject extends object, TMappedValue>(\n obj: TObject,\n fn: (...args: ArgEntries<TObject>) => TMappedValue,\n): {\n [K in keyof TObject]: TMappedValue;\n} {\n return Object.fromEntries((Object.entries(obj) as Entries<TObject>[]).map(([key, value]) => [key, fn(value, key)])) as {\n [K in keyof TObject]: TMappedValue;\n };\n}\n","import type { AnyProjection } from \"./projection\";\nimport { mapValues } from \"./utils/map-values\";\n\n/**\n * Node in the projection path graph tree.\n * Used for mapping GraphQL errors and data to their corresponding slices.\n */\nexport type ProjectionPathGraphNode = {\n readonly matches: { label: string; path: string; exact: boolean }[];\n readonly children: { readonly [segment: string]: ProjectionPathGraphNode };\n};\n\n/**\n * Payload from a slice that contains projection.\n */\nexport type AnySlicePayload = {\n readonly projection: AnyProjection;\n};\n\nexport type AnySlicePayloads = Record<string, AnySlicePayload>;\n\ntype ExecutionResultProjectionPathGraphIntermediate = {\n [segment: string]: { label: string; raw: string; segments: string[] }[];\n};\n\nfunction createPathGraph(paths: ExecutionResultProjectionPathGraphIntermediate[string]): ProjectionPathGraphNode {\n const intermediate = paths.reduce(\n (acc: ExecutionResultProjectionPathGraphIntermediate, { label, raw, segments: [segment, ...segments] }) => {\n if (segment) {\n (acc[segment] || (acc[segment] = [])).push({ label, raw, segments });\n }\n return acc;\n },\n {},\n );\n\n return {\n matches: paths.map(({ label, raw, segments }) => ({ label, path: raw, exact: segments.length === 0 })),\n children: mapValues(intermediate, (paths) => createPathGraph(paths)),\n } satisfies ProjectionPathGraphNode;\n}\n\n/**\n * Creates a projection path graph from slice entries with field prefixing.\n * Each slice's paths are prefixed with the slice label for disambiguation.\n */\nexport function createPathGraphFromSliceEntries(fragments: AnySlicePayloads) {\n const paths = Object.entries(fragments).flatMap(([label, slice]) =>\n Array.from(\n new Map(\n slice.projection.paths.map(({ full: raw, segments }) => {\n const [first, ...rest] = segments;\n return [raw, { label, raw, segments: [`${label}_${first}`, ...rest] }];\n }),\n ).values(),\n ),\n );\n\n return createPathGraph(paths);\n}\n","/** Result-like wrapper types returned from slice projections. */\n\nimport type { NormalizedError } from \"./types\";\n\n// biome-ignore lint/suspicious/noExplicitAny: Type alias for any SlicedExecutionResult regardless of data type\nexport type AnySlicedExecutionResult = SlicedExecutionResult<any>;\n\n/**\n * Internal discriminated union describing the Result-like wrapper exposed to\n * slice selection callbacks.\n */\nexport type AnySlicedExecutionResultRecord = {\n [path: string]: AnySlicedExecutionResult;\n};\n\nexport type SafeUnwrapResult<TTransformed, TError> =\n | {\n data?: never;\n error?: never;\n }\n | {\n data: TTransformed;\n error?: never;\n }\n | {\n data?: never;\n error: TError;\n };\n\n/** Utility signature returned by the safe unwrap helper. */\ntype SlicedExecutionResultCommon<TData, TError> = {\n safeUnwrap<TTransformed>(transform: (data: TData) => TTransformed): SafeUnwrapResult<TTransformed, TError>;\n};\n\n/** Public union used by selection callbacks to inspect data, empty, or error states. */\nexport type SlicedExecutionResult<TData> =\n | SlicedExecutionResultEmpty<TData>\n | SlicedExecutionResultSuccess<TData>\n | SlicedExecutionResultError<TData>;\n\n/** Runtime guard interface shared by all slice result variants. */\nclass SlicedExecutionResultGuards<TData> {\n isSuccess(): this is SlicedExecutionResultSuccess<TData> {\n return this.type === \"success\";\n }\n isError(): this is SlicedExecutionResultError<TData> {\n return this.type === \"error\";\n }\n isEmpty(): this is SlicedExecutionResultEmpty<TData> {\n return this.type === \"empty\";\n }\n\n constructor(private readonly type: \"success\" | \"error\" | \"empty\") {}\n}\n\n/** Variant representing an empty payload (no data, no error). */\nexport class SlicedExecutionResultEmpty<TData>\n extends SlicedExecutionResultGuards<TData>\n implements SlicedExecutionResultCommon<TData, NormalizedError>\n{\n constructor() {\n super(\"empty\");\n }\n\n unwrap(): null {\n return null;\n }\n\n safeUnwrap() {\n return {\n data: undefined,\n error: undefined,\n };\n }\n}\n\n/** Variant representing a successful payload. */\nexport class SlicedExecutionResultSuccess<TData>\n extends SlicedExecutionResultGuards<TData>\n implements SlicedExecutionResultCommon<TData, NormalizedError>\n{\n constructor(\n public readonly data: TData,\n public readonly extensions?: unknown,\n ) {\n super(\"success\");\n }\n\n unwrap(): TData {\n return this.data;\n }\n\n safeUnwrap<TTransformed>(transform: (data: TData) => TTransformed) {\n return {\n data: transform(this.data),\n error: undefined,\n };\n }\n}\n\n/** Variant representing an error payload. */\nexport class SlicedExecutionResultError<TData>\n extends SlicedExecutionResultGuards<TData>\n implements SlicedExecutionResultCommon<TData, NormalizedError>\n{\n constructor(\n public readonly error: NormalizedError,\n public readonly extensions?: unknown,\n ) {\n super(\"error\");\n }\n\n unwrap(): never {\n throw this.error;\n }\n\n safeUnwrap() {\n return {\n data: undefined,\n error: this.error,\n };\n }\n}\n","import type { GraphQLFormattedError } from \"graphql\";\nimport { type AnySlicePayloads, createPathGraphFromSliceEntries, type ProjectionPathGraphNode } from \"./projection-path-graph\";\nimport { SlicedExecutionResultEmpty, SlicedExecutionResultError, SlicedExecutionResultSuccess } from \"./sliced-execution-result\";\nimport type { NormalizedExecutionResult } from \"./types\";\n\n// Internal function to build path graph from slices\nconst createPathGraphFromSlices = createPathGraphFromSliceEntries;\n\nfunction* generateErrorMapEntries(errors: readonly GraphQLFormattedError[], projectionPathGraph: ProjectionPathGraphNode) {\n for (const error of errors) {\n const errorPath = error.path ?? [];\n let stack = projectionPathGraph;\n\n for (\n let i = 0;\n // i <= errorPath.length to handle the case where the error path is empty\n i <= errorPath.length;\n i++\n ) {\n const segment = errorPath[i];\n\n if (\n // the end of the path\n segment == null ||\n // FieldPath does not support index access. We treat it as the end of the path.\n typeof segment === \"number\"\n ) {\n yield* stack.matches.map(({ label, path }) => ({ label, path, error }));\n break;\n }\n\n yield* stack.matches.filter(({ exact }) => exact).map(({ label, path }) => ({ label, path, error }));\n\n const next = stack.children[segment];\n if (!next) {\n break;\n }\n\n stack = next;\n }\n }\n}\n\nconst createErrorMaps = (errors: readonly GraphQLFormattedError[] | undefined, projectionPathGraph: ProjectionPathGraphNode) => {\n const errorMaps: { [label: string]: { [path: string]: { error: GraphQLFormattedError }[] } } = {};\n for (const { label, path, error } of generateErrorMapEntries(errors ?? [], projectionPathGraph)) {\n const mapPerLabel = errorMaps[label] || (errorMaps[label] = {});\n const mapPerPath = mapPerLabel[path] || (mapPerLabel[path] = []);\n mapPerPath.push({ error });\n }\n return errorMaps;\n};\n\nconst accessDataByPathSegments = (data: object, pathSegments: string[]) => {\n let current: unknown = data;\n\n for (const segment of pathSegments) {\n if (current == null) {\n return { error: new Error(\"No data\") };\n }\n\n if (typeof current !== \"object\") {\n return { error: new Error(\"Incorrect data type\") };\n }\n\n if (Array.isArray(current)) {\n return { error: new Error(\"Incorrect data type\") };\n }\n\n current = (current as Record<string, unknown>)[segment];\n }\n\n return { data: current };\n};\n\n/**\n * Creates an execution result parser for composed operations.\n * The parser maps GraphQL errors and data to their corresponding slices\n * based on the projection path graph.\n *\n * @param slices - Object mapping labels to projections\n * @returns A parser function that takes a NormalizedExecutionResult and returns parsed slices\n *\n * @example\n * ```typescript\n * const parser = createExecutionResultParser({\n * userCard: userCardProjection,\n * posts: postsProjection,\n * });\n *\n * const results = parser({\n * type: \"graphql\",\n * body: { data, errors },\n * });\n * ```\n */\nexport const createExecutionResultParser = <TSlices extends AnySlicePayloads>(slices: TSlices) => {\n // Build path graph from slices\n const projectionPathGraph = createPathGraphFromSlices(slices);\n const fragments = slices;\n const prepare = (result: NormalizedExecutionResult<object, object>) => {\n if (result.type === \"graphql\") {\n const errorMaps = createErrorMaps(result.body.errors, projectionPathGraph);\n\n return { ...result, errorMaps };\n }\n\n if (result.type === \"non-graphql-error\") {\n return { ...result, error: new SlicedExecutionResultError({ type: \"non-graphql-error\", error: result.error }) };\n }\n\n if (result.type === \"empty\") {\n return { ...result, error: new SlicedExecutionResultEmpty() };\n }\n\n throw new Error(\"Invalid result type\", { cause: result satisfies never });\n };\n\n return (result: NormalizedExecutionResult<object, object>) => {\n const prepared = prepare(result);\n\n const entries = Object.entries(fragments).map(([label, fragment]) => {\n const { projection } = fragment;\n\n if (prepared.type === \"graphql\") {\n const matchedErrors = projection.paths.flatMap(({ full: raw }) => prepared.errorMaps[label]?.[raw] ?? []);\n const uniqueErrors = Array.from(new Set(matchedErrors.map(({ error }) => error)).values());\n\n if (uniqueErrors.length > 0) {\n return [label, projection.projector(new SlicedExecutionResultError({ type: \"graphql-error\", errors: uniqueErrors }))];\n }\n\n // Apply label prefix to first segment for data access (matching $colocate prefix pattern)\n const dataResults = projection.paths.map(({ segments }) => {\n const [first, ...rest] = segments;\n const prefixedSegments = [`${label}_${first}`, ...rest];\n return prepared.body.data\n ? accessDataByPathSegments(prepared.body.data, prefixedSegments)\n : { error: new Error(\"No data\") };\n });\n if (dataResults.some(({ error }) => error)) {\n const errors = dataResults.flatMap(({ error }) => (error ? [error] : []));\n return [label, projection.projector(new SlicedExecutionResultError({ type: \"parse-error\", errors }))];\n }\n\n const dataList = dataResults.map(({ data }) => data);\n return [label, projection.projector(new SlicedExecutionResultSuccess(dataList))];\n }\n\n if (prepared.type === \"non-graphql-error\") {\n return [label, projection.projector(prepared.error)];\n }\n\n if (prepared.type === \"empty\") {\n return [label, projection.projector(prepared.error)];\n }\n\n throw new Error(\"Invalid result type\", { cause: prepared satisfies never });\n });\n\n return Object.fromEntries(entries);\n };\n};\n"],"mappings":"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"}