UNPKG

9.48 kBJavaScriptView Raw
1// @flow
2
3import path from 'path'
4import { SyntaxTree } from './SyntaxTree'
5import { GQLBase, META_KEY } from './GQLBase'
6import { GQLEnum } from './GQLEnum'
7import { GQLInterface } from './GQLInterface'
8import { GQLScalar } from './GQLScalar'
9import { typeOf } from 'ne-types'
10import { LatticeLogs as ll } from './utils'
11import { merge } from 'lodash'
12import EventEmitter from 'events'
13import {
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 */
29export 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
304export default SchemaUtils