1const { inspect } = require('util')
2const t = require('tap')
3const Queryable = require('../../../lib/utils/queryable.js')
4
5t.test('retrieve single nested property', async t => {
6  const fixture = {
7    foo: {
8      bar: 'bar',
9      baz: 'baz',
10    },
11    lorem: {
12      ipsum: 'ipsum',
13    },
14  }
15  const q = new Queryable(fixture)
16  const query = 'foo.bar'
17  t.strictSame(q.query(query), { [query]: 'bar' },
18    'should retrieve property value when querying for dot-sep name')
19})
20
21t.test('query', async t => {
22  const fixture = {
23    o: 'o',
24    single: [
25      'item',
26    ],
27    w: [
28      'a',
29      'b',
30      'c',
31    ],
32    list: [
33      {
34        name: 'first',
35      },
36      {
37        name: 'second',
38      },
39    ],
40    foo: {
41      bar: 'bar',
42      baz: 'baz',
43    },
44    lorem: {
45      ipsum: 'ipsum',
46      dolor: [
47        'a',
48        'b',
49        'c',
50        {
51          sit: [
52            'amet',
53          ],
54        },
55      ],
56    },
57    a: [
58      [
59        [
60          {
61            b: [
62              [
63                {
64                  c: 'd',
65                },
66              ],
67            ],
68          },
69        ],
70      ],
71    ],
72  }
73  const q = new Queryable(fixture)
74  t.strictSame(
75    q.query(['foo.baz', 'lorem.dolor[0]']),
76    {
77      'foo.baz': 'baz',
78      'lorem.dolor[0]': 'a',
79    },
80    'should retrieve property values when querying for multiple dot-sep names')
81  t.strictSame(
82    q.query('lorem.dolor[3].sit[0]'),
83    {
84      'lorem.dolor[3].sit[0]': 'amet',
85    },
86    'should retrieve property from nested array items')
87  t.strictSame(
88    q.query('a[0][0][0].b[0][0].c'),
89    {
90      'a[0][0][0].b[0][0].c': 'd',
91    },
92    'should retrieve property from deep nested array items')
93  t.strictSame(
94    q.query('o'),
95    {
96      o: 'o',
97    },
98    'should retrieve single level property value')
99  t.strictSame(
100    q.query('list.name'),
101    {
102      'list[0].name': 'first',
103      'list[1].name': 'second',
104    },
105    'should automatically expand arrays')
106  t.strictSame(
107    q.query(['list.name']),
108    {
109      'list[0].name': 'first',
110      'list[1].name': 'second',
111    },
112    'should automatically expand multiple arrays')
113  t.strictSame(
114    q.query('w'),
115    {
116      w: ['a', 'b', 'c'],
117    },
118    'should return arrays')
119  t.strictSame(
120    q.query('single'),
121    {
122      single: 'item',
123    },
124    'should return single item')
125  t.strictSame(
126    q.query('missing'),
127    undefined,
128    'should return undefined')
129  t.strictSame(
130    q.query('missing[bar]'),
131    undefined,
132    'should return undefined also')
133  t.throws(() => q.query('lorem.dolor[]'),
134    { code: 'EINVALIDSYNTAX' },
135    'should throw if using empty brackets notation'
136  )
137  t.throws(() => q.query('lorem.dolor[].sit[0]'),
138    { code: 'EINVALIDSYNTAX' },
139    'should throw if using nested empty brackets notation'
140  )
141
142  const qq = new Queryable({
143    foo: {
144      bar: 'bar',
145    },
146  })
147  t.strictSame(
148    qq.query(''),
149    {
150      '': {
151        foo: {
152          bar: 'bar',
153        },
154      },
155    },
156    'should return an object with results in an empty key'
157  )
158})
159
160t.test('missing key', async t => {
161  const fixture = {
162    foo: {
163      bar: 'bar',
164    },
165  }
166  const q = new Queryable(fixture)
167  const query = 'foo.missing'
168  t.equal(q.query(query), undefined,
169    'should retrieve no results')
170})
171
172t.test('no data object', async t => {
173  t.throws(
174    () => new Queryable(),
175    { code: 'ENOQUERYABLEOBJ' },
176    'should throw ENOQUERYABLEOBJ error'
177  )
178  t.throws(
179    () => new Queryable(1),
180    { code: 'ENOQUERYABLEOBJ' },
181    'should throw ENOQUERYABLEOBJ error'
182  )
183})
184
185t.test('get values', async t => {
186  const q = new Queryable({
187    foo: {
188      bar: 'bar',
189    },
190  })
191  t.equal(q.get('foo.bar'), 'bar', 'should retrieve value')
192  t.equal(q.get('missing'), undefined, 'should return undefined')
193})
194
195t.test('set property values', async t => {
196  const fixture = {
197    foo: {
198      bar: 'bar',
199    },
200  }
201  const q = new Queryable(fixture)
202  q.set('foo.baz', 'baz')
203  t.strictSame(
204    q.toJSON(),
205    {
206      foo: {
207        bar: 'bar',
208        baz: 'baz',
209      },
210    },
211    'should add new property and its assigned value'
212  )
213  q.set('foo[lorem.ipsum]', 'LOREM IPSUM')
214  t.strictSame(
215    q.toJSON(),
216    {
217      foo: {
218        bar: 'bar',
219        baz: 'baz',
220        'lorem.ipsum': 'LOREM IPSUM',
221      },
222    },
223    'should be able to set square brackets props'
224  )
225  q.set('a.b[c.d]', 'omg')
226  t.strictSame(
227    q.toJSON(),
228    {
229      foo: {
230        bar: 'bar',
231        baz: 'baz',
232        'lorem.ipsum': 'LOREM IPSUM',
233      },
234      a: {
235        b: {
236          'c.d': 'omg',
237        },
238      },
239    },
240    'should be able to nest square brackets props'
241  )
242  q.set('a.b[e][f.g][1.0.0]', 'multiple')
243  t.strictSame(
244    q.toJSON(),
245    {
246      foo: {
247        bar: 'bar',
248        baz: 'baz',
249        'lorem.ipsum': 'LOREM IPSUM',
250      },
251      a: {
252        b: {
253          'c.d': 'omg',
254          e: {
255            'f.g': {
256              '1.0.0': 'multiple',
257            },
258          },
259        },
260      },
261    },
262    'should be able to nest multiple square brackets props'
263  )
264  q.set('a.b[e][f.g][2.0.0].author.name', 'Ruy Adorno')
265  t.strictSame(
266    q.toJSON(),
267    {
268      foo: {
269        bar: 'bar',
270        baz: 'baz',
271        'lorem.ipsum': 'LOREM IPSUM',
272      },
273      a: {
274        b: {
275          'c.d': 'omg',
276          e: {
277            'f.g': {
278              '1.0.0': 'multiple',
279              '2.0.0': {
280                author: {
281                  name: 'Ruy Adorno',
282                },
283              },
284            },
285          },
286        },
287      },
288    },
289    'should be able to use dot-sep notation after square bracket props'
290  )
291  q.set('a.b[e][f.g][2.0.0].author[url]', 'https://npmjs.com')
292  t.strictSame(
293    q.toJSON(),
294    {
295      foo: {
296        bar: 'bar',
297        baz: 'baz',
298        'lorem.ipsum': 'LOREM IPSUM',
299      },
300      a: {
301        b: {
302          'c.d': 'omg',
303          e: {
304            'f.g': {
305              '1.0.0': 'multiple',
306              '2.0.0': {
307                author: {
308                  name: 'Ruy Adorno',
309                  url: 'https://npmjs.com',
310                },
311              },
312            },
313          },
314        },
315      },
316    },
317    'should be able to have multiple, separated, square brackets props'
318  )
319  q.set('a.b[e][f.g][2.0.0].author[foo][bar].lorem.ipsum[dolor][sit][amet].omg', 'O_O')
320  t.strictSame(
321    q.toJSON(),
322    {
323      foo: {
324        bar: 'bar',
325        baz: 'baz',
326        'lorem.ipsum': 'LOREM IPSUM',
327      },
328      a: {
329        b: {
330          'c.d': 'omg',
331          e: {
332            'f.g': {
333              '1.0.0': 'multiple',
334              '2.0.0': {
335                author: {
336                  name: 'Ruy Adorno',
337                  url: 'https://npmjs.com',
338                  foo: {
339                    bar: {
340                      lorem: {
341                        ipsum: {
342                          dolor: {
343                            sit: {
344                              amet: {
345                                omg: 'O_O',
346                              },
347                            },
348                          },
349                        },
350                      },
351                    },
352                  },
353                },
354              },
355            },
356          },
357        },
358      },
359    },
360    'many many times...'
361  )
362  t.throws(
363    () => q.set('foo.bar.nest', 'should throw'),
364    { code: 'EOVERRIDEVALUE' },
365    'should throw if trying to override a literal value with an object'
366  )
367  q.set('foo.bar.nest', 'use the force!', { force: true })
368  t.strictSame(
369    q.toJSON().foo,
370    {
371      bar: {
372        nest: 'use the force!',
373      },
374      baz: 'baz',
375      'lorem.ipsum': 'LOREM IPSUM',
376    },
377    'should allow overriding literal values when using force option'
378  )
379
380  const qq = new Queryable({})
381  qq.set('foo.bar.baz', 'BAZ')
382  t.strictSame(
383    qq.toJSON(),
384    {
385      foo: {
386        bar: {
387          baz: 'BAZ',
388        },
389      },
390    },
391    'should add new props to qq object'
392  )
393  qq.set('foo.bar.bario', 'bario')
394  t.strictSame(
395    qq.toJSON(),
396    {
397      foo: {
398        bar: {
399          baz: 'BAZ',
400          bario: 'bario',
401        },
402      },
403    },
404    'should add new props to a previously existing object'
405  )
406  qq.set('lorem', 'lorem')
407  t.strictSame(
408    qq.toJSON(),
409    {
410      foo: {
411        bar: {
412          baz: 'BAZ',
413          bario: 'bario',
414        },
415      },
416      lorem: 'lorem',
417    },
418    'should append new props added to object later'
419  )
420  qq.set('foo.bar[foo.bar]', 'foo.bar.with.dots')
421  t.strictSame(
422    qq.toJSON(),
423    {
424      foo: {
425        bar: {
426          'foo.bar': 'foo.bar.with.dots',
427          baz: 'BAZ',
428          bario: 'bario',
429        },
430      },
431      lorem: 'lorem',
432    },
433    'should append new props added to object later'
434  )
435})
436
437t.test('set arrays', async t => {
438  const q = new Queryable({})
439
440  q.set('foo[1]', 'b')
441  t.strictSame(
442    q.toJSON(),
443    {
444      foo: [
445        undefined,
446        'b',
447      ],
448    },
449    'should be able to set items in an array using index references'
450  )
451
452  q.set('foo[0]', 'a')
453  t.strictSame(
454    q.toJSON(),
455    {
456      foo: [
457        'a',
458        'b',
459      ],
460    },
461    'should be able to set a previously missing item to an array'
462  )
463
464  q.set('foo[2]', 'c')
465  t.strictSame(
466    q.toJSON(),
467    {
468      foo: [
469        'a',
470        'b',
471        'c',
472      ],
473    },
474    'should be able to append more items to an array'
475  )
476
477  q.set('foo[2]', 'C')
478  t.strictSame(
479    q.toJSON(),
480    {
481      foo: [
482        'a',
483        'b',
484        'C',
485      ],
486    },
487    'should be able to override array items'
488  )
489
490  t.throws(
491    () => q.set('foo[2].bar', 'bar'),
492    { code: 'EOVERRIDEVALUE' },
493    'should throw if trying to override an array literal item with an obj'
494  )
495
496  q.set('foo[2].bar', 'bar', { force: true })
497  t.strictSame(
498    q.toJSON(),
499    {
500      foo: [
501        'a',
502        'b',
503        { bar: 'bar' },
504      ],
505    },
506    'should be able to override an array string item with an obj'
507  )
508
509  q.set('foo[3].foo', 'surprise surprise, another foo')
510  t.strictSame(
511    q.toJSON(),
512    {
513      foo: [
514        'a',
515        'b',
516        { bar: 'bar' },
517        {
518          foo: 'surprise surprise, another foo',
519        },
520      ],
521    },
522    'should be able to append more items to an array'
523  )
524
525  q.set('foo[3].foo', 'FOO')
526  t.strictSame(
527    q.toJSON(),
528    {
529      foo: [
530        'a',
531        'b',
532        { bar: 'bar' },
533        {
534          foo: 'FOO',
535        },
536      ],
537    },
538    'should be able to override property of an obj inside an array'
539  )
540
541  const qq = new Queryable({})
542  qq.set('foo[0].bar[1].baz.bario[0][0][0]', 'something')
543  t.strictSame(
544    qq.toJSON(),
545    {
546      foo: [
547        {
548          bar: [
549            undefined,
550            {
551              baz: {
552                bario: [[['something']]],
553              },
554            },
555          ],
556        },
557      ],
558    },
559    'should append as many arrays as necessary'
560  )
561  qq.set('foo[0].bar[1].baz.bario[0][1][0]', 'something else')
562  t.strictSame(
563    qq.toJSON(),
564    {
565      foo: [
566        {
567          bar: [
568            undefined,
569            {
570              baz: {
571                bario: [[
572                  ['something'],
573                  ['something else'],
574                ]],
575              },
576            },
577          ],
578        },
579      ],
580    },
581    'should append as many arrays as necessary'
582  )
583  qq.set('foo', null)
584  t.strictSame(
585    qq.toJSON(),
586    {
587      foo: null,
588    },
589    'should be able to set a value to null'
590  )
591  qq.set('foo.bar', 'bar')
592  t.strictSame(
593    qq.toJSON(),
594    {
595      foo: {
596        bar: 'bar',
597      },
598    },
599    'should be able to replace a null value with properties'
600  )
601
602  const qqq = new Queryable({
603    arr: [
604      'a',
605      'b',
606    ],
607  })
608
609  qqq.set('arr[]', 'c')
610  t.strictSame(
611    qqq.toJSON(),
612    {
613      arr: [
614        'a',
615        'b',
616        'c',
617      ],
618    },
619    'should be able to append to array using empty bracket notation'
620  )
621
622  qqq.set('arr[].foo', 'foo')
623  t.strictSame(
624    qqq.toJSON(),
625    {
626      arr: [
627        'a',
628        'b',
629        'c',
630        {
631          foo: 'foo',
632        },
633      ],
634    },
635    'should be able to append objects to array using empty bracket notation'
636  )
637
638  qqq.set('arr[].bar.name', 'BAR')
639  t.strictSame(
640    qqq.toJSON(),
641    {
642      arr: [
643        'a',
644        'b',
645        'c',
646        {
647          foo: 'foo',
648        },
649        {
650          bar: {
651            name: 'BAR',
652          },
653        },
654      ],
655    },
656    'should be able to append more objects to array using empty brackets'
657  )
658
659  qqq.set('foo.bar.baz[].lorem.ipsum', 'something')
660  t.strictSame(
661    qqq.toJSON(),
662    {
663      arr: [
664        'a',
665        'b',
666        'c',
667        {
668          foo: 'foo',
669        },
670        {
671          bar: {
672            name: 'BAR',
673          },
674        },
675      ],
676      foo: {
677        bar: {
678          baz: [
679            {
680              lorem: {
681                ipsum: 'something',
682              },
683            },
684          ],
685        },
686      },
687    },
688    'should be able to append to array using empty brackets in nested objs'
689  )
690
691  qqq.set('foo.bar.baz[].lorem.array[]', 'new item')
692  t.strictSame(
693    qqq.toJSON(),
694    {
695      arr: [
696        'a',
697        'b',
698        'c',
699        {
700          foo: 'foo',
701        },
702        {
703          bar: {
704            name: 'BAR',
705          },
706        },
707      ],
708      foo: {
709        bar: {
710          baz: [
711            {
712              lorem: {
713                ipsum: 'something',
714              },
715            },
716            {
717              lorem: {
718                array: [
719                  'new item',
720                ],
721              },
722            },
723          ],
724        },
725      },
726    },
727    'should be able to append to array using empty brackets in nested objs'
728  )
729
730  const qqqq = new Queryable({
731    arr: [
732      'a',
733      'b',
734    ],
735  })
736  t.throws(
737    () => qqqq.set('arr.foo', 'foo'),
738    { code: 'ENOADDPROP' },
739    'should throw an override error'
740  )
741
742  qqqq.set('arr.foo', 'foo', { force: true })
743  t.strictSame(
744    qqqq.toJSON(),
745    {
746      arr: {
747        0: 'a',
748        1: 'b',
749        foo: 'foo',
750      },
751    },
752    'should be able to override arrays with objects when using force=true'
753  )
754
755  qqqq.set('bar[]', 'item', { force: true })
756  t.strictSame(
757    qqqq.toJSON(),
758    {
759      arr: {
760        0: 'a',
761        1: 'b',
762        foo: 'foo',
763      },
764      bar: [
765        'item',
766      ],
767    },
768    'should be able to create new array with item when using force=true'
769  )
770
771  qqqq.set('bar[]', 'something else', { force: true })
772  t.strictSame(
773    qqqq.toJSON(),
774    {
775      arr: {
776        0: 'a',
777        1: 'b',
778        foo: 'foo',
779      },
780      bar: [
781        'item',
782        'something else',
783      ],
784    },
785    'should be able to append items to arrays when using force=true'
786  )
787
788  const qqqqq = new Queryable({
789    arr: [
790      null,
791    ],
792  })
793  qqqqq.set('arr[]', 'b')
794  t.strictSame(
795    qqqqq.toJSON(),
796    {
797      arr: [
798        null,
799        'b',
800      ],
801    },
802    'should be able to append items with empty items'
803  )
804  qqqqq.set('arr[0]', 'a')
805  t.strictSame(
806    qqqqq.toJSON(),
807    {
808      arr: [
809        'a',
810        'b',
811      ],
812    },
813    'should be able to replace empty items in an array'
814  )
815  qqqqq.set('lorem.ipsum', 3)
816  t.strictSame(
817    qqqqq.toJSON(),
818    {
819      arr: [
820        'a',
821        'b',
822      ],
823      lorem: {
824        ipsum: 3,
825      },
826    },
827    'should be able to replace empty items in an array'
828  )
829  t.throws(
830    () => qqqqq.set('lorem[]', 4),
831    { code: 'ENOAPPEND' },
832    'should throw error if using empty square bracket in an non-array item'
833  )
834  qqqqq.set('lorem[0]', 3)
835  t.strictSame(
836    qqqqq.toJSON(),
837    {
838      arr: [
839        'a',
840        'b',
841      ],
842      lorem: {
843        0: 3,
844        ipsum: 3,
845      },
846    },
847    'should be able add indexes as props when finding an object'
848  )
849  qqqqq.set('lorem.1', 3)
850  t.strictSame(
851    qqqqq.toJSON(),
852    {
853      arr: [
854        'a',
855        'b',
856      ],
857      lorem: {
858        0: 3,
859        1: 3,
860        ipsum: 3,
861      },
862    },
863    'should be able add numeric props to an obj'
864  )
865})
866
867t.test('delete values', async t => {
868  const q = new Queryable({
869    foo: {
870      bar: {
871        lorem: 'lorem',
872      },
873    },
874  })
875  q.delete('foo.bar.lorem')
876  t.strictSame(
877    q.toJSON(),
878    {
879      foo: {
880        bar: {},
881      },
882    },
883    'should delete queried item'
884  )
885  q.delete('foo')
886  t.strictSame(
887    q.toJSON(),
888    {},
889    'should delete nested items'
890  )
891  q.set('foo.a.b.c[0]', 'value')
892  q.delete('foo.a.b.c[0]')
893  t.strictSame(
894    q.toJSON(),
895    {
896      foo: {
897        a: {
898          b: {
899            c: [],
900          },
901        },
902      },
903    },
904    'should delete array item'
905  )
906  // creates an array that has an implicit empty first item
907  q.set('foo.a.b.c[1][0].foo.bar[0][0]', 'value')
908  q.delete('foo.a.b.c[1]')
909  t.strictSame(
910    q.toJSON(),
911    {
912      foo: {
913        a: {
914          b: {
915            c: [null],
916          },
917        },
918      },
919    },
920    'should delete array item'
921  )
922})
923
924t.test('logger', async t => {
925  const q = new Queryable({})
926  q.set('foo.bar[0].baz', 'baz')
927  t.strictSame(
928    inspect(q, { depth: 10 }),
929    inspect({
930      foo: {
931        bar: [
932          {
933            baz: 'baz',
934          },
935        ],
936      },
937    }, { depth: 10 }),
938    'should retrieve expected data'
939  )
940})
941
942t.test('bracket lovers', async t => {
943  const q = new Queryable({})
944  q.set('[iLoveBrackets]', 'seriously?')
945  t.strictSame(
946    q.toJSON(),
947    {
948      '[iLoveBrackets]': 'seriously?',
949    },
950    'should be able to set top-level props using square brackets notation'
951  )
952
953  t.equal(q.get('[iLoveBrackets]'), 'seriously?',
954    'should bypass square bracket in top-level properties')
955
956  q.set('[0]', '-.-')
957  t.strictSame(
958    q.toJSON(),
959    {
960      '[iLoveBrackets]': 'seriously?',
961      '[0]': '-.-',
962    },
963    'any top-level item can not be parsed with square bracket notation'
964  )
965})
966