UNPKG

9.58 kBJavaScriptView Raw
1/**
2 @namespace GQLInterface
3 @flow
4 */
5
6import { GQLBase } from './GQLBase'
7import { GraphQLEnumType, parse } from 'graphql'
8import { Getters } from './decorators/ModelProperties'
9import { LatticeLogs as ll } from './utils'
10
11/* Internal Symbol referring to real accessor to GQLBase model object */
12const _MODEL_KEY = Symbol.for('data-model-contents-value');
13
14/* Internal Symbol referring to the static object containing a proxy handler */
15const _PROXY_HANDLER = Symbol.for('internal-base-proxy-handler')
16
17/* Internal Symbol property referring to the mapping of values on the GQLEnum */
18const ENUMS = Symbol();
19
20/**
21 * GraphQL Enum types can be a bit picky when it comes to how scalar types
22 * equate to enum values. Lattice makes this easier by allowing you to specify
23 * a value or the key when your enum has a value other than the key; GraphQL
24 * does not allow this by default.
25 *
26 * Further more, when instantiating a GQLEnum type, you can pass a string or
27 * value matching the enum key or value or you can pass an object with key of
28 * value and the value being either the enum key or value. If any of those
29 * things match, then your `instance.value` will equate to the enum's key. If,
30 * on the other hand, your supplied values do not match then `instance.value`
31 * will be `null`.
32 *
33 * @class GQLEnum
34 */
35@Getters('symbol')
36export class GQLEnum extends GQLBase {
37 constructor(enumValueOrKey: ?Object, requestData: ?Object) {
38 super({}, requestData)
39
40 const Class = this.constructor
41 const enums = Class.enums;
42 let symbol;
43 let enumVK: (Object | string | null) = enumValueOrKey || null
44
45 // @ComputedType
46 symbol = enums[enumVK] || enumVK && enums[enumVK.value] || null
47
48 Object.assign(this.getModel(), {
49 name: symbol ? symbol.name : null,
50 value: symbol ? symbol.value : null,
51 symbol: symbol ? symbol : null
52 })
53 }
54
55 /**
56 * Retrieves the actual symbol stored name property from the internal
57 * model object for this enum instance. That is a mouthfull, but it
58 * basically means that if your enum is something like:
59 *
60 * ```
61 * enum Person { TALL, SHORT }
62 * ```
63 *
64 * and you create an instance using any of the following
65 *
66 * ```
67 * p = new Person('TALL')
68 * p = new Person(valueFor('TALL'))
69 * p = new Person({value: 'TALL'})
70 * ```
71 *
72 * that your response to `p.name` will equate to `TALL`.
73 *
74 * @method ⬇︎⠀name
75 * @memberof GQLEnum
76 * @return {mixed} typically a String but any valid type supplied
77 */
78 get name(): mixed {
79 const name = this.getModel().name
80
81 return (
82 name !== undefined &&
83 name !== null &&
84 name !== NaN
85 ) ? name : null;
86 }
87
88 /**
89 * Much like the `.name` getter, the `.value` getter will typically
90 * retreive the name of the enum key you are requesting. In rare cases
91 * where you have defined values that differ from the name, the `.value`
92 * getter will retrieve that custom value from the `.value` property on
93 * the symbol in question.
94 *
95 * This should do the right thing even if you instantiated the instance
96 * using the name.
97 *
98 * @memberof GQLEnum
99 * @method ⬇︎⠀value
100 * @return {mixed} the value of the enum type; this in all likihood should
101 * be a String or potentially an object
102 */
103 get value(): mixed {
104 const value = this.getModel().value
105
106 return (
107 value !== undefined &&
108 value !== null &&
109 value !== NaN
110 ) ? value : null;
111 }
112
113 /**
114 * Determines the default type targeted by this GQLBase class. Any
115 * type will technically be valid but only will trigger special behavior
116 *
117 * @memberof GQLEnum
118 * @method ⬇︎⠀GQL_TYPE
119 * @static
120 * @const
121 *
122 * @return {Function} a type, such as `GraphQLObjectType` or
123 * `GraphQLInterfaceType`
124 */
125 static get GQL_TYPE(): Function {
126 return GraphQLEnumType;
127 }
128
129 /**
130 * Each instance of GQLEnum must specify a map of keys and values. If this
131 * method returns null or is not defined, the value of the enum will match
132 * the name of the enum as per the reference implementation.
133 *
134 * Example:
135 * ```
136 * static get values(): ?Object {
137 * const { valueOf } = this;
138 *
139 * return {
140 * NAME: valueOf(value)
141 * }
142 * }
143 * ```
144 *
145 * @method ⬇︎⠀values
146 * @memberof GQLEnum
147 * @static
148 *
149 * @return {Object|Null} an object mapping with each key mapping to an object
150 * possessing at least a value field, which in turn maps to the desired value
151 */
152 static get values(): Object {
153 return {};
154 }
155
156 /**
157 * Shorthand method to generate a GraphQLEnumValueDefinition implementation
158 * object. Use this for building and customizing your `values` key/value
159 * object in your child classes.
160 *
161 * @memberof GQLEnum
162 * @method valueFor
163 * @static
164 *
165 * @param {mixed} value any nonstandard value you wish your enum to have
166 * @param {String} deprecationReason an optional reason to deprecate an enum
167 * @param {String} description a non Lattice standard way to write a comment
168 * @return {Object} an object that conforms to the GraphQLEnumValueDefinition
169 * defined here http://graphql.org/graphql-js/type/#graphqlenumtype
170 */
171 static valueFor(
172 value: mixed,
173 deprecationReason: ?string,
174 description: ?string
175 ): Object {
176 const result: Object = { value }
177
178 if (deprecationReason) { result.deprecationReason = deprecationReason }
179 if (description) { result.description = description }
180
181 return result;
182 }
183
184 /**
185 * For easier use within JavaScript, the static enums method provides a
186 * Symbol backed solution for each of the enums defined. Each `Symbol`
187 * instance is wrapped in Object so as to allow some additional properties
188 * to be written to it.
189 *
190 * @memberof GQLEnum
191 * @method ⬇︎⠀enums
192 * @static
193 *
194 * @return {Array<Symbol>} an array of modified Symbols for each enum
195 * variation defined.
196 */
197 static get enums(): Array<Symbol> {
198 // @ComputedType
199 if (!this[ENUMS]) {
200 const map: Map<*,*> = new Map();
201 const ast = parse((this.SCHEMA: any));
202 const array = new Proxy([], GQLEnum.GenerateEnumsProxyHandler(map));
203 const values = this.values || {};
204 let astValues: Array<any>;
205
206 try {
207 // TODO: $FlowFixMe
208 astValues = ast.definitions[0].values;
209 }
210 catch (error) {
211 ll.error('Unable to discern the values from your enums SCHEMA')
212 ll.error(error)
213 throw error;
214 }
215
216 // Walk the AST for the class' schema and extract the names (same as
217 // values when specified in GraphQL SDL) and build an object the has
218 // the actual defined value and the AST generated name/value.
219 for (let enumDef of astValues) {
220 let defKey = enumDef.name.value;
221 let symObj: Object = Object(Symbol.for(defKey));
222
223 symObj.value = (values[defKey] && values[defKey].value) || defKey;
224 symObj.name = defKey
225 symObj.sym = symObj.valueOf()
226
227 map.set(symObj.name, symObj)
228 map.set(symObj.value, symObj)
229
230 // This bit of logic allows us to look into the "enums" property and
231 // get the generated Object wrapped Symbol with keys and values by
232 // supplying either a key or value.
233 array.push(symObj)
234 }
235
236 // @ComputedType
237 this[ENUMS] = array;
238 }
239
240 // @ComputedType
241 return this[ENUMS];
242 }
243
244 /**
245 * Due to the complexity of being able to access both the keys and values
246 * properly for an enum type, a Map is used as the backing store. The handler
247 * returned by this method is to be passed to a Proxy.
248 *
249 * @method GQLEnum#GenerateEnumsProxyHandler
250 * @static
251 *
252 * @param {Map} map the map containing the key<->value and
253 * value<->key mappings; the true storage backing the array in question.
254 * @return {Object}
255 */
256 static GenerateEnumsProxyHandler(map: Map<*, *>) {
257 return {
258 /**
259 * Get handler for the Map backed Array Proxy
260 *
261 * @memberof! GQLEnum
262 * @method get
263 *
264 * @param {mixed} obj the object targeted by the Proxy
265 * @param {string} key `key` of the value being requested
266 * @return {mixed} the `value` being requested
267 */
268 get(obj, key) {
269 if (map.has(key)) {
270 return map.get(key)
271 }
272
273 return obj[key]
274 },
275
276 /**
277 * Set handler for the Map backed Array Proxy.
278 *
279 * @memberof! GQLEnum
280 * @method set
281 *
282 * @param {mixed} obj the object the Proxy is targeting
283 * @param {string} key a string `key` being set
284 * @param {mixed} value the `value` being assigned to `key`
285 */
286 set(obj, key, value) {
287 if (isFinite(key) && value instanceof Symbol) {
288 map.set(value.name, value)
289 map.set(value.value, value)
290 }
291
292 // Some accessor on the receiving array
293 obj[key] = value;
294
295 // Arrays return length when pushing. Assume value as return
296 // otherwise. ¯\_(ツ)_/¯
297 return isFinite(key) ? obj.length : obj[key];
298 }
299 }
300 }
301
302 /** @inheritdoc */
303 static apiDocs(): Object {
304 const { DOC_CLASS, DOC_FIELDS, joinLines } = this;
305
306 return {
307 [DOC_CLASS]: joinLines`
308 GQLEnums allow the definition of enum types with description fields
309 and values other than a 1:1 mapping of their types and their type
310 names. If you are reading this, the implementor likely did not
311 contribute comments for their type.
312 `
313 }
314 }
315}