@tinkerstack/graphql-annotation 0.0.4 → 0.0.7
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 +138 -138
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/nest-cli.json +8 -8
- package/package.json +7 -2
- package/src/example/consumer/consumer.app.ts +38 -38
- package/src/example/consumer/consumer.example.ts +57 -57
- package/src/example/main.ts +23 -23
- package/src/example/producer/producer.app.ts +22 -22
- package/src/example/producer/producer.example.ts +28 -28
- package/src/modules/core/annotation.constants.ts +37 -37
- package/src/modules/core/annotation.decorators.ts +161 -161
- package/src/modules/core/annotation.loader.ts +318 -315
- package/src/modules/core/annotation.module.ts +47 -47
- package/src/modules/core/annotation.resolver.ts +87 -87
- package/src/modules/core/annotation.utils.ts +48 -48
- package/src/modules/core/index.ts +4 -4
- package/src/modules/core/resolver.explorer.ts +31 -31
- package/src/modules/index.ts +1 -1
- package/tsconfig.build.json +5 -5
- package/tsconfig.json +30 -30
- package/tsup.config.ts +10 -10
- package/.turbo/turbo-build.log +0 -20
|
@@ -1,315 +1,318 @@
|
|
|
1
|
-
import { stitchSchemas } from "@graphql-tools/stitch";
|
|
2
|
-
import { MapperKind, mapSchema } from "@graphql-tools/utils";
|
|
3
|
-
import { FilterRootFields, wrapSchema } from "@graphql-tools/wrap";
|
|
4
|
-
import { Injectable, Logger, ParamData } from "@nestjs/common";
|
|
5
|
-
import {
|
|
6
|
-
ExternalContextCreator,
|
|
7
|
-
MetadataScanner,
|
|
8
|
-
ParamsFactory,
|
|
9
|
-
} from "@nestjs/core";
|
|
10
|
-
import { ParamMetadata } from "@nestjs/core/helpers/interfaces/params-metadata.interface";
|
|
11
|
-
import {
|
|
12
|
-
GqlContextType,
|
|
13
|
-
GraphQLExecutionContext,
|
|
14
|
-
PARAM_ARGS_METADATA,
|
|
15
|
-
} from "@nestjs/graphql";
|
|
16
|
-
import { GqlParamtype } from "@nestjs/graphql/dist/enums/gql-paramtype.enum";
|
|
17
|
-
import { GraphQLResolveInfo, GraphQLSchema } from "graphql";
|
|
18
|
-
import { GraphQLClient } from "graphql-request";
|
|
19
|
-
import {
|
|
20
|
-
ANNOTATION_RESOLVER_METADATA,
|
|
21
|
-
AnnotationResolverMetadata,
|
|
22
|
-
AnnotationSchemaSources,
|
|
23
|
-
ResolverName,
|
|
24
|
-
} from "./annotation.constants";
|
|
25
|
-
import {
|
|
26
|
-
ANNOTATION_QUERY_NAME,
|
|
27
|
-
GraphQLAnnotationMeta,
|
|
28
|
-
GraphQLAnnotationResolver,
|
|
29
|
-
} from "./annotation.resolver";
|
|
30
|
-
import {
|
|
31
|
-
isPlainObject,
|
|
32
|
-
loadGraphQLSchema,
|
|
33
|
-
shallowMerge,
|
|
34
|
-
split,
|
|
35
|
-
} from "./annotation.utils";
|
|
36
|
-
import { ResolverDiscoveryService } from "./resolver.explorer";
|
|
37
|
-
import { instanceToPlain } from "class-transformer";
|
|
38
|
-
|
|
39
|
-
@Injectable()
|
|
40
|
-
export class GraphQLAnnotatedSchemaLoader {
|
|
41
|
-
private _logger = new Logger("GraphQLAnnotationModule");
|
|
42
|
-
|
|
43
|
-
constructor(
|
|
44
|
-
private readonly resolverDiscoveryService: ResolverDiscoveryService,
|
|
45
|
-
private readonly metadataScanner: MetadataScanner,
|
|
46
|
-
private readonly externalContextCreator: ExternalContextCreator,
|
|
47
|
-
) {}
|
|
48
|
-
|
|
49
|
-
public async load(
|
|
50
|
-
sources: AnnotationSchemaSources,
|
|
51
|
-
opts?: { ignoreMissingSources?: boolean },
|
|
52
|
-
) {
|
|
53
|
-
const loadedSources = await this.batchLoadSources(sources, {
|
|
54
|
-
ignoreMissingSources: opts?.ignoreMissingSources ?? false,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const schema = stitchSchemas({
|
|
58
|
-
subschemas: loadedSources.map((s) => s.subschema),
|
|
59
|
-
});
|
|
60
|
-
const annotations = shallowMerge(
|
|
61
|
-
loadedSources.map((s) => s.annotation),
|
|
62
|
-
) as GraphQLAnnotationMeta;
|
|
63
|
-
const annotationResolvers = await this.discoverAnnotationResolvers();
|
|
64
|
-
|
|
65
|
-
return this.embedAnnotationResolversToSchema(
|
|
66
|
-
schema,
|
|
67
|
-
annotations,
|
|
68
|
-
annotationResolvers,
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private async discoverAnnotationResolvers() {
|
|
73
|
-
type AnnotationName = string;
|
|
74
|
-
type AnnotationCallback = (...args: any[]) => any;
|
|
75
|
-
const annotationResolvers = new Map<AnnotationName, AnnotationCallback>();
|
|
76
|
-
|
|
77
|
-
const paramFactory = new GraphQLAnnotationResolverParamsFactory();
|
|
78
|
-
const instances = this.resolverDiscoveryService.explore();
|
|
79
|
-
|
|
80
|
-
for (const { instance } of instances)
|
|
81
|
-
for (const methodName of this.metadataScanner.getAllMethodNames(
|
|
82
|
-
Object.getPrototypeOf(instance),
|
|
83
|
-
)) {
|
|
84
|
-
const resolverMeta = Reflect.getMetadata(
|
|
85
|
-
ANNOTATION_RESOLVER_METADATA,
|
|
86
|
-
instance[methodName],
|
|
87
|
-
) as AnnotationResolverMetadata;
|
|
88
|
-
|
|
89
|
-
if (!resolverMeta) continue; // Skip non-annotated
|
|
90
|
-
|
|
91
|
-
this._logger.log(
|
|
92
|
-
`Discovered resolver for annotation "${resolverMeta.annotation}"`,
|
|
93
|
-
);
|
|
94
|
-
annotationResolvers.set(
|
|
95
|
-
resolverMeta.annotation,
|
|
96
|
-
this.externalContextCreator.create<
|
|
97
|
-
Record<number, ParamMetadata>,
|
|
98
|
-
GqlContextType
|
|
99
|
-
>(
|
|
100
|
-
instance,
|
|
101
|
-
instance[methodName],
|
|
102
|
-
methodName,
|
|
103
|
-
PARAM_ARGS_METADATA,
|
|
104
|
-
paramFactory,
|
|
105
|
-
undefined,
|
|
106
|
-
undefined,
|
|
107
|
-
undefined,
|
|
108
|
-
"graphql",
|
|
109
|
-
),
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return annotationResolvers;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private async batchLoadSources(
|
|
117
|
-
sources: AnnotationSchemaSources,
|
|
118
|
-
opts?: { ignoreMissingSources?: boolean },
|
|
119
|
-
) {
|
|
120
|
-
const pending = Object.entries(sources).map(async ([name, url]) => {
|
|
121
|
-
const loadId = `${name}@${url}`;
|
|
122
|
-
try {
|
|
123
|
-
const loaded = {
|
|
124
|
-
id: loadId,
|
|
125
|
-
url: url,
|
|
126
|
-
name: name,
|
|
127
|
-
subschema: await loadGraphQLSchema(url),
|
|
128
|
-
annotation: await this.loadAnnotations(url),
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
// Cleanup
|
|
132
|
-
// Remove `_annotations` from being merged to main schema
|
|
133
|
-
loaded.subschema.transforms = [
|
|
134
|
-
new FilterRootFields(
|
|
135
|
-
(op, fieldName) => !fieldName.endsWith(ANNOTATION_QUERY_NAME),
|
|
136
|
-
),
|
|
137
|
-
];
|
|
138
|
-
|
|
139
|
-
this._logger.log(`Loaded schema for ${loadId}`);
|
|
140
|
-
return loaded;
|
|
141
|
-
} catch (err) {
|
|
142
|
-
this._logger.error(`Unable to load schema for ${loadId}`);
|
|
143
|
-
throw err;
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const [loaded, failed] = split(
|
|
148
|
-
await Promise.allSettled(pending),
|
|
149
|
-
(q) => q.status === "fulfilled",
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
const shouldThrow = opts?.ignoreMissingSources ??
|
|
153
|
-
if (failed.length > 0) {
|
|
154
|
-
if (shouldThrow)
|
|
155
|
-
throw new Error(
|
|
156
|
-
`Failed to load ${failed.length} schema(s) from sources.`,
|
|
157
|
-
);
|
|
158
|
-
else
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
this._logger.
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
info,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
1
|
+
import { stitchSchemas } from "@graphql-tools/stitch";
|
|
2
|
+
import { MapperKind, mapSchema } from "@graphql-tools/utils";
|
|
3
|
+
import { FilterRootFields, wrapSchema } from "@graphql-tools/wrap";
|
|
4
|
+
import { Injectable, Logger, ParamData } from "@nestjs/common";
|
|
5
|
+
import {
|
|
6
|
+
ExternalContextCreator,
|
|
7
|
+
MetadataScanner,
|
|
8
|
+
ParamsFactory,
|
|
9
|
+
} from "@nestjs/core";
|
|
10
|
+
import { ParamMetadata } from "@nestjs/core/helpers/interfaces/params-metadata.interface";
|
|
11
|
+
import {
|
|
12
|
+
GqlContextType,
|
|
13
|
+
GraphQLExecutionContext,
|
|
14
|
+
PARAM_ARGS_METADATA,
|
|
15
|
+
} from "@nestjs/graphql";
|
|
16
|
+
import { GqlParamtype } from "@nestjs/graphql/dist/enums/gql-paramtype.enum";
|
|
17
|
+
import { GraphQLResolveInfo, GraphQLSchema } from "graphql";
|
|
18
|
+
import { GraphQLClient } from "graphql-request";
|
|
19
|
+
import {
|
|
20
|
+
ANNOTATION_RESOLVER_METADATA,
|
|
21
|
+
AnnotationResolverMetadata,
|
|
22
|
+
AnnotationSchemaSources,
|
|
23
|
+
ResolverName,
|
|
24
|
+
} from "./annotation.constants";
|
|
25
|
+
import {
|
|
26
|
+
ANNOTATION_QUERY_NAME,
|
|
27
|
+
GraphQLAnnotationMeta,
|
|
28
|
+
GraphQLAnnotationResolver,
|
|
29
|
+
} from "./annotation.resolver";
|
|
30
|
+
import {
|
|
31
|
+
isPlainObject,
|
|
32
|
+
loadGraphQLSchema,
|
|
33
|
+
shallowMerge,
|
|
34
|
+
split,
|
|
35
|
+
} from "./annotation.utils";
|
|
36
|
+
import { ResolverDiscoveryService } from "./resolver.explorer";
|
|
37
|
+
import { instanceToPlain } from "class-transformer";
|
|
38
|
+
|
|
39
|
+
@Injectable()
|
|
40
|
+
export class GraphQLAnnotatedSchemaLoader {
|
|
41
|
+
private _logger = new Logger("GraphQLAnnotationModule");
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
private readonly resolverDiscoveryService: ResolverDiscoveryService,
|
|
45
|
+
private readonly metadataScanner: MetadataScanner,
|
|
46
|
+
private readonly externalContextCreator: ExternalContextCreator,
|
|
47
|
+
) {}
|
|
48
|
+
|
|
49
|
+
public async load(
|
|
50
|
+
sources: AnnotationSchemaSources,
|
|
51
|
+
opts?: { ignoreMissingSources?: boolean },
|
|
52
|
+
) {
|
|
53
|
+
const loadedSources = await this.batchLoadSources(sources, {
|
|
54
|
+
ignoreMissingSources: opts?.ignoreMissingSources ?? false,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const schema = stitchSchemas({
|
|
58
|
+
subschemas: loadedSources.map((s) => s.subschema),
|
|
59
|
+
});
|
|
60
|
+
const annotations = shallowMerge(
|
|
61
|
+
loadedSources.map((s) => s.annotation),
|
|
62
|
+
) as GraphQLAnnotationMeta;
|
|
63
|
+
const annotationResolvers = await this.discoverAnnotationResolvers();
|
|
64
|
+
|
|
65
|
+
return this.embedAnnotationResolversToSchema(
|
|
66
|
+
schema,
|
|
67
|
+
annotations,
|
|
68
|
+
annotationResolvers,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async discoverAnnotationResolvers() {
|
|
73
|
+
type AnnotationName = string;
|
|
74
|
+
type AnnotationCallback = (...args: any[]) => any;
|
|
75
|
+
const annotationResolvers = new Map<AnnotationName, AnnotationCallback>();
|
|
76
|
+
|
|
77
|
+
const paramFactory = new GraphQLAnnotationResolverParamsFactory();
|
|
78
|
+
const instances = this.resolverDiscoveryService.explore();
|
|
79
|
+
|
|
80
|
+
for (const { instance } of instances)
|
|
81
|
+
for (const methodName of this.metadataScanner.getAllMethodNames(
|
|
82
|
+
Object.getPrototypeOf(instance),
|
|
83
|
+
)) {
|
|
84
|
+
const resolverMeta = Reflect.getMetadata(
|
|
85
|
+
ANNOTATION_RESOLVER_METADATA,
|
|
86
|
+
instance[methodName],
|
|
87
|
+
) as AnnotationResolverMetadata;
|
|
88
|
+
|
|
89
|
+
if (!resolverMeta) continue; // Skip non-annotated
|
|
90
|
+
|
|
91
|
+
this._logger.log(
|
|
92
|
+
`Discovered resolver for annotation "${resolverMeta.annotation}"`,
|
|
93
|
+
);
|
|
94
|
+
annotationResolvers.set(
|
|
95
|
+
resolverMeta.annotation,
|
|
96
|
+
this.externalContextCreator.create<
|
|
97
|
+
Record<number, ParamMetadata>,
|
|
98
|
+
GqlContextType
|
|
99
|
+
>(
|
|
100
|
+
instance,
|
|
101
|
+
instance[methodName],
|
|
102
|
+
methodName,
|
|
103
|
+
PARAM_ARGS_METADATA,
|
|
104
|
+
paramFactory,
|
|
105
|
+
undefined,
|
|
106
|
+
undefined,
|
|
107
|
+
undefined,
|
|
108
|
+
"graphql",
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return annotationResolvers;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async batchLoadSources(
|
|
117
|
+
sources: AnnotationSchemaSources,
|
|
118
|
+
opts?: { ignoreMissingSources?: boolean },
|
|
119
|
+
) {
|
|
120
|
+
const pending = Object.entries(sources).map(async ([name, url]) => {
|
|
121
|
+
const loadId = `${name}@${url}`;
|
|
122
|
+
try {
|
|
123
|
+
const loaded = {
|
|
124
|
+
id: loadId,
|
|
125
|
+
url: url,
|
|
126
|
+
name: name,
|
|
127
|
+
subschema: await loadGraphQLSchema(url),
|
|
128
|
+
annotation: await this.loadAnnotations(url),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Cleanup
|
|
132
|
+
// Remove `_annotations` from being merged to main schema
|
|
133
|
+
loaded.subschema.transforms = [
|
|
134
|
+
new FilterRootFields(
|
|
135
|
+
(op, fieldName) => !fieldName.endsWith(ANNOTATION_QUERY_NAME),
|
|
136
|
+
),
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
this._logger.log(`Loaded schema for ${loadId}`);
|
|
140
|
+
return loaded;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
this._logger.error(`Unable to load schema for ${loadId}`);
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const [loaded, failed] = split(
|
|
148
|
+
await Promise.allSettled(pending),
|
|
149
|
+
(q) => q.status === "fulfilled",
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const shouldThrow = !(opts?.ignoreMissingSources ?? false);
|
|
153
|
+
if (failed.length > 0) {
|
|
154
|
+
if (shouldThrow)
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Failed to load ${failed.length} schema(s) from sources.`,
|
|
157
|
+
);
|
|
158
|
+
else
|
|
159
|
+
this._logger.warn(
|
|
160
|
+
`Failed to load ${failed.length} schema(s) from sources.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return loaded.map((l) => l.value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async loadAnnotations(url: string) {
|
|
168
|
+
const client = new GraphQLClient(url);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
type ExpectedResult = {
|
|
172
|
+
[ANNOTATION_QUERY_NAME]: GraphQLAnnotationMeta;
|
|
173
|
+
};
|
|
174
|
+
const res = await client.rawRequest<ExpectedResult>(`
|
|
175
|
+
query IntrospectAnnotations {
|
|
176
|
+
${ANNOTATION_QUERY_NAME} {
|
|
177
|
+
name
|
|
178
|
+
resolvers
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
`);
|
|
182
|
+
|
|
183
|
+
this._logger.log(`Loaded annotations for ${url}`);
|
|
184
|
+
return res.data[ANNOTATION_QUERY_NAME];
|
|
185
|
+
} catch (err) {
|
|
186
|
+
this._logger.warn(`Failed to load annotations at ${url}, assuming empty`);
|
|
187
|
+
return {} as GraphQLAnnotationMeta;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async embedAnnotationResolversToSchema(
|
|
192
|
+
schema: GraphQLSchema,
|
|
193
|
+
annotations: GraphQLAnnotationMeta,
|
|
194
|
+
annotationResolvers: Map<ResolverName, (...args: any[]) => any>,
|
|
195
|
+
) {
|
|
196
|
+
return mapSchema(schema, {
|
|
197
|
+
[MapperKind.ROOT_FIELD]: (fieldSchema, name, type) => {
|
|
198
|
+
const fieldAnnotations = (annotations.resolvers[name] ?? [])
|
|
199
|
+
.map((info) => ({
|
|
200
|
+
...info,
|
|
201
|
+
resolver: annotationResolvers.get(info.annotation)!, // NOTE: should be validated in filter
|
|
202
|
+
}))
|
|
203
|
+
.filter((info) => {
|
|
204
|
+
const annotationHasResolver = info.resolver !== undefined;
|
|
205
|
+
if (!annotationHasResolver)
|
|
206
|
+
this._logger.warn(
|
|
207
|
+
`Found unhandled annotation "${info.annotation}", skipping linking process`,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return annotationHasResolver;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (fieldAnnotations.length <= 0) return fieldSchema; // No transform if there's no annotation
|
|
214
|
+
|
|
215
|
+
// If there exist a resolver for an annotate parameter, remove it from schema to prevent external injection
|
|
216
|
+
for (const annotation of fieldAnnotations)
|
|
217
|
+
if (annotation.target.type === "parameter" && fieldSchema.args)
|
|
218
|
+
delete fieldSchema.args[annotation.target.paramName];
|
|
219
|
+
|
|
220
|
+
const defaultResolver = fieldSchema.resolve!;
|
|
221
|
+
fieldSchema.resolve = async function (
|
|
222
|
+
parent,
|
|
223
|
+
args: Record<string, any>,
|
|
224
|
+
context: GraphQLExecutionContext,
|
|
225
|
+
info,
|
|
226
|
+
) {
|
|
227
|
+
const annotationCallbacks = await Promise.all(
|
|
228
|
+
fieldAnnotations.map(async (annotation) => ({
|
|
229
|
+
annotation,
|
|
230
|
+
return: await annotation.resolver(
|
|
231
|
+
...([
|
|
232
|
+
info.rootValue,
|
|
233
|
+
annotation.data as Record<string, unknown>,
|
|
234
|
+
context,
|
|
235
|
+
info,
|
|
236
|
+
] satisfies GraphQLAnnotationResolverArgs),
|
|
237
|
+
),
|
|
238
|
+
})),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const resolvedParams = annotationCallbacks.reduce(
|
|
242
|
+
(params, call) => {
|
|
243
|
+
if (call.annotation.target.type !== "parameter") return params;
|
|
244
|
+
|
|
245
|
+
const returnedClassType =
|
|
246
|
+
typeof call.return === "object" && !isPlainObject(call.return);
|
|
247
|
+
const returnValue = returnedClassType
|
|
248
|
+
? instanceToPlain(call.return)
|
|
249
|
+
: call.return;
|
|
250
|
+
params[call.annotation.target.paramName] = returnValue;
|
|
251
|
+
|
|
252
|
+
return params;
|
|
253
|
+
},
|
|
254
|
+
{} as Record<string, any>,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
return defaultResolver(
|
|
258
|
+
parent,
|
|
259
|
+
{ ...args, ...resolvedParams },
|
|
260
|
+
context,
|
|
261
|
+
info,
|
|
262
|
+
);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
return fieldSchema;
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
type GraphQLAnnotationResolverArgs = [
|
|
272
|
+
// Root/parent (follows graphql context to match)
|
|
273
|
+
any,
|
|
274
|
+
// Data
|
|
275
|
+
Record<string, unknown>,
|
|
276
|
+
// GraphQL Context
|
|
277
|
+
GraphQLExecutionContext,
|
|
278
|
+
// GraphQL Resolver Info
|
|
279
|
+
GraphQLResolveInfo,
|
|
280
|
+
];
|
|
281
|
+
class GraphQLAnnotationResolverParamsFactory implements ParamsFactory {
|
|
282
|
+
// This param factory extends graphql's own just with different inputs from annotations instead.
|
|
283
|
+
// https://github.com/nestjs/graphql/blob/master/packages/graphql/lib/factories/params.factory.ts#L9
|
|
284
|
+
exchangeKeyForValue(
|
|
285
|
+
type: number,
|
|
286
|
+
possibleKey: ParamData,
|
|
287
|
+
// NOTE: nestjs passes this in array, so we have to "get the first element"
|
|
288
|
+
argsContext: GraphQLAnnotationResolverArgs,
|
|
289
|
+
) {
|
|
290
|
+
if (!argsContext) return null;
|
|
291
|
+
|
|
292
|
+
const args = {
|
|
293
|
+
parentValue: argsContext[0],
|
|
294
|
+
data: argsContext[1],
|
|
295
|
+
context: argsContext[2],
|
|
296
|
+
info: argsContext[3],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
switch (type as GqlParamtype) {
|
|
300
|
+
case GqlParamtype.ROOT:
|
|
301
|
+
return args.parentValue;
|
|
302
|
+
case GqlParamtype.ARGS:
|
|
303
|
+
return possibleKey && args.data
|
|
304
|
+
? args.data[possibleKey as string]
|
|
305
|
+
: args.data;
|
|
306
|
+
case GqlParamtype.CONTEXT:
|
|
307
|
+
return possibleKey && args.context
|
|
308
|
+
? args.context[possibleKey as string]
|
|
309
|
+
: args.context;
|
|
310
|
+
case GqlParamtype.INFO:
|
|
311
|
+
return possibleKey && args.context
|
|
312
|
+
? args.info[possibleKey as string]
|
|
313
|
+
: args.info;
|
|
314
|
+
default:
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|