Skip to content

Commit a359146

Browse files
authored
Update transform API to operate on top of a stitching scaffolding (#17)
1 parent 61b888a commit a359146

30 files changed

+2045
-1080
lines changed

.changeset/cute-cougars-drop.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+
Add initial functionality for composition of subschemas, i.e. stitching.
6+
7+
This functionality will supplant object extension.

.changeset/happy-pens-walk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@yaacovcr/transform': patch
3+
---
4+
5+
Change main export to `transform` from `transformResult`.

.mocharc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ extension:
55
- ts
66
node-option:
77
- 'loader=ts-node/esm/transpile-only'
8+
- 'enable-source-maps'

README.md

Lines changed: 74 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,31 +22,60 @@ pnpm add @yaacovCR/transform [email protected]
2222

2323
## Overview
2424

25-
This library offers:
25+
This library, primarily through its `transform()` export, provides several helpful functionalities for working with GraphQL results:
2626

27-
- Configurable synchronous leaf value transformers, object field transformers, and path-scoped field transformers for modifying GraphQL results.
28-
- Mapping from the latest incremental delivery format to the legacy format.
27+
- **Result Composition:** Combine GraphQL results, including incremental delivery payloads (`@defer`/`@stream`), from multiple downstream GraphQL services into a single unified result. (Note: Composition is currently under development and primarily functional for root fields.)
28+
- **Result Transformation:** Modify results using configurable transformers based on the leaf type, object field, or specific location within a GraphQL operation.
29+
- **Legacy Incremental Format Mapping:** Convert results from the modern incremental delivery specification to the legacy format used in earlier `graphql-js` versions.
2930

30-
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.
31+
Notes:
32+
33+
- When multiple transformer types are applied to the same field, they execute in the following order: leaf value transformer -> object field transformer -> path-scoped field transformer.
34+
35+
- A separate `legacyExecuteIncrementally()` export is also available. It leverages the same underlying transformation logic but is specifically designed to provide backwards compatibility for the legacy incremental delivery format when interacting with a single GraphQL service (i.e., without composition).
3136

3237
## Usage
3338

34-
### Configurable Leaf Transformers
39+
### Composition of GraphQL Results
3540

36-
Pass `leafTransformers` in a `Transformers` object to `transformResult()`.
41+
To compose results from multiple GraphQL services, provide a `subschemas` array to `transform()`:
3742

3843
```ts
39-
import { transformResult } from '@yaacovCR/transform';
44+
import { transform } from '@yaacovCR/transform';
4045

41-
const originalResult = await experimentalExecuteIncrementally({
46+
const transformed = await transform({
4247
schema,
4348
document,
44-
rootValue,
45-
contextValue,
4649
variableValues,
50+
subschemas: [
51+
{
52+
label: 'Subschema1',
53+
subschema: new GraphQLSchema({ ... }),
54+
executor: myExecutor1,
55+
},
56+
{
57+
label: 'Subschema2',
58+
subschema: new GraphQLSchema({ ... }),
59+
executor: myExecutor1,
60+
},
61+
],
4762
});
63+
```
64+
65+
### Transformation of GraphQL Results
66+
67+
## Configurable Leaf Transformers
4868

49-
const transformed = await transformResult(originalResult, {
69+
To apply transformations to specific leaf types (scalars or enums), pass `leafTransformers` to `transform()`:
70+
71+
```ts
72+
import { transform } from '@yaacovCR/transform';
73+
74+
const transformed = await transform({
75+
schema,
76+
document,
77+
variableValues,
78+
subschemas,
5079
leafTransformers: {
5180
Color: (value) => (value === 'GREEN' ? 'DARK_GREEN' : value),
5281
},
@@ -65,22 +94,18 @@ type LeafTransformer = (
6594
) => unknown;
6695
```
6796

68-
### Configurable Object Field Transformers
97+
## Configurable Object Field Transformers
6998

70-
Pass `objectFieldTransformers` in a `Transformers` object to `transformResult()`, namespaced by type and field.
99+
To apply transformations to specific fields within object types, pass `objectFieldTransformers` to `transform()`, namespaced by type name and field name:
71100

72101
```ts
73-
import { transformResult } from '@yaacovCR/transform';
102+
import { transform } from '@yaacovCR/transform';
74103

75-
const originalResult = await experimentalExecuteIncrementally({
104+
const transformed = await transform({
76105
schema,
77106
document,
78-
rootValue,
79-
contextValue,
80107
variableValues,
81-
});
82-
83-
const transformed = await transformResult(originalResult, {
108+
subschemas,
84109
objectFieldTransformers: {
85110
SomeType: {
86111
someField: (value) => 'transformed',
@@ -101,22 +126,18 @@ type FieldTransformer = (
101126
) => unknown;
102127
```
103128

104-
### Configurable Path Scoped Field Transformers
129+
## Configurable Path-Scoped Field Transformers
105130

106-
Pass `pathScopedFieldTransformers` in a `Transformers` object to `transformResult()`, keyed by a period-delimited path to the given field within the operation. (Numeric indices for list fields are simply skipped, reflecting the path within the given operation rather than the result.)
131+
To apply transformations based on a field's specific location within the operation, pass `pathScopedFieldTransformers` to `transform()`. The keys are period-delimited paths reflecting the field structure in the operation (numeric list indices are omitted).
107132

108133
```ts
109-
import { transformResult } from '@yaacovCR/transform';
134+
import { transform } from '@yaacovCR/transform';
110135

111-
const originalResult = await experimentalExecuteIncrementally({
136+
const transformed = await transform({
112137
schema,
113138
document,
114-
rootValue,
115-
contextValue,
116139
variableValues,
117-
});
118-
119-
const transformed = await transformResult(originalResult, {
140+
subschemas,
120141
objectFieldTransformers: {
121142
'someType.someFieldNameOrAlias': (value) => 'transformed',
122143
},
@@ -125,17 +146,39 @@ const transformed = await transformResult(originalResult, {
125146

126147
### Legacy Incremental Delivery Format
127148

128-
Convert from the the latest incremental delivery format to the legacy format:
149+
If you need to interact with systems expecting the older incremental delivery format (from `graphql-js` pre-v17), you can enable conversion:
150+
151+
```ts
152+
import { transform } from '@yaacovCR/transform';
153+
154+
const result = await transform({
155+
schema,
156+
document,
157+
variableValues,
158+
subschemas,
159+
legacyIncremental: true,
160+
});
161+
```
162+
163+
Alternatively, if you are working with a single GraphQL service (no composition) and need the legacy format, the `legacyExecuteIncrementally()` function provides a dedicated interface:
129164

130165
```ts
131166
import { legacyExecuteIncrementally } from '@yaacovCR/transform';
132167

133168
const result = await legacyExecuteIncrementally({
134169
schema,
135170
document,
171+
variableValues,
172+
operationName,
136173
rootValue,
137174
contextValue,
138-
variableValues,
175+
fieldResolver,
176+
typeResolver,
177+
subscribeFieldResolver,
178+
perEventExecutor,
179+
enableEarlyExecution,
180+
hideSuggestions,
181+
abortSignal,
139182
});
140183
```
141184

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66

77
export { buildTransformationContext } from './transform/buildTransformationContext.js';
88
export { legacyExecuteIncrementally } from './transform/legacyExecuteIncrementally.js';
9-
export { transformResult } from './transform/transformResult.js';
9+
export { transform } from './transform/transform.js';
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { promiseWithResolvers } from './promiseWithResolvers.js';
2+
import type { SimpleAsyncGenerator } from './SimpleAsyncGenerator.js';
3+
4+
interface WaitingConsumer<T> {
5+
resolve: (result: IteratorResult<T>) => void;
6+
reject: (error?: unknown) => void;
7+
}
8+
9+
type GeneratorResult<T> =
10+
| {
11+
iteratorResult: IteratorResult<T>;
12+
error?: never;
13+
}
14+
| {
15+
iteratorResult?: never;
16+
error: unknown;
17+
};
18+
19+
/**
20+
* @internal
21+
*/
22+
export class AsyncGeneratorCombinator<T> implements SimpleAsyncGenerator<T> {
23+
private _generators: Set<SimpleAsyncGenerator<T>>;
24+
private _availableGenerators: Set<SimpleAsyncGenerator<T>>;
25+
private _waitingConsumers: Array<WaitingConsumer<T>>;
26+
private _availableResults: Array<GeneratorResult<T>>;
27+
private _isDone: boolean;
28+
29+
constructor() {
30+
this._generators = new Set();
31+
this._availableGenerators = new Set();
32+
this._waitingConsumers = [];
33+
this._availableResults = [];
34+
this._isDone = false;
35+
}
36+
37+
add(generator: SimpleAsyncGenerator<T>): void {
38+
this._generators.add(generator);
39+
this._availableGenerators.add(generator);
40+
if (this._waitingConsumers.length > 0) {
41+
this._pollGenerator(generator);
42+
}
43+
}
44+
45+
async next(): Promise<IteratorResult<T>> {
46+
if (this._isDone) {
47+
return { value: undefined, done: true };
48+
}
49+
50+
const queuedResult = this._availableResults.shift();
51+
if (queuedResult) {
52+
const iteratorResult = queuedResult.iteratorResult;
53+
if (iteratorResult) {
54+
return iteratorResult;
55+
}
56+
this._isDone = true;
57+
throw queuedResult.error;
58+
}
59+
60+
const { promise, resolve, reject } =
61+
promiseWithResolvers<IteratorResult<T>>();
62+
63+
this._waitingConsumers.push({ resolve, reject });
64+
65+
this._pollAvailableGenerators();
66+
67+
return promise;
68+
}
69+
70+
async return(): Promise<IteratorResult<T>> {
71+
this._flush();
72+
await Promise.all(
73+
Array.from(this._generators).map((generator) => generator.return()),
74+
);
75+
return { value: undefined, done: true };
76+
}
77+
78+
async throw(error?: unknown): Promise<IteratorResult<T>> {
79+
this._flush();
80+
await Promise.all(
81+
Array.from(this._generators).map((generator) => generator.throw(error)),
82+
);
83+
throw error; /* c8 ignore start */
84+
} /* c8 ignore stop */
85+
86+
[Symbol.asyncIterator](): this {
87+
return this;
88+
}
89+
90+
private _pollAvailableGenerators(): void {
91+
for (const generator of this._availableGenerators) {
92+
this._pollGenerator(generator);
93+
}
94+
}
95+
96+
private _pollGenerator(generator: SimpleAsyncGenerator<T>): void {
97+
this._availableGenerators.delete(generator);
98+
generator.next().then(
99+
(iteratorResult) => this._onIteratorResult(generator, iteratorResult),
100+
(error: unknown) => this._onError(error),
101+
);
102+
}
103+
104+
private _onIteratorResult(
105+
generator: SimpleAsyncGenerator<T>,
106+
iteratorResult: IteratorResult<T>,
107+
): void {
108+
if (iteratorResult.done) {
109+
this._generators.delete(generator);
110+
if (this._generators.size === 0) {
111+
this._flush();
112+
}
113+
} else if (!this._isDone) {
114+
this._availableGenerators.add(generator);
115+
116+
const waitingConsumer = this._waitingConsumers.shift();
117+
if (waitingConsumer) {
118+
waitingConsumer.resolve(iteratorResult);
119+
if (this._waitingConsumers.length > 0) {
120+
this._pollAvailableGenerators();
121+
}
122+
} else {
123+
this._availableResults.push({ iteratorResult });
124+
}
125+
}
126+
}
127+
128+
private _onError(error: unknown): void {
129+
const waitingConsumer = this._waitingConsumers.shift();
130+
if (waitingConsumer) {
131+
waitingConsumer.reject(error);
132+
this._flush();
133+
} else {
134+
this._availableResults.push({ error });
135+
}
136+
}
137+
138+
private _flush(): void {
139+
this._isDone = true;
140+
this._waitingConsumers.forEach(({ resolve }) =>
141+
resolve({ value: undefined, done: true }),
142+
);
143+
this._waitingConsumers = [];
144+
}
145+
}

0 commit comments

Comments
 (0)