1 | // @flow
|
2 |
|
3 | import path from 'path'
|
4 | import { SyntaxTree } from './SyntaxTree'
|
5 | import { GQLBase, META_KEY } from './GQLBase'
|
6 | import { GQLEnum } from './GQLEnum'
|
7 | import { GQLInterface } from './GQLInterface'
|
8 | import { GQLScalar } from './GQLScalar'
|
9 | import { typeOf } from 'ne-types'
|
10 | import { LatticeLogs as ll } from './utils'
|
11 | import { merge } from 'lodash'
|
12 | import EventEmitter from 'events'
|
13 | import {
|
14 | parse,
|
15 | print,
|
16 | buildSchema,
|
17 | GraphQLInterfaceType,
|
18 | GraphQLEnumType,
|
19 | GraphQLScalarType,
|
20 | GraphQLSchema
|
21 | } from 'graphql'
|
22 |
|
23 | /**
|
24 | * The SchemaUtils is used by tools such as GQLExpressMiddleware in order to
|
25 | * apply GraphQL Lattice specifics to the build schema.
|
26 | *
|
27 | * @class SchemaUtils
|
28 | */
|
29 | export class SchemaUtils extends EventEmitter {
|
30 | /**
|
31 | * Calls all the Lattice post-schema creation routines on a given Schema
|
32 | * using data from a supplied array of classes.
|
33 | *
|
34 | * @param {GraphQLSchema} schema the schema to post-process
|
35 | * @param {Array<GQLBase>} Classes the Classes from which to drive post
|
36 | * processing data from
|
37 | */
|
38 | static injectAll(schema: GraphQLSchema, Classes: Array<GQLBase>) {
|
39 | SchemaUtils.injectInterfaceResolvers(schema, Classes);
|
40 | SchemaUtils.injectEnums(schema, Classes);
|
41 | SchemaUtils.injectScalars(schema, Classes);
|
42 | SchemaUtils.injectComments(schema, Classes);
|
43 | }
|
44 |
|
45 | /**
|
46 | * Until such time as I can get the reference Facebook GraphQL AST parser to
|
47 | * read and apply descriptions or until such time as I employ the Apollo
|
48 | * AST parser, providing a `static get apiDocs()` getter is the way to get
|
49 | * your descriptions into the proper fields, post schema creation.
|
50 | *
|
51 | * This method walks the types in the registered classes and the supplied
|
52 | * schema type. It then injects the written comments such that they can
|
53 | * be exposed in graphiql and to applications or code that read the meta
|
54 | * fields of a built schema
|
55 | *
|
56 | * @memberof SchemaUtils
|
57 | * @method ⌾⠀injectComments
|
58 | * @static
|
59 | * @since 2.7.0
|
60 | *
|
61 | * @param {Object} schema a built GraphQLSchema object created via buildSchema
|
62 | * or some other alternative but compatible manner
|
63 | * @param {Function[]} Classes these are GQLBase extended classes used to
|
64 | * manipulate the schema with.
|
65 | */
|
66 | static injectComments(schema: Object, Classes: Array<GQLBase>) {
|
67 | const {
|
68 | DOC_CLASS, DOC_FIELDS, DOC_QUERIES, DOC_MUTATORS, DOC_SUBSCRIPTIONS,
|
69 | DOC_QUERY, DOC_MUTATION, DOC_SUBSCRIPTION
|
70 | } = GQLBase;
|
71 |
|
72 | for (let Class of Classes) {
|
73 | const docs = Class.apiDocs();
|
74 | const query = schema._typeMap.Query;
|
75 | const mutation = schema._typeMap.Mutation;
|
76 | const subscription = schema._typeMap.Subscription;
|
77 | let type;
|
78 |
|
79 | if ((type = schema._typeMap[Class.name])) {
|
80 | let fields = type._fields;
|
81 | let values = type._values;
|
82 |
|
83 | if (docs[DOC_CLASS]) { type.description = docs[DOC_CLASS] }
|
84 |
|
85 | for (let field of Object.keys(docs[DOC_FIELDS] || {})) {
|
86 | if (fields && field in fields) {
|
87 | fields[field].description = docs[DOC_FIELDS][field];
|
88 | }
|
89 | if (values) {
|
90 | for (let value of values) {
|
91 | if (value.name === field) {
|
92 | value.description = docs[DOC_FIELDS][field]
|
93 | }
|
94 | }
|
95 | }
|
96 | }
|
97 | }
|
98 |
|
99 | for (let [_type, _CONST, _topCONST] of [
|
100 | [query, DOC_QUERIES, DOC_QUERY],
|
101 | [mutation, DOC_MUTATORS, DOC_MUTATION],
|
102 | [subscription, DOC_SUBSCRIPTIONS, DOC_SUBSCRIPTION]
|
103 | ]) {
|
104 | if (
|
105 | _type
|
106 | && (
|
107 | (Object.keys(docs[_CONST] || {}).length)
|
108 | || (docs[_topCONST] && docs[_topCONST].length)
|
109 | )
|
110 | ) {
|
111 | let fields = _type._fields;
|
112 |
|
113 | if (docs[_topCONST]) {
|
114 | _type.description = docs[_topCONST]
|
115 | }
|
116 |
|
117 | for (let field of Object.keys(docs[_CONST])) {
|
118 | if (field in fields) {
|
119 | fields[field].description = docs[_CONST][field];
|
120 | }
|
121 | }
|
122 | }
|
123 | }
|
124 | }
|
125 | }
|
126 |
|
127 | /**
|
128 | * Somewhat like `injectComments` and other similar methods, the
|
129 | * `injectInterfaceResolvers` method walks the registered classes and
|
130 | * finds `GQLInterface` types and applies their `resolveType()`
|
131 | * implementations.
|
132 | *
|
133 | * @memberof SchemaUtils
|
134 | * @method ⌾⠀injectInterfaceResolvers
|
135 | * @static
|
136 | *
|
137 | * @param {Object} schema a built GraphQLSchema object created via buildSchema
|
138 | * or some other alternative but compatible manner
|
139 | * @param {Function[]} Classes these are GQLBase extended classes used to
|
140 | * manipulate the schema with.
|
141 | */
|
142 | static injectInterfaceResolvers(schema: Object, Classes: Array<GQLBase>) {
|
143 | for (let Class of Classes) {
|
144 | if (Class.GQL_TYPE === GraphQLInterfaceType) {
|
145 | schema._typeMap[Class.name].resolveType =
|
146 | schema._typeMap[Class.name]._typeConfig.resolveType =
|
147 | Class.resolveType;
|
148 | }
|
149 | }
|
150 | }
|
151 |
|
152 | /**
|
153 | * Somewhat like `injectComments` and other similar methods, the
|
154 | * `injectInterfaceResolvers` method walks the registered classes and
|
155 | * finds `GQLInterface` types and applies their `resolveType()`
|
156 | * implementations.
|
157 | *
|
158 | * @memberof SchemaUtils
|
159 | * @method ⌾⠀injectEnums
|
160 | * @static
|
161 | *
|
162 | * @param {Object} schema a built GraphQLSchema object created via buildSchema
|
163 | * or some other alternative but compatible manner
|
164 | * @param {Function[]} Classes these are GQLBase extended classes used to
|
165 | * manipulate the schema with.
|
166 | */
|
167 | static injectEnums(schema: Object, Classes: Array<GQLBase>) {
|
168 | for (let Class of Classes) {
|
169 | if (Class.GQL_TYPE === GraphQLEnumType) {
|
170 | const __enum = schema._typeMap[Class.name];
|
171 | const values = Class.values;
|
172 |
|
173 | for (let value of __enum._values) {
|
174 | if (value.name in values) {
|
175 | merge(value, values[value.name])
|
176 | }
|
177 | }
|
178 | }
|
179 | }
|
180 | }
|
181 |
|
182 | /**
|
183 | * GQLScalar types must define three methods to have a valid implementation.
|
184 | * They are serialize, parseValue and parseLiteral. See their docs for more
|
185 | * info on how to do so.
|
186 | *
|
187 | * This code finds each scalar and adds their implementation details to the
|
188 | * generated schema type config.
|
189 | *
|
190 | * @memberof SchemaUtils
|
191 | * @method ⌾⠀injectScalars
|
192 | * @static
|
193 | *
|
194 | * @param {Object} schema a built GraphQLSchema object created via buildSchema
|
195 | * or some other alternative but compatible manner
|
196 | * @param {Function[]} Classes these are GQLBase extended classes used to
|
197 | * manipulate the schema with.
|
198 | */
|
199 | static injectScalars(schema: Object, Classes: Array<GQLBase>) {
|
200 | for (let Class of Classes) {
|
201 | if (Class.GQL_TYPE === GraphQLScalarType) {
|
202 | // @ComputedType
|
203 | const type = schema._typeMap[Class.name];
|
204 |
|
205 | // @ComputedType
|
206 | const { serialize, parseValue, parseLiteral } = Class;
|
207 |
|
208 | if (!serialize || !parseValue || !parseLiteral) {
|
209 | // @ComputedType
|
210 | ll.error(`Scalar type ${Class.name} has invaild impl.`);
|
211 | continue;
|
212 | }
|
213 |
|
214 | merge(type._scalarConfig, {
|
215 | serialize,
|
216 | parseValue,
|
217 | parseLiteral
|
218 | });
|
219 | }
|
220 | }
|
221 | }
|
222 |
|
223 | /**
|
224 | * A function that combines the IDL schemas of all the supplied classes and
|
225 | * returns that value to the middleware getter.
|
226 | *
|
227 | * @static
|
228 | * @memberof GQLExpressMiddleware
|
229 | * @method ⌾⠀generateSchemaSDL
|
230 | *
|
231 | * @return {string} a dynamically generated GraphQL IDL schema string
|
232 | */
|
233 | static generateSchemaSDL(
|
234 | Classes: Array<GQLBase>,
|
235 | logOutput: boolean = true
|
236 | ): string {
|
237 | let schema = SyntaxTree.EmptyDocument();
|
238 | let log = (...args) => {
|
239 | if (logOutput) {
|
240 | console.log(...args);
|
241 | }
|
242 | }
|
243 |
|
244 | for (let Class of Classes) {
|
245 | let classSchema = Class.SCHEMA;
|
246 |
|
247 | if (typeOf(classSchema) === 'Symbol') {
|
248 | let handler = Class.handler;
|
249 | let filename = path.basename(Class.handler.path)
|
250 |
|
251 | classSchema = handler.getSchema();
|
252 | log(
|
253 | `\nRead schema (%s)\n%s\n%s\n`,
|
254 | filename,
|
255 | '-'.repeat(14 + filename.length),
|
256 | classSchema.replace(/^/gm, ' ')
|
257 | )
|
258 | }
|
259 |
|
260 | schema.appendDefinitions(classSchema);
|
261 | }
|
262 |
|
263 | log('\nGenerated GraphQL Schema\n----------------\n%s', schema);
|
264 |
|
265 | return schema.toString();
|
266 | }
|
267 |
|
268 | /**
|
269 | * An asynchronous function used to parse the supplied classes for each
|
270 | * ones resolvers and mutators. These are all combined into a single root
|
271 | * object passed to express-graphql.
|
272 | *
|
273 | * @static
|
274 | * @memberof SchemaUtils
|
275 | * @method ⌾⠀createMergedRoot
|
276 | *
|
277 | * @param {Array<GQLBase>} Classes the GQLBase extended class objects or
|
278 | * functions from which to merge the RESOLVERS and MUTATORS functions.
|
279 | * @param {Object} requestData for Express apss, this will be an object
|
280 | * containing { req, res, gql } where those are the Express request and
|
281 | * response object as well as the GraphQL parameters for the request.
|
282 | * @return {Promise<Object>} a Promise resolving to an Object containing all
|
283 | * the functions described in both Query and Mutation types.
|
284 | */
|
285 | static async createMergedRoot(
|
286 | Classes: Array<GQLBase>,
|
287 | requestData: Object,
|
288 | separateByType: boolean = false
|
289 | ): Promise<Object> {
|
290 | const root = {};
|
291 |
|
292 | for (let Class of Classes) {
|
293 | merge(
|
294 | root,
|
295 | // $FlowFixMe
|
296 | await Class.getMergedRoot(requestData, separateByType)
|
297 | );
|
298 | }
|
299 |
|
300 | return root;
|
301 | }
|
302 | }
|
303 |
|
304 | export default SchemaUtils
|