1 | /**
|
2 | @namespace GQLInterface
|
3 | @flow
|
4 | */
|
5 |
|
6 | import { GQLBase } from './GQLBase'
|
7 | import { GraphQLEnumType, parse } from 'graphql'
|
8 | import { Getters } from './decorators/ModelProperties'
|
9 | import { LatticeLogs as ll } from './utils'
|
10 |
|
11 | /* Internal Symbol referring to real accessor to GQLBase model object */
|
12 | const _MODEL_KEY = Symbol.for('data-model-contents-value');
|
13 |
|
14 | /* Internal Symbol referring to the static object containing a proxy handler */
|
15 | const _PROXY_HANDLER = Symbol.for('internal-base-proxy-handler')
|
16 |
|
17 | /* Internal Symbol property referring to the mapping of values on the GQLEnum */
|
18 | const 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')
|
36 | export 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 | }
|