Skip to content

Commit 61b888a

Browse files
authored
add path scoped field transformers (#15)
1 parent 4b4775c commit 61b888a

16 files changed

+240
-1334
lines changed

.c8rc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"src/**/index.ts",
66
"src/jsutils/ObjMap.ts",
77
"src/jsutils/PromiseOrValue.ts",
8+
"src/jsutils/SimpleAsyncGenerator.ts",
89
"src/transform/PayloadPublisher.ts"
910
],
1011
"clean": true,

.changeset/new-bushes-roll.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@yaacovcr/transform': patch
3+
---
4+
5+
Remove path-scoped object extenders.
6+
7+
While workable for non-incremental requests, API for modifying existing deferred fragments would be quite complex.

README.md

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ pnpm add @yaacovCR/transform [email protected]
2525
This library offers:
2626

2727
- Configurable synchronous leaf value transformers, object field transformers, and path-scoped field transformers for modifying GraphQL results.
28-
- Configurable synchronous/asynchronous extenders of object values with additional GraphQL results.
2928
- Mapping from the latest incremental delivery format to the legacy format.
3029

3130
Note: In the case of multiple transformers, execution is in the above order: first any leaf value transformer is executed, followed by any object field transformer, and then the path-scoped field transformer.
@@ -124,45 +123,6 @@ const transformed = await transformResult(originalResult, {
124123
});
125124
```
126125

127-
### Configurable Path Scoped Object Extenders
128-
129-
Pass `pathScopedObjectBatchExtenders` in a `Transformers` object to `transformResult()`, keyed by a period-delimited path to the given field within the operation as well as the type of object. (If the field is of abstract type, the type may vary, and you can specify how to extend the object value for each possible type.)
130-
131-
```ts
132-
import { transformResult } from '@yaacovCR/transform';
133-
134-
const originalResult = await experimentalExecuteIncrementally({
135-
schema,
136-
document,
137-
rootValue,
138-
contextValue,
139-
variableValues,
140-
});
141-
142-
const transformed = await transformResult(originalResult, {
143-
pathScopedObjectBatchExtenders: {
144-
someType.someField: {
145-
SomeField: (objects) =>
146-
objects.map(() => ({
147-
data: { additionalField: 'additionalField' },
148-
})),
149-
},
150-
},
151-
});
152-
```
153-
154-
The signature is as follows:
155-
156-
```ts
157-
type PathScopedObjectBatchExtenders = ObjMap<ObjMap<ObjectBatchExtender>>;
158-
159-
export type ObjectBatchExtender = (
160-
objects: ReadonlyArray<ObjMap<unknown>>,
161-
type: GraphQLObjectType,
162-
fieldDetailsList: ReadonlyArray<FieldDetails>,
163-
) => PromiseOrValue<ReadonlyArray<ExecutionResult>>;
164-
```
165-
166126
### Legacy Incremental Delivery Format
167127

168128
Convert from the the latest incremental delivery format to the legacy format:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type SimpleAsyncGenerator<TSubsequent> = AsyncGenerator<
2+
TSubsequent,
3+
void,
4+
void
5+
>;

src/transform/IncrementalGraph.ts

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import type {
1515
StreamItem,
1616
SubsequentResultRecord,
1717
} from './types.js';
18-
import { isStream } from './types.js';
18+
import {
19+
isDeferredFragment,
20+
isPendingExecutionGroup,
21+
isStream,
22+
} from './types.js';
1923

2024
/**
2125
* @internal
@@ -46,9 +50,11 @@ export class IncrementalGraph {
4650
}
4751

4852
getNewRootNodes(
53+
deferredFragments: ReadonlyArray<DeferredFragment>,
4954
incrementalDataRecords: ReadonlyArray<IncrementalDataRecord>,
5055
): ReadonlyArray<SubsequentResultRecord> {
5156
const initialResultChildren = new Set<SubsequentResultRecord>();
57+
this._addDeferredFragments(deferredFragments, initialResultChildren);
5258
this._addIncrementalDataRecords(
5359
incrementalDataRecords,
5460
undefined,
@@ -89,13 +95,16 @@ export class IncrementalGraph {
8995
}
9096
}
9197

98+
// TODO: add test case for async transformation
99+
/* c8 ignore start */
92100
nextCompletedBatch(): Promise<Iterable<IncrementalGraphEvent> | undefined> {
93101
const { promise, resolve } = promiseWithResolvers<
94102
Iterable<IncrementalGraphEvent> | undefined
95103
>();
96104
this._nextQueue.push(resolve);
97105
return promise;
98106
}
107+
/* c8 ignore stop */
99108

100109
hasPending(): boolean {
101110
return this._pending.size > 0;
@@ -184,13 +193,22 @@ export class IncrementalGraph {
184193
}
185194
}
186195

196+
private _addDeferredFragments(
197+
deferredFragments: ReadonlyArray<DeferredFragment>,
198+
initialResultChildren?: Set<SubsequentResultRecord>,
199+
): void {
200+
for (const deferredFragment of deferredFragments) {
201+
this._addDeferredFragment(deferredFragment, initialResultChildren);
202+
}
203+
}
204+
187205
private _addIncrementalDataRecords(
188206
incrementalDataRecords: ReadonlyArray<IncrementalDataRecord>,
189207
parents: ReadonlyArray<DeferredFragment> | undefined,
190208
initialResultChildren?: Set<SubsequentResultRecord>,
191209
): void {
192210
for (const incrementalDataRecord of incrementalDataRecords) {
193-
if (!isStream(incrementalDataRecord)) {
211+
if (isPendingExecutionGroup(incrementalDataRecord)) {
194212
let ready = false;
195213
for (const deferredFragment of incrementalDataRecord.deferredFragments) {
196214
this._addDeferredFragment(deferredFragment, initialResultChildren);
@@ -202,13 +220,15 @@ export class IncrementalGraph {
202220
if (ready && this._completesRootNode(incrementalDataRecord)) {
203221
this._onExecutionGroup(incrementalDataRecord);
204222
}
205-
} else if (parents === undefined) {
206-
invariant(initialResultChildren !== undefined);
207-
initialResultChildren.add(incrementalDataRecord);
208-
} else {
209-
for (const parent of parents) {
210-
this._addDeferredFragment(parent, initialResultChildren);
211-
parent.children.add(incrementalDataRecord);
223+
} else if (isStream(incrementalDataRecord)) {
224+
if (parents === undefined) {
225+
invariant(initialResultChildren !== undefined);
226+
initialResultChildren.add(incrementalDataRecord);
227+
} else {
228+
for (const parent of parents) {
229+
this._addDeferredFragment(parent, initialResultChildren);
230+
parent.children.add(incrementalDataRecord);
231+
}
212232
}
213233
}
214234
}
@@ -219,7 +239,7 @@ export class IncrementalGraph {
219239
): ReadonlyArray<SubsequentResultRecord> {
220240
const newRootNodes: Array<SubsequentResultRecord> = [];
221241
for (const node of maybeEmptyNewRootNodes) {
222-
if (!isStream(node)) {
242+
if (isDeferredFragment(node)) {
223243
if (node.pendingExecutionGroups.size > 0) {
224244
// TODO: add test cases for executor returning results early
225245
/* c8 ignore start */
@@ -282,7 +302,6 @@ export class IncrementalGraph {
282302
return;
283303
}
284304
parent.children.add(deferredFragment);
285-
this._addDeferredFragment(parent, initialResultChildren);
286305
}
287306

288307
private _onExecutionGroup(
@@ -295,16 +314,21 @@ export class IncrementalGraph {
295314

296315
pendingExecutionGroup.result = result = result();
297316
const value = result.value;
317+
318+
// TODO: add test case for async transformation
319+
/* c8 ignore start */
298320
if (isPromise(value)) {
299321
this._pending.add(pendingExecutionGroup);
300322
// eslint-disable-next-line @typescript-eslint/no-floating-promises
301323
value.then((resolved) => {
302324
this._pending.delete(pendingExecutionGroup);
303325
this._enqueue(resolved);
304326
});
305-
} else {
306-
this._enqueue(value);
327+
return;
307328
}
329+
/* c8 ignore stop */
330+
331+
this._enqueue(value);
308332
}
309333

310334
private async _onStreamItems(stream: Stream): Promise<void> {
@@ -315,6 +339,7 @@ export class IncrementalGraph {
315339

316340
let items: Array<unknown> = [];
317341
let errors: Array<GraphQLError> = [];
342+
const deferredFragments: Array<DeferredFragment> = [];
318343
let incrementalDataRecords: Array<IncrementalDataRecord> = [];
319344

320345
const streamItemQueue = stream.streamItemQueue;
@@ -325,12 +350,20 @@ export class IncrementalGraph {
325350
? streamItem.result()
326351
: streamItem.result;
327352
if (!(result instanceof BoxedPromiseOrValue)) {
353+
// TODO: add test case for async transformation
354+
/* c8 ignore start */
328355
if (items.length > 0) {
329356
this._enqueue({
330357
stream,
331-
result: { items, errors, incrementalDataRecords },
358+
result: {
359+
items,
360+
errors,
361+
deferredFragments,
362+
incrementalDataRecords,
363+
},
332364
});
333365
}
366+
/* c8 ignore stop */
334367
this._enqueue(
335368
result === null
336369
? { stream }
@@ -343,6 +376,8 @@ export class IncrementalGraph {
343376
}
344377

345378
let value = result.value;
379+
// TODO: add test case for async transformation
380+
/* c8 ignore start */
346381
if (isPromise(value)) {
347382
this._pending.add(stream);
348383
if (items.length > 0) {
@@ -351,6 +386,7 @@ export class IncrementalGraph {
351386
result: {
352387
items,
353388
errors,
389+
deferredFragments,
354390
incrementalDataRecords,
355391
},
356392
});
@@ -360,12 +396,13 @@ export class IncrementalGraph {
360396
}
361397
// eslint-disable-next-line no-await-in-loop
362398
value = await value;
363-
this._pending.delete(stream);
364399
// wait an additional tick to coalesce resolving additional promises
365400
// within the queue
366401
// eslint-disable-next-line no-await-in-loop
367402
await Promise.resolve();
403+
this._pending.delete(stream);
368404
}
405+
/* c8 ignore stop */
369406
const item = value.item;
370407
if (item === undefined) {
371408
// TODO: add test case for failure via transformation with existing items
@@ -376,6 +413,7 @@ export class IncrementalGraph {
376413
result: {
377414
items,
378415
errors,
416+
deferredFragments,
379417
incrementalDataRecords,
380418
},
381419
});
@@ -389,13 +427,14 @@ export class IncrementalGraph {
389427
}
390428
items.push(item);
391429
errors.push(...value.errors);
430+
deferredFragments.push(...value.deferredFragments);
392431
incrementalDataRecords.push(...value.incrementalDataRecords);
393432
}
394433

395434
if (items.length > 0) {
396435
this._enqueue({
397436
stream,
398-
result: { items, errors, incrementalDataRecords },
437+
result: { items, errors, deferredFragments, incrementalDataRecords },
399438
});
400439
}
401440

@@ -407,7 +446,8 @@ export class IncrementalGraph {
407446
const next = this._nextQueue.shift();
408447
if (next === undefined) {
409448
return;
410-
}
449+
} /* c8 ignore start */
450+
// TODO: add test case for async transformation
411451
next(this.currentCompletedBatch());
412-
}
452+
} /* c8 ignore stop */
413453
}

0 commit comments

Comments
 (0)