Skip to content

Commit 7ee9fbf

Browse files
committed
[WIP] Sketch of derived context
Summary: A sketch of derived contexts as described in #159 Not sure this is how the implemenetaiton should work. Was just focusing on getting things working end to end. If we go this way, I'd want to focus a bit more on internal architecture as well as error handling. Test Plan: ghstack-source-id: fee290a Pull Request resolved: #161
1 parent 831703d commit 7ee9fbf

File tree

10 files changed

+219
-41
lines changed

10 files changed

+219
-41
lines changed

src/Extractor.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export type ExtractionSnapshot = {
8282
readonly definitions: DefinitionNode[];
8383
readonly unresolvedNames: Map<ts.EntityName, NameNode>;
8484
readonly nameDefinitions: Map<ts.DeclarationStatement, NameDefinition>;
85+
readonly implicitNameDefinitions: Map<NameDefinition, ts.TypeReferenceNode>;
8586
readonly typesWithTypename: Set<string>;
8687
readonly interfaceDeclarations: Array<ts.InterfaceDeclaration>;
8788
};
@@ -113,6 +114,8 @@ class Extractor {
113114
// Snapshot data
114115
unresolvedNames: Map<ts.EntityName, NameNode> = new Map();
115116
nameDefinitions: Map<ts.DeclarationStatement, NameDefinition> = new Map();
117+
implicitNameDefinitions: Map<NameDefinition, ts.TypeReferenceNode> =
118+
new Map();
116119
typesWithTypename: Set<string> = new Set();
117120
interfaceDeclarations: Array<ts.InterfaceDeclaration> = [];
118121

@@ -132,6 +135,7 @@ class Extractor {
132135
name: NameNode,
133136
kind: NameDefinition["kind"],
134137
): void {
138+
// @ts-ignore FIXME
135139
this.nameDefinitions.set(node, { name, kind });
136140
}
137141

@@ -218,8 +222,12 @@ class Extractor {
218222
if (!ts.isDeclarationStatement(node)) {
219223
this.report(tag, E.contextTagOnNonDeclaration());
220224
} else {
221-
const name = this.gql.name(tag, "CONTEXT_DUMMY_NAME");
222-
this.recordTypeName(node, name, "CONTEXT");
225+
if (ts.isFunctionDeclaration(node)) {
226+
this.recordDerivedContext(node, tag);
227+
} else {
228+
const name = this.gql.name(tag, "CONTEXT_DUMMY_NAME");
229+
this.recordTypeName(node, name, "CONTEXT");
230+
}
223231
}
224232
break;
225233
}
@@ -293,11 +301,41 @@ class Extractor {
293301
definitions: this.definitions,
294302
unresolvedNames: this.unresolvedNames,
295303
nameDefinitions: this.nameDefinitions,
304+
implicitNameDefinitions: this.implicitNameDefinitions,
296305
typesWithTypename: this.typesWithTypename,
297306
interfaceDeclarations: this.interfaceDeclarations,
298307
});
299308
}
300309

310+
recordDerivedContext(node: ts.FunctionDeclaration, tag: ts.JSDocTag) {
311+
const returnType = node.type;
312+
if (returnType == null) {
313+
throw new Error("Function declaration must have a return type");
314+
}
315+
if (!ts.isTypeReferenceNode(returnType)) {
316+
throw new Error("Function declaration must return an explicit type");
317+
}
318+
319+
const funcName = this.namedFunctionExportName(node);
320+
321+
if (!ts.isSourceFile(node.parent)) {
322+
return this.report(node, E.functionFieldNotTopLevel());
323+
}
324+
325+
const tsModulePath = relativePath(node.getSourceFile().fileName);
326+
327+
const name = this.gql.name(tag, "CONTEXT_DUMMY_NAME");
328+
this.implicitNameDefinitions.set(
329+
{
330+
kind: "DERIVED_CONTEXT",
331+
name,
332+
path: tsModulePath,
333+
exportName: funcName?.text ?? null,
334+
},
335+
returnType,
336+
);
337+
}
338+
301339
extractType(node: ts.Node, tag: ts.JSDocTag) {
302340
if (ts.isClassDeclaration(node)) {
303341
this.typeClassDeclaration(node, tag);

src/TypeContext.ts

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,25 @@ import { ExtractionSnapshot } from "./Extractor";
1818

1919
export const UNRESOLVED_REFERENCE_NAME = `__UNRESOLVED_REFERENCE__`;
2020

21-
export type NameDefinition = {
22-
name: NameNode;
23-
kind:
24-
| "TYPE"
25-
| "INTERFACE"
26-
| "UNION"
27-
| "SCALAR"
28-
| "INPUT_OBJECT"
29-
| "ENUM"
30-
| "CONTEXT"
31-
| "INFO";
32-
};
21+
export type NameDefinition =
22+
| {
23+
name: NameNode;
24+
kind:
25+
| "TYPE"
26+
| "INTERFACE"
27+
| "UNION"
28+
| "SCALAR"
29+
| "INPUT_OBJECT"
30+
| "ENUM"
31+
| "CONTEXT"
32+
| "INFO";
33+
}
34+
| {
35+
name: NameNode;
36+
path: string;
37+
exportName: string | null;
38+
kind: "DERIVED_CONTEXT";
39+
};
3340

3441
type TsIdentifier = number;
3542

@@ -61,7 +68,16 @@ export class TypeContext {
6168
self._markUnresolvedType(node, typeName);
6269
}
6370
for (const [node, definition] of snapshot.nameDefinitions) {
64-
self._recordTypeName(node, definition.name, definition.kind);
71+
self._recordTypeName(node, definition);
72+
}
73+
for (const [definition, reference] of snapshot.implicitNameDefinitions) {
74+
const declaration = self.maybeTsDeclarationForTsName(reference.typeName);
75+
if (declaration == null) {
76+
throw new Error(
77+
"Expected to find declaration for implicit name definition.",
78+
);
79+
}
80+
self._recordTypeName(declaration, definition);
6581
}
6682
return self;
6783
}
@@ -72,13 +88,9 @@ export class TypeContext {
7288

7389
// Record that a GraphQL construct of type `kind` with the name `name` is
7490
// declared at `node`.
75-
private _recordTypeName(
76-
node: ts.Declaration,
77-
name: NameNode,
78-
kind: NameDefinition["kind"],
79-
) {
80-
this._idToDeclaration.set(name.tsIdentifier, node);
81-
this._declarationToName.set(node, { name, kind });
91+
private _recordTypeName(node: ts.Declaration, definition: NameDefinition) {
92+
this._idToDeclaration.set(definition.name.tsIdentifier, node);
93+
this._declarationToName.set(node, definition);
8294
}
8395

8496
// Record that a type references `node`

src/codegen/resolverCodegen.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,16 @@ export default class ResolverCodegen {
178178
F.createIdentifier("args"),
179179
F.createIdentifier(arg.name),
180180
);
181+
case "derivedContext": {
182+
const localName = "derivedContext";
183+
this.ts.importUserConstruct(arg.path, arg.exportName, localName);
184+
return F.createCallExpression(
185+
F.createIdentifier(localName),
186+
undefined,
187+
[this.resolverParam(arg.input)],
188+
);
189+
}
190+
181191
default:
182192
// @ts-expect-error
183193
throw new Error(`Unexpected resolver kind ${arg.kind}`);

src/lib.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ function combineSnapshots(snapshots: ExtractionSnapshot[]): ExtractionSnapshot {
173173
const result: ExtractionSnapshot = {
174174
definitions: [],
175175
nameDefinitions: new Map(),
176+
implicitNameDefinitions: new Map(),
176177
unresolvedNames: new Map(),
177178
typesWithTypename: new Set(),
178179
interfaceDeclarations: [],
@@ -191,6 +192,10 @@ function combineSnapshots(snapshots: ExtractionSnapshot[]): ExtractionSnapshot {
191192
result.unresolvedNames.set(node, typeName);
192193
}
193194

195+
for (const [node, definition] of snapshot.implicitNameDefinitions) {
196+
result.implicitNameDefinitions.set(node, definition);
197+
}
198+
194199
for (const typeName of snapshot.typesWithTypename) {
195200
result.typesWithTypename.add(typeName);
196201
}

src/metadata.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export type ResolverArgument =
8787
| SourceArgument
8888
| ArgumentsObjectArgument
8989
| ContextArgument
90+
| DerivedContextArgument
9091
| InformationArgument
9192
| NamedArgument;
9293

@@ -105,6 +106,15 @@ export type ContextArgument = {
105106
kind: "context";
106107
};
107108

109+
/** A context value which is expressed as a function of the global context */
110+
export type DerivedContextArgument = {
111+
kind: "derivedContext";
112+
path: string; // Path to the module
113+
exportName: string | null; // Export name. If omitted, the class is the default export
114+
input: ContextArgument | DerivedContextArgument;
115+
// TODO: Add a parent which could be ContextArgument or another DerivedContextArgument
116+
};
117+
108118
/** The GraphQL info object */
109119
export type InformationArgument = {
110120
kind: "information";

src/resolverSignature.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ export type ContextResolverArgument = {
6060
node: ts.Node;
6161
};
6262

63+
export type DerivedContextResolverArgument = {
64+
kind: "derivedContext";
65+
path: string;
66+
exportName: string | null;
67+
// TODO: Support custom inputs
68+
node: ts.Node;
69+
};
70+
6371
export type InformationResolverArgument = {
6472
kind: "information";
6573
node: ts.Node;
@@ -82,6 +90,7 @@ export type ResolverArgument =
8290
| SourceResolverArgument
8391
| ArgumentsObjectResolverArgument
8492
| ContextResolverArgument
93+
| DerivedContextResolverArgument
8594
| InformationResolverArgument
8695
| NamedResolverArgument
8796
| UnresolvedResolverArgument;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/** @gqlContext */
2+
type RootContext = {
3+
userName: string;
4+
};
5+
6+
type DerivedContext = {
7+
greeting: string;
8+
};
9+
10+
/** @gqlContext */
11+
export function createDerivedContext(ctx: RootContext): DerivedContext {
12+
return { greeting: `Hello, ${ctx.userName}!` };
13+
}
14+
15+
/** @gqlType */
16+
type Query = unknown;
17+
18+
/** @gqlField */
19+
export function greeting(_: Query, ctx: DerivedContext): string {
20+
return ctx.greeting;
21+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
-----------------
2+
INPUT
3+
-----------------
4+
/** @gqlContext */
5+
type RootContext = {
6+
userName: string;
7+
};
8+
9+
type DerivedContext = {
10+
greeting: string;
11+
};
12+
13+
/** @gqlContext */
14+
export function createDerivedContext(ctx: RootContext): DerivedContext {
15+
return { greeting: `Hello, ${ctx.userName}!` };
16+
}
17+
18+
/** @gqlType */
19+
type Query = unknown;
20+
21+
/** @gqlField */
22+
export function greeting(_: Query, ctx: DerivedContext): string {
23+
return ctx.greeting;
24+
}
25+
26+
-----------------
27+
OUTPUT
28+
-----------------
29+
-- SDL --
30+
type Query {
31+
greeting: String
32+
}
33+
-- TypeScript --
34+
import { greeting as queryGreetingResolver, createDerivedContext as derivedContext } from "./simpleDerivedContext";
35+
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql";
36+
export function getSchema(): GraphQLSchema {
37+
const QueryType: GraphQLObjectType = new GraphQLObjectType({
38+
name: "Query",
39+
fields() {
40+
return {
41+
greeting: {
42+
name: "greeting",
43+
type: GraphQLString,
44+
resolve(source) {
45+
return queryGreetingResolver(source, derivedContext(context));
46+
}
47+
}
48+
};
49+
}
50+
});
51+
return new GraphQLSchema({
52+
query: QueryType,
53+
types: [QueryType]
54+
});
55+
}

src/transforms/makeResolverSignature.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -78,23 +78,33 @@ function transformArgs(
7878
if (args == null) {
7979
return null;
8080
}
81-
return args.map((arg): ResolverArgument => {
82-
switch (arg.kind) {
83-
case "argumentsObject":
84-
return { kind: "argumentsObject" };
85-
case "named":
86-
return { kind: "named", name: arg.name };
87-
case "source":
88-
return { kind: "source" };
89-
case "information":
90-
return { kind: "information" };
91-
case "context":
92-
return { kind: "context" };
93-
case "unresolved":
94-
throw new Error("Unresolved argument in resolver");
95-
default:
96-
// @ts-expect-error
97-
throw new Error(`Unknown argument kind: ${arg.kind}`);
98-
}
99-
});
81+
return args.map(transformArg);
82+
}
83+
84+
function transformArg(arg: DirectiveResolverArgument): ResolverArgument {
85+
switch (arg.kind) {
86+
case "argumentsObject":
87+
return { kind: "argumentsObject" };
88+
case "named":
89+
return { kind: "named", name: arg.name };
90+
case "source":
91+
return { kind: "source" };
92+
case "information":
93+
return { kind: "information" };
94+
case "context":
95+
return { kind: "context" };
96+
case "derivedContext":
97+
return {
98+
kind: "derivedContext",
99+
path: arg.path,
100+
exportName: arg.exportName,
101+
// TODO: Support custom inputs
102+
input: { kind: "context" },
103+
};
104+
case "unresolved":
105+
throw new Error("Unresolved argument in resolver");
106+
default:
107+
// @ts-expect-error
108+
throw new Error(`Unknown argument kind: ${arg.kind}`);
109+
}
100110
}

src/transforms/resolveResolverParams.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ class ResolverParamsResolver {
108108
case "argumentsObject":
109109
case "information":
110110
case "context":
111+
case "derivedContext":
111112
case "source":
112113
return param;
113114
case "unresolved": {
@@ -124,6 +125,13 @@ class ResolverParamsResolver {
124125
return param;
125126
}
126127
switch (resolved.value.kind) {
128+
case "DERIVED_CONTEXT":
129+
return {
130+
kind: "derivedContext",
131+
node: param.node,
132+
path: resolved.value.path,
133+
exportName: resolved.value.exportName,
134+
};
127135
case "CONTEXT":
128136
return { kind: "context", node: param.node };
129137
case "INFO":

0 commit comments

Comments
 (0)