UNPKG

29 kBJavaScriptView Raw
1// @flow
2
3import { GQLBase, META_KEY } from './GQLBase'
4import { GQLEnum } from './GQLEnum'
5import { GQLInterface } from './GQLInterface'
6import { GQLScalar } from './GQLScalar'
7// import { GQLUnion } from './GQLUnion'
8import { SyntaxTree } from './SyntaxTree'
9import { customDedent } from 'ne-tag-fns'
10import { typeOf, extendsFrom } from 'ne-types'
11import { inspect } from 'util'
12import { resolver, mutator, subscriptor } from './decorators/Resolvers'
13import { Properties } from './decorators/ModelProperties'
14import { LatticeLogs as LL } from './utils'
15
16const _i = (...args) => inspect(...args, {colors: true, depth: 3})
17
18
19/**
20 * The CHECKLIST Symbol is used as a storage key in the metadata staging
21 * area for each of the GQLBase extended classes. In the LatticeFactory
22 * it is used to determine where in the flow of construction the class
23 * currently is.
24 *
25 * @type {Symbol}
26 */
27export const CHECKLIST = Symbol.for('checklist')
28
29/**
30 * The CHECK_SCHEMA Symbol is part of the CHECKLIST for a constructed
31 * GQLBase extended class. It denotes that the class has had its SCHEMA
32 * getter defined.
33 *
34 * @type {Symbol}
35 */
36export const CHECK_SCHEMA = Symbol.for('checklist-schema')
37
38/**
39 * The CHECK_RESOLVERS Symbol is part of the CHECKLIST for a constructed
40 * GQLBase extended class. It denotes that the class has had its instance
41 * field resolvers as well as its static Query, Mutation and Subscription
42 * resolvers injected and defined.
43 *
44 * @type {Symbol}
45 */
46export const CHECK_RESOLVERS = Symbol.for('checklist-resolvers')
47
48/**
49 * The CHECK_API_DOCS Symbol is part of the CHECKLIST for a constructed
50 * GQLBase extended class. It denotes that the class has had its api docs
51 * defined, processed and setup on the class in a way that it will be
52 * picked up in the build lifecycle.
53 *
54 * @type {Symbol}
55 */
56export const CHECK_API_DOCS = Symbol.for('checklist-api-docs')
57
58/**
59 * Peeks into the metadata storage area of a given GQLBase extended
60 * class and fetches the factory checklist if one exists.
61 *
62 * @param {Function} Class a reference to the GQLBase class to peek in
63 * @return {Object} an object setup with at least three booleans keyed by
64 * the constants CHECK_SCHEMA, CHECK_RESOLVERS, and CHECK_API_DOCS or null
65 * if none exists
66 */
67export function getChecklist(Class: Function) {
68 return (Class && Class[META_KEY] && Class[META_KEY][CHECKLIST]) || null
69}
70
71/**
72 * Obtains the checklist from the supplied GQLBase extended class. If the
73 * class has a checklist, its checklist item is set to true or the boolean
74 * value specified.
75 *
76 * @param {Function} Class a reference to the GQLBase class to set
77 * @param {Symbol} item one of CHECK_SCHEMA, CHECK_RESOLVERS, or
78 * CHECK_API_DOCS
79 * @param {Boolean} value the value for the checklist item to set
80 */
81export function setChecklist(
82 Class: Function,
83 item: Symbol,
84 value: boolean = true
85) {
86 let checklist = getChecklist(Class)
87
88 if (checklist) {
89 // $FlowFixMe
90 checklist[item] = value
91 }
92}
93
94/**
95 * This function, when invoked with only a class will return true if the
96 * Class has a defined checklist. If one ore more CHECKLIST symbols are
97 * passed, the function will only return true if all the supplied symbols
98 * are set to truthy values.
99 *
100 * @param {Function} Class a reference to the GQLBase class to set
101 * @param {Array<Symbol>} items any of CHECK_SCHEMA, CHECK_RESOLVERS, or
102 * CHECK_API_DOCS
103 * @return {Boolean} true if the checklist and/or all items are true and
104 * present.
105 */
106export function hasChecklist(Class: Function, ...items: Array<Symbol>) {
107 let checklist = getChecklist(Class)
108
109 if (checklist && items.length) {
110 for (let item of items) {
111 if (!checklist[item]) {
112 return false
113 }
114 }
115
116 return true
117 }
118
119 return checklist
120}
121
122/**
123 * Injects and creates a new CHECKLIST object on the supplied GQLBase
124 * extended class. All items are installed and set to false.
125 *
126 * @param {Function} Class a reference to the GQLBase class to set
127 */
128export function newChecklist(Class: Function) {
129 if (Class) {
130 // $FlowFixMe
131 Class[META_KEY][CHECKLIST] = {
132 [CHECK_SCHEMA]: false,
133 [CHECK_RESOLVERS]: false,
134 [CHECK_API_DOCS]: false,
135
136 get keys() {
137 return [
138 CHECK_SCHEMA, CHECK_RESOLVERS, CHECK_API_DOCS
139 ]
140 },
141
142 get complete() {
143 return this.keys.reduce((p,c,i,a) => {
144 if (!p || !this[c]) { return false }
145 }, true)
146 }
147 }
148 }
149 else {
150 throw new Error(customDedent({dropLowest:true})`
151 Cannot create new checklist metadata on a non-existent class
152 `)
153 }
154}
155
156export class ValidationResults {
157 errors: Array<Error>
158
159 constructor(errors: Array<Error> = []) {
160 this.errors = errors
161 }
162
163 get valid(): boolean { return this.errors.length === 0 }
164
165 // $FlowFixMe
166 get [Symbol.toStringTag](): string { return this.constructor.name }
167
168 // $FlowFixMe
169 static get [Symbol.toStringTag](): string { return this.name }
170}
171
172export class LatticeFactory {
173
174 /**
175 * Walks through a supplied template object and collects errors with its
176 * format before bubbling up an exception if any part of it fails to
177 * pass muster. The exception can be prevented from throwing if hide is set
178 * to true
179 *
180 * @param {Object} template an object to be parsed for construction via the
181 * Lattice Factory
182 * @param {boolean} hide if true, an invalid template will NOT throw errors
183 * @return {ValidationResults} a `ValidationResults` object containing the
184 * collected errors and a `valid` that is dynamically calculated.
185 */
186 static validateTemplate(
187 template: Object,
188 hide: boolean = false
189 ): ValidationResults {
190 let results = new ValidationResults()
191 let indent = (string: string, count: number = 4, space: string = ' ') => (
192 string
193 .split('\n')
194 .map(s => s.trim().replace(/(^)/gm, `$1${space.repeat(count)}`))
195 .join('\n')
196 )
197
198 if (typeof template.name === 'undefined') {
199 results.errors.push(new Error(customDedent({dropLowest:true})`
200 The \`template.name\` field must exist or the creation for the Lattice
201 factory class generation to succeed.
202
203 Please read the documentation for more information on the format of
204 a LatticeFactory template.
205 `))
206 }
207
208 if (!extendsFrom(template.name, String)) {
209 results.errors.push(new Error(customDedent({dropLowest:true})`
210 The \`template.name\` field must be a string.
211
212 Please read the documentation for more information on the format of
213 a LatticeFactory template.
214 `))
215 }
216
217 if (typeof template.schema === 'undefined') {
218 results.errors.push(new Error(customDedent({dropLowest:true})`
219 The \`template.schema\` field must exist or the creation for the
220 Lattice factory class generation to succeed.
221
222 Please read the documentation for more information on the format of
223 a LatticeFactory template.
224 `))
225 }
226
227 if (!extendsFrom(template.schema, String)) {
228 results.errors.push(new Error(customDedent({dropLowest:true})`
229 The \`template.schema\` field must be a string of GraphQL SDL/IDL
230
231 Please read the documentation for more information on the format of
232 a LatticeFactory template.
233 `))
234 }
235
236 if (
237 !extendsFrom(template.resolvers, Object) // Supports 95% of objects
238 || typeof template.resolvers !== 'object' // Supports Object.create(null)
239 ) {
240 results.errors.push(new Error(customDedent({dropLowest:true})`\x1b[91;1m
241 The \`template.resolvers\` field must be an Object containing the
242 resolver functions. Query, Mutation and Subscription resolvers will
243 take the following signature. Additionally, the keys for these special
244 resolvers must be Query, Mutation or Subscription; respectively
245 \x1b[37;22m
246 Query: { [resolver]: (requestData, resolverParameters) => {} }
247 Mutation: { [resolver]: (requestData, resolverParameters) => {} }
248 Subscription: { [resolver]: (requestData, resolverParameters) => {} }
249
250 where:
251 \`requestData\` is an object with { req, res, gql|next } depending
252 on the graphql server implementation (FB Reference, Apollo, etc)
253 \`resovlerParameters\` is an object with keys matching those
254 parameters defined in the SCHEMA for the resolver in question.
255 \x1b[91;1m
256 Field resolvers should be found under the key name of the type
257 or interface in question and must correspond to the following signature
258 \x1b[37;22m
259 [Type]: { [resolver]: (resolverParameters) => {} }
260
261 where:
262 \`Type\` is the name of the GQL type defined in the schema
263 \`resovlerParameters\` is an object with keys matching those
264 parameters defined in the SCHEMA for the resolver in question.
265
266 * it is worth noting that the field resolvers are not static and
267 can access the \`requestData\` object via \`this.requestData\`
268 \x1b[91;1m
269 Please read the documentation for more information on the format of
270 a LatticeFactory template.\x1b[0m
271 `))
272 }
273
274 if (typeof template.docs === 'undefined') {
275 results.errors.push(new Error(customDedent({dropLowest:true})`
276 The \`template.docs\` field must exist for the creation of the
277 Lattice factory class generation to succeed.
278
279 Please read the documentation for more information on the format of
280 a LatticeFactory template.
281 `))
282 }
283
284 if (
285 !extendsFrom(template.docs, Object) // Supports 95% of objects
286 || typeof template.docs !== 'object' // Supports Object.create(null)
287 ) {
288 let dr = '\x1b[31m', br = '\x1b[91m'
289 let b1 = '\x1b[1m', b0 = '\x1b[22m'
290 let bb = '\x1b[90m'
291 let dg = '\x1b[37m', bg = '\x1b[97m'
292 let a0 = '\x1b[0m'
293 let gr = '\x1b[32m', bgr = '\x1b[92m'
294
295 results.errors.push(new Error(customDedent({dropLowest:true})`\x1b[1;91m
296 The \`template.docs\` field must be an object containing keys and
297 value pairs matching the types, enums, unions and interfaces defined
298 in your schema.
299
300 The special Symbol object TYPE can be used to reference the docs for
301 the named or keyed field describing the documentation to be processed
302 Comments for the \`Query\`, \`Mutation\`, and \`Subscription\` [TYPE]
303 entries will replace any previous one that comes before it. Typically
304 this field is best left undescribed since there will ever only be
305 one of each at most.
306
307 \x1b[22;31mExamples should look something like this:\x1b[22;37m
308 import { TYPE, joinLines } from 'graphql-lattice'
309
310 export default {
311 ${bb}/* other fields */${dg}
312
313 ${b1}schema:${b0} joinLines${gr}\`
314 type Person { id: ID name: String }
315 type Query { findPerson(id: ID): Person }
316 type Mutation { setPersonName(id: ID, name: String): Person }
317 \`${dg},
318
319 ${b1}docs:${b0} {
320 ${b1}Person:${b0} {
321 [TYPE]: ${gr}'A contrived person type'${dg},
322 id: ${gr}'A unique identifier for a person'${dg},
323 name: ${gr}'A string denoting the name of a person'${dg}
324 },
325 ${b1}Query:${b0} {
326 findPerson: ${gr}'A query taking an ID, returns a Person'${dg},
327 },
328 ${b1}Mutation:${b0} {
329 setPersonName: joinLines${gr}\`
330 A mutation that sets the name of the user identified by an
331 ID to the new name value supplied
332 \`${dg}
333 }
334 }
335 }
336 \x1b[22;31m
337 Note the usage of \`Person\`, \`Query\` and \`Mutation\` explicitly
338 as keys to the supplied \`docs\` object.\x1b[0m
339 `))
340 }
341
342 if (!results.valid) {
343 let errorStrings = []
344
345 for (let error of results.errors) {
346 let { message, stack } = error
347
348 stack = stack
349 .trim()
350 .split('\n')
351 .splice(message.split('\n').length)
352 .map(s => s.trim())
353 .join('\n')
354 message = message.replace(/(Error:\s)/, '$1\n').trim()
355
356 errorStrings.push(
357 `\x1b[31;1m${message}\x1b[0m\n` + indent(stack)
358 )
359 }
360
361 let error = new Error(customDedent({dropLowest:true})`
362 OOPS!
363
364 An error occurred validating your factory template. The object
365 in question is as follows:
366
367 @template
368
369 The individual errors that occurred are:
370 \n@errors`
371 .replace(/@template/, indent(_i(template)))
372 .replace(/@errors/, errorStrings.join('\n\n'))
373 )
374
375 error.stack = error.message
376 error.message = ''
377
378 if (!hide) throw error
379 }
380
381 return results
382 }
383
384 /**
385 * The starting point of a LatticeFactory object -> class creation. The name
386 * of the class and baseClass to use are provided and are created from there.
387 * At this point, the generated class is still incomplete. It must complete
388 * the entire checklist before being deemed valid.
389 *
390 * @param {string} name name of the class to create
391 * @param {Function} baseClass the Lattice class your new class should extend;
392 * while this can be anything, it should be GQLBase, GQLInterface, GQLEnum or
393 * GQLUnion. This defaults to GQLBase should nothing be supplied
394 * @return {Function} actually this returns the generated class
395 */
396 static generateClass(name: string, baseClass: Function = GQLBase) {
397 if (!name) {
398 throw new Error('LatticeFactory.generateClass needs a name!!')
399 }
400
401 // Alright ladies and gentlemen, hold onto your hats; we're entering the
402 // meta zone!!! The way the following works is to make sure that our
403 // passed in base class `baseClass` is actually in scope as the name of
404 // the value it represents. We use the `new Function()` syntax to do that
405 // but we do it via eval since we don't know the name of the function
406 // at the time we write the code
407 //
408 // So given a class name of "Car" and baseName equalling GQLBase, the Class
409 // instance, fn would look something like the results of calling this
410 //
411 // let fn = new Function(
412 // "GQLBase",
413 // "class Car extends GQLBase {}; return Car;"
414 // )
415 //
416 // Which in turn sets fn to something that would be the same as
417 //
418 // function fn(GQLBase) { class Car extends GQLBase {}; return Car }
419 //
420 // Which means that when we invoke fn(baseClass), which is fn(GQLBase),
421 // we get the results we intend; even if GQLBase is not necessarily in
422 // the scope of the function at the time of call. Neat. Scary. OMG Thanks
423 // for code comments. You're welcome future me.
424 let fn = eval(`(new Function(
425 "${baseClass.name}",
426 "class ${name} extends ${baseClass.name} {}; return ${name};"
427 ))`);
428
429 let Class = fn(baseClass)
430
431 this.brandClass(Class)
432 newChecklist(Class)
433
434 return Class;
435 }
436
437 /**
438 * Injects the SCHEMA property into the newly defined class. The supplied
439 * `schema` string becomes what the new class returns when `.SCHEMA` is
440 * gotten.
441 *
442 * @param {Function} Class this will throw an error if the class is not one
443 * generated by the LatticeFactory or if the class itself is null or undefined
444 * @param {string} schema the string that the new class should return
445 * @return {Function} returns the modified Class with the `CHECK_SCHEMA`
446 * portion ticked off internally.
447 */
448 static injectSchema(Class: Function, schema: string) {
449 if (!Class || !hasChecklist(Class)) {
450 throw new Error(customDedent({dropLowest:true})`
451 Either the supplied schema string is invalid
452 SCHEMA: \`
453 ${schema}
454 \`
455
456 Or your supplied class ${(Class && Class.name) || 'undefined'} is
457 non-existent. Please check your code and try the LatticeFactory
458 again.
459 `)
460 }
461
462 // $FlowFixMe
463 Object.defineProperty(Class, 'SCHEMA', {
464 get() { return schema }
465 })
466
467 setChecklist(Class, CHECK_SCHEMA)
468
469 return Class
470 }
471
472 /**
473 * Injects the resolvers into appropriate areas. Resolvers keyed by `Query`,
474 * `Mutation`, or `Subscription` will be placed into the appropriate area
475 * in `Class[META_KEY]` which acts as a staging area originally designed for
476 * use with the @resolver, @mutator and @subscriptor decorators. These will
477 * be bound in a typical fashion as is done with the decorators making the
478 * first parameter becoming the requestData of the object instance and the
479 * second being the object containing the parameters for the resolver as
480 * passed in by GraphQL. Subsequent parameters will be supplied as is the
481 * fashion of the system you're using; Facebook's reference implementation or
482 * Apollo or something else.
483 *
484 * Resolvers keyed by type name are considered to be field resolvers and
485 * have a different signature. They can be properties of the key, in
486 * which case they will simply be installed as getters. Or they can be
487 * functions; synchronous or asynchronous. Function field resolvers are
488 * instance methods and can make use of `this.getModel()` or
489 * `this.requestData` internally.
490 *
491 * @param {Function} Class the class, generated by generateClass() lest an
492 * error be thrown, to which to add the resolvers from a template
493 * @param {Object} resolverObj an object containing the resolvers as dictated
494 * by the new format.
495 * @return {Function} returns the modified Class with the `CHECK_RESOLVERS`
496 * portion ticked off internally.
497 */
498 static injectResolvers(Class: Function, resolvers: Object): Function {
499 if (!hasChecklist(Class, CHECK_SCHEMA)) {
500 throw new Error(customDedent({dropLowest:true})`
501 \`injectResolvers\` cannot be called on a class without a SCHEMA.
502 Please verify your progress in the process and try again.
503 `)
504 }
505
506 let tree = SyntaxTree.from(Class.SCHEMA)
507 let outline = tree ? tree.outline : {}
508
509 if (Class.name in outline && Class.name in resolvers) {
510 let fields = Object.keys(outline[Class.name])
511
512 for (let fieldResolver of fields) {
513 if (!fieldResolver in resolvers[Class.name]) {
514 LL.warn(customDedent({dropLowest: true})`
515 ${fieldResolver} not supplied in resolvers for ${Class.name}
516 `)
517 continue;
518 }
519
520 let prop = resolvers[Class.name][fieldResolver]
521
522 if (prop && typeof prop === 'function') {
523 LL.info('Injecting [fn] %s', fieldResolver)
524 Object.defineProperty(Class.prototype, fieldResolver, {
525 value: prop
526 })
527 }
528 else {
529 LL.info('Injecting [prop] %s', fieldResolver)
530 Properties(fieldResolver)(Class, ['factory-props'])
531 }
532 }
533 }
534
535 for (let [type: string, decorator: Function] of [
536 ['Query', resolver],
537 ['Mutation', mutator],
538 ['Subscription', subscriptor]
539 ]) {
540 let keys = Object.keys(outline[type] || {})
541
542 // $FlowFixMe
543 if (!type in outline || !keys.length) { continue; }
544
545 for (let fnName of keys) {
546 let fn = resolvers[fnName]
547 decorator(Class, fnName, {value: fn})
548 LL.info('Adding %s resolver [%s]', type, fnName)
549 }
550 }
551
552 setChecklist(Class, CHECK_RESOLVERS)
553
554 return Class
555 }
556
557 static injectDocs(Class: Function, docs: Object): Function {
558 if (!hasChecklist(Class, CHECK_SCHEMA, CHECK_RESOLVERS)) {
559 throw new Error(customDedent({dropLowest:true})`
560 \`injectDocs\` cannot be called on a class without a SCHEMA or
561 RESOLVERS defined. Please verify your progress in the process and try
562 again.
563 `)
564 }
565
566 let copyProp = (
567 o: mixed,
568 prop: string | Symbol,
569 to: mixed,
570 as: ?(string | Symbol)
571 ): mixed => {
572 // $FlowFixMe
573 let prototype = o.prototype || Object.getPrototypeOf(o)
574 let descriptor = Object.getOwnPropertyDescriptor(prototype, prop)
575
576 if (!as) {
577 as = prop
578 }
579
580 if (descriptor) {
581 Object.defineProperty(to, as, descriptor)
582 }
583 else {
584 // $FlowFixMe
585 to[as] = o[prop]
586 }
587 }
588
589 // Create an object our future `static apiDocs()` method of our factory
590 // generated class will return
591 let result = {}
592
593 // Setup the constants we will need in this conversion
594 const { TYPE } = this;
595 const {
596 DOC_CLASS, DOC_FIELDS, DOC_QUERY, DOC_MUTATION, DOC_SUBSCRIPTION,
597 DOC_QUERIES, DOC_MUTATIONS, DOC_SUBSCRIPTIONS
598 } = GQLBase
599
600 // This part might get a little meta, so I have provided comments. You are
601 // welcome future me. I hope it helps. This gnarly block should cover all
602 // the descriptions for Query, Mutation, Subscription and the Class we
603 // are creating. Other superfluous
604 for (let [Type, TopLevelConstant, FieldConstants] of [
605 ['Query', DOC_QUERY, DOC_QUERIES],
606 ['Mutation', DOC_MUTATION, DOC_MUTATIONS],
607 ['Subscription', DOC_SUBSCRIPTION, DOC_SUBSCRIPTIONS],
608 [Class.name, DOC_CLASS, DOC_FIELDS]
609 ]) {
610 // One of 'Query', 'Mutation', or 'Subscription'
611 if (docs[Type]) {
612 // If a top level description is present (i.e. Query, Mutation or
613 // Subscription description)
614 if (docs[Type][TYPE]) {
615 copyProp(docs[Type], TYPE, result, TopLevelConstant)
616 }
617
618 // Fetch the properties from the supplied docs object; TYPE Symbols
619 // do not show up in a call to entries which is why it is handled above
620 // $FlowFixMe
621 let entries = Object.entries(docs[Type])
622
623 // If we have entries to document, create an object to hold those
624 // values; i.e. if we have `{ Query: { getPeople: 'desc' } }`, we need
625 // to make sure we have `{ [DOC_QUERIES]: { getPeople: 'desc' } }` in
626 // our result. The object holding getPeople in the end there is defined
627 // below when we have something to copy.
628 if (entries.length) {
629 result[FieldConstants] = {}
630 }
631
632 // For each name value pair defined above, copy its descriptor or base
633 // value if a descriptor isn't available
634 for (let [prop, value] of entries) {
635 copyProp(docs[Type], prop, result[FieldConstants])
636 }
637 }
638 }
639
640 Object.defineProperty(Class, 'apiDocs', {
641 value: function() { return result }
642 })
643
644 setChecklist(Class, CHECK_API_DOCS)
645
646 return Class
647 }
648
649 static build(template: Object): Function {
650 let validationResults = this.validateTemplate(template)
651 let Class = this.generateClass(template.name, template.type || GQLBase)
652
653 if (!Class) {
654 throw new Error(customDedent({dropLowest: true})`
655 LatticeFactory was unable to build your Class from the name and types
656 supplied in your template. You provided the following template. Please
657 look it over and correct any errors before trying again.
658
659 \x1b[1mTemplate\x1b[0m
660 ${_i(template)}
661 `)
662 }
663
664 this.injectSchema(Class, template.schema)
665 this.injectResolvers(Class, template.resolvers || {})
666 this.injectDocs(Class, template.docs || {})
667
668 // Need to fix how auto-props work; for now create one instance...
669 new Class({})
670
671 if (!hasChecklist(Class, CHECK_SCHEMA, CHECK_RESOLVERS, CHECK_API_DOCS)) {
672 let _schema = hasChecklist(Class, CHECK_SCHEMA) ? '✅' : '❌'
673 let _resolvers = hasChecklist(Class, CHECK_RESOLVERS) ? '✅' : '❌'
674 let _apiDocs = hasChecklist(Class, CHECK_API_DOCS) ? '✅' : '❌'
675
676 throw new Error(customDedent({dropLowest: true})`
677 Something went wrong in the process of building the class called
678 ${Class && Class.name || template && template.name || 'Unknown!'},
679 please check the supplied template for errors.
680
681 [ ${_schema} ] Has a SCHEMA defined
682 [ ${_resolvers} ] Has defined RESOLVERS matching the SCHEMA
683 [ ${_apiDocs} ] Has defined API Docs matching the SCHEMA
684
685 \x1b[1mTemplate\x1b[0m
686 ${_i(template)}
687
688 \x1b[1mClass\x1b[0m
689 ${_i(Class)}
690 `)
691 }
692
693 return Class
694 }
695
696 /**
697 * A static helper method to consistently tag, or brand, classes with a
698 * symbol that denotes they were created using the LatticeFactory process.
699 * This is done by setting a `Symbol` on the root of the class or in the
700 * `[META_KEY]` object for classes extending `GQLBase`.
701 *
702 * @method ⌾⠀brandClass
703 * @memberof LatticeFactory
704 * @static
705 *
706 * @param {Function} Class the class to brand with the `FACTORY_CLASS` symbol
707 * @return {Function} returns the Class value passed in
708 */
709 static brandClass(Class: ?Function): ?Function {
710 if (Class) {
711 if (extendsFrom(Class, GQLBase)) {
712 Class[META_KEY][this.FACTORY_CLASS] = true
713 }
714 else {
715 Class[this.FACTORY_CLASS] = true
716 }
717 }
718
719 return Class
720 }
721
722 /**
723 * A static helper to check and see if the supplied class or function was
724 * branded with the `brandClass()` function. This amounts to storing the
725 * boolean true under the property `Class[LatticeFactory.FACTORY_CLASS]` or
726 * `Class[META_KEY][LatticeFacatory.FACTORY_CLASS]` for `GQLBase` extended
727 * classes.
728 *
729 * @method ⌾⠀isFactoryClass
730 * @memberof LatticeFactory
731 * @static
732 *
733 * @param {Function} Class the class to check for `FACTORY_CLASS` branding
734 * @return {boolean} true if the brand exists, false otherwise
735 */
736 static isFactoryClass(Class: Function): boolean {
737 if (Class) {
738 return (extendsFrom(Class, GQLBase)
739 ? !!Class[META_KEY][this.FACTORY_CLASS]
740 : !!Class[this.FACTORY_CLASS]
741 )
742 }
743
744 return false
745 }
746
747 /**
748 * A static helper method to consistently remove any previous tag or brand
749 * applied with `brandClass`, this is done by removing a previously set
750 * `Symbol` on the root of the class or in the `[META_KEY]` object for
751 * classes extending `GQLBase`.
752 *
753 * @method ⌾⠀removeClassBrand
754 * @memberof LatticeFactory
755 * @static
756 *
757 * @param {Function} Class the class to brand with the `FACTORY_CLASS` symbol
758 * @return {Function} returns the Class value passed in
759 */
760 static removeClassBrand(Class: Function): Function {
761 if (Class) {
762 if (extendsFrom(Class, GQLBase)) {
763 delete Class[META_KEY][this.FACTORY_CLASS]
764 }
765 else {
766 delete Class[this.FACTORY_CLASS]
767 }
768 }
769
770 return Class
771 }
772
773 /**
774 * A constant that reports that this class is `'[object LatticeFactory]'`
775 * rather than `'[object Object]'` when introspected with tools such as
776 * `Object.prototype.toString.apply(class)`.
777 *
778 * @memberof LatticeFactory
779 * @type {Symbol}
780 * @static
781 */
782 // $FlowFixMe
783 static get [Symbol.toStringTag]() { return this.name }
784
785 /**
786 * A constant exported as part of LatticeFactory that can be used for
787 * defining documentation for the type itself.
788 *
789 * @memberof LatticeFactory
790 * @type {Symbol}
791 * @static
792 */
793 static get TYPE(): Symbol { return Symbol.for('API Docs Type Constant') }
794
795 /**
796 * A constant exported as part of LatticeFactory that can be used for
797 * identifying classes that were generated with LatticeFactory.
798 *
799 * @memberof LatticeFactory
800 * @type {Symbol}
801 * @static
802 */
803 static get FACTORY_CLASS(): Symbol { return Symbol.for('Factory Class') }
804}
805
806export const isFactoryClass = LatticeFactory.isFactoryClass
807
808// TESTING REPL
809/**
810var { LatticeFactory, getChecklist, hasChecklist, CHECKLIST, CHECK_SCHEMA, CHECK_RESOLVERS } = require('./dist/LatticeFactory'); var { GQLBase, META_KEY, joinLines, SyntaxTree, typeOf } = require('./dist/lattice'); var gql = joinLines, LF = LatticeFactory, TYPE = LF.TYPE;
811var PersonDef = { name: 'Person', schema: gql` enum StatType { PHYSICAL, MENTAL } type Person { name: String stats(type:StatType): Stat } type Query { findPerson(id: ID): Person } `, resolvers: { Query: { findPerson({req, res, next}, {id}) { console.log('find person') } }, Person: { stats({type}) { let { req, res, next} = this.requestData } } }, docs: { StatType: { [TYPE]: `A type of statistic associated with people`, PHYSICAL: `Physical attributes`, MENTAL: `Mental attributes` }, Person: { [TYPE]: `Represents a person`, personId: `Unique id of the person in question`, name: `The name of the person`, stats: `Allows you to query the stats of a person based on type` }, Query: { [TYPE]: 'Top level query desc.', findPerson: `Searches the system for the specified user` } } };
812var Person = LF.build(PersonDef), p = new Person({name: 'Brielle'})
813Person.getProp('stats',true,{requestData:{req:1,res:2,next:3}})
814var Broke = LF.build({name: 'Broke', schema: gql`type Broke {name: String}`, resolvers:{}, docs:{}})
815var t = LF.validateTemplate({name: '', type: GQLBase, resolvers: {}, docs: {}, schema: ''});
816*/