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