UNPKG

18.6 kBJavaScriptView Raw
1import {
2 AdjacentSchema,
3 GQLBase,
4 GQLEnum,
5 Getters,
6 Setters,
7 Properties,
8 Schema,
9 SchemaUtils,
10 SyntaxTree,
11
12 resolver,
13 mutator,
14 subscriptor,
15
16 META_KEY,
17 MODEL_KEY,
18 AUTO_PROPS,
19 PROPS,
20 GETTERS,
21 SETTERS,
22
23 typeOf,
24 types,
25 DirectTypeManager
26} from '../es6/lattice'
27
28import { parse } from 'graphql'
29import { inspect } from 'util'
30
31const { isOfType } = types;
32
33describe('@AdjacentSchema', () => {
34 @AdjacentSchema(module)
35 class Sample extends GQLBase { }
36
37 it('should add a module getter', () => {
38 expect(Sample.module).toEqual(module);
39 })
40
41 it('should add a SCHEMA property matching ADJACENT_FILE', () => {
42 expect(Sample.SCHEMA).toEqual(GQLBase.ADJACENT_FILE)
43 })
44})
45
46describe('@Getters', () => {
47 @Schema('type Sample {test: String fun: String}')
48 @Getters('test', 'fun')
49 class Sample extends GQLBase { }
50
51 @Schema('type Employee {name: String job: String}')
52 @Getters(['name', 'firstName'], 'job')
53 class Employee extends GQLBase { }
54
55 @Schema('type Person {employee: Employee}')
56 @Getters(['employee', '_emp', Employee])
57 class Person extends GQLBase { }
58
59 @Getters('broken')
60 class InvalidGQLBase extends GQLBase { }
61
62 const test = 'with jest'
63 const fun = 'always'
64 const firstName = 'Jane'
65 const job = 'Engineer'
66 const broken = 'It is just broke'
67 const instance = new Sample({test, fun}, null, {autoProps: false})
68 const employee = new Employee({firstName, job}, null, {autoProps: false})
69 const person = new Person({_emp: {firstName, job}}, null, {autoProps: false})
70
71
72 it('should have a getter for "test"', () => {
73 expect(instance.test).toEqual(test)
74 expect(() => {
75 instance.test = 'Something else'
76 }).toThrow();
77 expect(instance.test).toEqual(test)
78 })
79
80 it('should allow for remapping between type fields and model fields', () => {
81 expect(employee.name).toEqual(firstName)
82 expect(employee.job).toEqual(job);
83 })
84
85 it('should return an actual Employee object', () => {
86 expect(typeOf(person.employee)).toEqual(Employee.name)
87 expect(person.employee.name).toEqual(firstName)
88 })
89
90 it('should throw an error due to a missing SCHEMA', () => {
91 expect(() => {
92 const invalid = new InvalidGQLBase({broken}, null, {autoProps: false})
93 invalid.broken
94 }).toThrow()
95 })
96
97 it('should have appropriately defined GETTERS tags', async () => {
98 const sampleGetters = Object.keys(Sample[META_KEY][GETTERS])
99 const sampleFields = ['test', 'fun']
100 const employeeGetters = Object.keys(Employee[META_KEY][GETTERS])
101 const employeeFields = ['name', 'job']
102 const personGetters = Object.keys(Person[META_KEY][GETTERS])
103 const personFields = ['employee']
104
105 expect(sampleGetters).toEqual(expect.arrayContaining(sampleFields))
106 expect(employeeGetters).toEqual(expect.arrayContaining(employeeFields))
107 expect(personGetters).toEqual(expect.arrayContaining(personFields))
108 })
109})
110
111describe('@Setters', () => {
112 @Schema('type Sample {test: String fun: String}')
113 @Setters('test', 'fun')
114 class Sample extends GQLBase { }
115
116 @Schema('type Employee {name: String job: String}')
117 @Setters(['name', 'firstName'], 'job')
118 class Employee extends GQLBase { }
119
120 @Schema('type Person {employee: Employee}')
121 @Setters(['employee', '_emp', Employee])
122 class Person extends GQLBase { }
123
124 const test = 'with jest'
125 const fun = 'always'
126 const firstName = 'Brielle'
127 const job = 'Engineer'
128 const instance = new Sample({test, fun}, null, {autoProps: false})
129 const employee = new Employee({firstName, job}, null, {autoProps: false})
130
131 it('should have a setter for "test"', () => {
132 expect(() => {
133 instance.test = 'Something else'
134 }).not.toThrow();
135 expect(instance.test).toBeUndefined()
136 })
137
138 it('should allow for remapping between type fields and model fields', () => {
139 expect(() => {
140 employee.name = 'Dorkis'
141 employee.job = 'Vendor'
142 }).not.toThrow()
143
144 expect(employee.model.firstName).toEqual('Dorkis')
145 expect(employee.model.job).toEqual('Vendor');
146 })
147
148 it('should not break if we create and set a complex type to null', () => {
149 expect(() => {
150 const emptyPerson = new Person({_emp: null}, null, {autoProps: false})
151
152 emptyPerson.employee = null;
153 }).not.toThrow()
154 })
155
156 it('should have appropriately defined SETTERS tags', async () => {
157 const sampleSetters = Object.keys(Sample[META_KEY][SETTERS])
158 const sampleFields = ['test', 'fun']
159 const employeeSetters = Object.keys(Employee[META_KEY][SETTERS])
160 const employeeFields = ['name', 'job']
161 const personSetters = Object.keys(Person[META_KEY][SETTERS])
162 const personFields = ['employee']
163
164 expect(sampleSetters).toEqual(expect.arrayContaining(sampleFields))
165 expect(employeeSetters).toEqual(expect.arrayContaining(employeeFields))
166 expect(personSetters).toEqual(expect.arrayContaining(personFields))
167 })
168})
169
170describe('@Properties', () => {
171 @Schema('type Sample {test: String fun: String}')
172 @Properties('test', 'fun')
173 class Sample extends GQLBase { }
174
175 @Schema('type Employee {name: String job: String}')
176 @Properties(['name', 'firstName'], 'job')
177 class Employee extends GQLBase { }
178
179 @Schema('type Person {employee: Employee}')
180 @Properties(['employee', '_emp', Employee])
181 class Person extends GQLBase { }
182
183 @Getters('broken')
184 class InvalidGQLBase extends GQLBase { }
185
186 const test = 'with jest'
187 const fun = 'always'
188 const firstName = 'Brielle'
189 const job = 'Engineer'
190 const broken = 'It is just broke'
191 const instance = new Sample({test, fun}, null, {autoProps: false})
192 const employee = new Employee({firstName, job}, null, {autoProps: false})
193
194 it('should have a setter for "test"', () => {
195 expect(instance.test).toEqual(test)
196 expect(() => {
197 instance.test = 'Something else'
198 }).not.toThrow();
199 expect(instance.test).not.toBeUndefined()
200 })
201
202 it('should allow for remapping between type fields and model fields', () => {
203 expect(employee.name).toEqual(firstName);
204 expect(employee.job).toEqual(job);
205 expect(() => {
206 employee.name = 'Dorkis'
207 employee.job = 'Vendor'
208 }).not.toThrow()
209
210 expect(employee.name).toEqual('Dorkis')
211 expect(employee.job).toEqual('Vendor');
212 })
213
214 it('should throw due to a missing SCHEMA', () => {
215 expect(() => {
216 const invalid = new InvalidGQLBase({broken}, null, {autoProps: false})
217 invalid.broken
218 }).toThrow()
219 })
220
221 it('should be able to create a GQL object with a null complex type', () => {
222 let emptyPerson;
223
224 expect(() => {
225 emptyPerson = new Person({_emp: null}, null, {autoProps: false})
226 }).not.toThrow();
227
228 expect(() => {
229 emptyPerson.employee = null;
230 }).not.toThrow();
231
232 emptyPerson[MODEL_KEY]._emp = {name: 'Bubba', job: 'Monster Hunter'};
233 expect(typeOf(emptyPerson.employee)).toEqual(Employee.name)
234 })
235
236 it('should have appropriately defined PROPS tags', async () => {
237 const sampleProps = Object.keys(Sample[META_KEY][PROPS])
238 const sampleFields = ['test', 'fun']
239 const employeeProps = Object.keys(Employee[META_KEY][PROPS])
240 const employeeFields = ['name', 'job']
241 const personProps = Object.keys(Person[META_KEY][PROPS])
242 const personFields = ['employee']
243
244 expect(sampleProps).toEqual(expect.arrayContaining(sampleFields))
245 expect(employeeProps).toEqual(expect.arrayContaining(employeeFields))
246 expect(personProps).toEqual(expect.arrayContaining(personFields))
247 })
248})
249
250describe('@Schema', () => {
251 const schema = `
252 type Sample {
253 name: String!
254 id: ID
255 }
256 `
257
258 @Schema(schema)
259 class Sample extends GQLBase { }
260
261 let instance = new Sample(undefined, null, {autoProps: false})
262
263 it('should have a schema matching ours', () => {
264 expect(Sample.SCHEMA).toEqual(schema);
265 })
266
267 it('should have a non-nullable name', () => {
268 let { meta } = SyntaxTree.findField(
269 parse(Sample.SCHEMA), Sample.name, 'name'
270 );
271
272 expect(meta.nullable).toEqual(false)
273 expect(meta.type).not.toEqual(null)
274 })
275
276 it('should have a nullable id', () => {
277 let { meta } = SyntaxTree.findField(
278 parse(Sample.SCHEMA), Sample.name, 'id'
279 );
280
281 expect(meta.nullable).toEqual(true)
282 expect(meta.type).not.toEqual(null)
283 })
284})
285
286describe('DIRECT_TYPES', () => {
287 @Schema(/* GraphQL */`type Job { company: String }`)
288 class Job extends GQLBase {
289 get company() { return answer5 }
290 }
291 @Schema(/* GraphQL */`type Person { name: String job: Job }`)
292 @Getters(['name', String], ['job', Job])
293 class Person extends GQLBase { }
294
295 const answer1 = 'Harrison, Brielle'
296 const answer2 = '5'
297 const answer3 = 'David'
298 const answer4 = 'Sourceress'
299 const answer5 = `The greatest ${answer4}`
300 const job = new Job({ company: answer4 })
301 const model1 = { name: answer1, job: { company: answer4 } }
302 const model2 = { name: answer1, job: job }
303
304 let peep;
305
306 it('should allow using String to coerce an object with toString()', () => {
307 peep = new Person({name: {
308 get first() { return 'Brielle' },
309 get last() { return 'Harrison'},
310
311 toString() { return `${this.last}, ${this.first}` }
312 }}, null, {autoProps: false})
313 expect(peep.name).toBe(answer1)
314 })
315
316 it('should run any value for name through as a String', () => {
317 peep = new Person({name: 5}, null, {autoProps: false})
318 expect(5).not.toBe(answer2);
319 expect(peep.name).toBe(answer2);
320 })
321
322 it('should not coerce values if String is removed from DIRECT_TYPES', () => {
323 DirectTypeManager.clear();
324 expect(DirectTypeManager.types.length).toEqual(0);
325
326 peep = new Person({name: answer3}, null, {autoProps: false})
327 expect(typeOf(peep.name)).toBe(String.name)
328
329 // This is due to how `new String(...)` and `String(...)` differ. The use
330 // of DIRECT_TYPES is directly related to this inconsistency.
331 expect(new String(answer3)).not.toBe(answer3)
332 expect(String(answer3)).toBe(answer3)
333
334 DirectTypeManager.reset();
335 })
336
337 it('should give me a job type when given model data for a Job', () => {
338 const peep = new Person(model1, null, {autoProps: false})
339
340 expect(typeOf(peep.job)).toBe(Job.name)
341 expect(peep.job).not.toBe(job)
342 expect(peep.name).toEqual(answer1)
343 expect(peep.model.job.model).not.toBeDefined()
344 expect(peep.model.job.company).toEqual(answer4)
345 expect(peep.job.company).toEqual(answer5)
346 expect(typeof peep.model.job).toEqual('object')
347 expect(typeOf(peep.model.job)).toEqual(Object.name)
348 })
349
350 it('should give me the job type when given a model with one already', () => {
351 const peep = new Person(model2, null, {autoProps: false})
352
353 expect(typeOf(peep.job)).toBe(Job.name)
354 expect(peep.job === model2.job).toBe(true);
355 expect(peep.job).toEqual(job)
356 expect(peep.name).toEqual(answer1)
357 expect(peep.model.job.model.company).toEqual(answer4)
358 expect(peep.model.job.company).toEqual(answer5)
359 expect(peep.job.company).toEqual(answer5)
360 expect(typeof peep.model.job).toEqual('object')
361 expect(typeOf(peep.model.job)).not.toEqual(Object.name)
362 expect(typeOf(peep.model.job)).toEqual(Job.name)
363 })
364})
365
366describe('@resolver/@mutator/@subscriptor', () => {
367 @Schema(`
368 type Thing {
369 name: String
370 }
371
372 type Query {
373 getThing: Thing
374 traditionalResolver: Thing
375 asyncResolver: Thing
376 }
377
378 type Mutation {
379 changeThing(thingName: String): Thing
380 }
381
382 type Subscription {
383 watchThing(thingName: String): Thing
384 }
385 `)
386 @Properties('name')
387 class Thing extends GQLBase {
388 @resolver getThing(requestData, thingName = 'Jane Doe') {
389 // Potentially do something with requestData
390 return new Thing({name: thingName}, requestData)
391 }
392
393 @mutator changeThing(requestData, thingName) {
394 return new Thing({name: 'Changed Name'}, requestData)
395 }
396
397 @subscriptor watchThing(requestData, thingName) {
398 return new Thing({name: 'Watched Thing'}, requestData)
399 }
400
401 @resolver async asyncResolver(requestData) {
402 return new Thing({}, requestData)
403 }
404
405 @resolver static staticResolver(requestData) {
406 return new Thing({}, requestData)
407 }
408
409 @resolver get invalidResolver() {
410 return true;
411 }
412
413 @resolver
414 invalidPropertyResolver;
415
416 static async RESOLVERS(requestData) {
417 return {
418 // Potentially do something with requestData
419 traditionalResolver(thingName = 'Jane Doe') {
420 return new Thing({name: thingName}, requestData)
421 }
422 };
423 }
424 }
425
426 const express = {
427 req: {},
428 res: {},
429 next: function() {}
430 }
431
432 it('should have our getThing resolver', async () => {
433 let root = await SchemaUtils.createMergedRoot([Thing], express);
434
435 expect(root.getThing).toBeDefined()
436 })
437
438 it('should have our traditionalResolver resolver', async () => {
439 let root = await SchemaUtils.createMergedRoot([Thing], express);
440
441 expect(root.traditionalResolver).toBeDefined()
442 });
443
444 it('should have our requestData in both the old and new ways', async () => {
445 let root = await SchemaUtils.createMergedRoot([Thing], express);
446 let newWay;
447 let oldWay;
448
449 expect(root.getThing).toBeDefined()
450 newWay = root.getThing('ball')
451 expect(newWay.requestData).toBe(express)
452 expect(newWay.name).toBe('ball')
453
454 expect(root.traditionalResolver).toBeDefined()
455 oldWay = root.traditionalResolver('basket')
456 expect(oldWay.requestData).toBe(express)
457 expect(oldWay.name).toBe('basket')
458 })
459
460 it('should have moved our function out of the prototype', () => {
461 let thingInstance = new Thing({name: 'Jane Doe'})
462
463 expect(thingInstance.getThing).not.toBeDefined()
464 })
465
466 it ('should have ignored our invalidResolver getter', () => {
467 let thingInstance = new Thing({name: 'A thing'})
468
469 expect(thingInstance.invalidResolver).toBe(true)
470 })
471
472 it ('should have ignored our invalidPropertyResolver', () => {
473 let thingInstance = new Thing({name: 'A thing'})
474
475 expect(thingInstance.invalidPropertyResolver).not.toBeDefined()
476 })
477
478 it('should also not be available in the static scope', () => {
479 expect(Thing.getThing).not.toBeDefined()
480 })
481
482 it('should also contain our mutators and subscriptors', async () => {
483 let root = await SchemaUtils.createMergedRoot([Thing], express);
484
485 expect(root.changeThing).toBeDefined()
486 expect(root.watchThing).toBeDefined()
487 })
488
489 it('should work fine with async decorated functions', async () => {
490 let root = await SchemaUtils.createMergedRoot([Thing], express);
491 let obj;
492
493 expect(root.asyncResolver).toBeDefined();
494
495 obj = await root.asyncResolver()
496 expect(isOfType(obj, Thing)).toBe(true)
497 expect(obj.requestData).toBe(express)
498 })
499
500 it('should not care if the decorated function is static', async () => {
501 let root = await SchemaUtils.createMergedRoot([Thing], express);
502
503 expect(root.staticResolver).toBeDefined()
504 })
505})
506
507describe('Auto properties testing', () => {
508 @Schema(/* GraphQL */`
509 type Contrived {
510 name: String
511 job: String
512 age: Int
513 }
514 `)
515 class Contrived extends GQLBase {
516 job() {
517 return 'Sourceress'
518 }
519 }
520
521 @Schema(/* GraphQL */`
522 type Special {
523 id: ID
524 name(surname:String): String
525 ooh: Contrived
526 locale: String
527 }
528 `)
529 @Properties(['ooh', Contrived])
530 class Special extends GQLBase {
531 async name({surname}) {
532 return `My name is ${surname}`
533 }
534
535 get locale() {
536 return 'ja_JP'
537 }
538 }
539
540 @Schema(/* GraphQL */`
541 enum Car { SLOW, FAST, RED_ONE }
542 `)
543 class Car extends GQLEnum {}
544
545 it('should have appropriately defined AUTO_PROPS tags', async () => {
546 // TODO move applyAutoProps to a non-instance locaation
547 new Contrived({})
548 new Special({})
549
550 let contrivedProps = Object.keys(Contrived[META_KEY][AUTO_PROPS])
551 let contrivedFields = ['name', 'age']
552 let specialProps = Object.keys(Special[META_KEY][AUTO_PROPS])
553 let specialFields = ['id']
554
555 expect(contrivedProps).toEqual(expect.arrayContaining(contrivedFields))
556 expect(specialProps).toEqual(expect.arrayContaining(specialFields))
557 })
558
559 it('should make instances that return Sourceresses', async () => {
560 let instance = new Contrived({name: 'Brie', job: 'Engineer', age: 21})
561 let props = Contrived[META_KEY][PROPS]
562 let autoProps = Contrived[META_KEY][AUTO_PROPS]
563 let expected = ['name', 'age']
564
565 expect(Object.keys(props)).toEqual(expect.arrayContaining(expected))
566 expect(autoProps.name).toBeDefined()
567 expect(autoProps.age).toBeDefined()
568
569 expect(await instance.callProp('name')).toBe('Brie')
570 expect(await instance.callProp('age')).toBe(21)
571
572 // Note that we have a custom property resolver for 'job' that returns
573 // the value 'Sourceress' and does not look at the model value 'Engineer'
574 expect(await instance.callProp('job')).toBe('Sourceress')
575 })
576
577 it('should not create auto-props for enums', async () => {
578 let slowCar = new Car('SLOW')
579 let fastCar = new Car('FAST')
580 let redCar = new Car({ value: 'RED_ONE' })
581 let props = Car[META_KEY].props
582 let autoProps = Car[META_KEY][AUTO_PROPS]
583
584 expect(props).not.toEqual(expect.arrayContaining(['SLOW']))
585 expect(props).not.toEqual(expect.arrayContaining(['FAST']))
586 expect(props).not.toEqual(expect.arrayContaining(['RED_ONE']))
587
588 expect(autoProps).toBeUndefined()
589
590 expect(slowCar.SLOW).toBeUndefined()
591 expect(slowCar.FAST).toBeUndefined()
592 expect(slowCar.RED_ONE).toBeUndefined()
593 expect(fastCar.SLOW).toBeUndefined()
594 expect(fastCar.FAST).toBeUndefined()
595 expect(fastCar.RED_ONE).toBeUndefined()
596 expect(redCar.SLOW).toBeUndefined()
597 expect(redCar.FAST).toBeUndefined()
598 expect(redCar.RED_ONE).toBeUndefined()
599 })
600
601 it('should not create auto-props for custom implementations', async () => {
602 let special = new Special({
603 id: 'XELOK',
604 name: 'ignored',
605 ooh: {
606 name: 'Thing',
607 job: 'To be a thing',
608 age: 1
609 }
610 })
611 let autoProps = Special[META_KEY][AUTO_PROPS]
612 let props = Special[META_KEY][PROPS]
613
614 expect(special.id).toEqual('XELOK')
615 expect(await special.name({surname: 'Brie'})).toEqual('My name is Brie')
616 expect(special.ooh.name).toEqual('Thing')
617 expect(special.locale).toEqual('ja_JP')
618
619 expect(autoProps.ooh).toBeUndefined()
620 expect(autoProps.name).toBeUndefined()
621 expect(autoProps.locale).toBeUndefined()
622 expect(autoProps.id).toBeDefined()
623
624 expect(Object.keys(props)).toEqual(expect.arrayContaining(['ooh', 'id']))
625 expect(Object.keys(autoProps)).not.toEqual(expect.arrayContaining(['ooh']))
626 })
627})