Jsqry — библиотека для запросов к JS объектам и массивам

    Представляю вашему вниманию небольшую js-библиотеку Jsqry.
    Проще всего проиллюстрировать её назначение следующим примером.


    До:


    var name;
    for (var i = 0; i < users.length; i++) {
        if (users[i].id == 123) {
            name = users[i].name;
            break;
        }
    }

    После:


    var name = one(users, '[_.id==?].name', 123);

    Библиотечка позволяет извлекать информацию из объектов/массивов в одну строку, используя несложный язык запросов, вместо написания циклов (подчас вложенных).


    По сути, она реализует всего две функции:


    • query — для возвращения списка результатов и
    • one — для возвращения первого найденного результата.

    Список возможностей включает:


    1. Фильтрацию
    2. Трансформацию
    3. Индексы/срезы в стиле Python

    Библиотека появилась спонтанно в одном проекте, построенном на модном ныне подходе одностраничного приложения. Мы загружаем один большой JSON, части которого затем используются для рендеринга на клиенте разных представлений сайта. И вот для выдирания этих самых частей и захотелось более удобного способа. Затем, впрочем, библиотека оказалась востребована и в других случаях.


    Поясню немного по функционалу. Запрос в общем случае может иметь вид


    field1.field2[ condition or index or slice ].field3{ transformation }.field4

    Тут:


    • field1.field2.field3... — обычный доступ к полям объектов, как в js
    • [ condition ] — фильтрация
    • [ index ] — доступ по индексу, тоже как в js
    • [ from:to:step ]срезы в стиле Python
    • { transformation } — преобразование объектов

    На condition и transformation стоит остановиться подробнее.
    На самом деле тут все очень просто. Достаточно понять, что каждое выражение внутри квадратных/фигурных скобок при выполнении заменяется на функцию по такому принципу:


    condition_or_transformation ⟶ function(_,i) { return condition_or_transformation }


    (тут _ — значение передаваемого элемента, i — его индекс).


    Пример:


    query([1,2,3,4,5],'[_>2]{_+10}') // [13, 14, 15]

    Также поддерживаются параметризация запроса:


    query([1,2,3,4,5],'[_>?]{_+?}', 2, 10) // [13, 14, 15] 

    Комбинируя эти возможности можно строить весьма сложные и гибкие запросы. Больше примеров использования можно посмотреть тут.


    Из интересного в реализации — AST дерево запроса кешируется, что придает библиотеке скорости.


    Разумеется, моя библиотека не уникальна в своём роде. Стоит привести "небольшой" список аналогов:



    Зачем еще одна библиотека? На самом деле, она поддерживает не весь возможный спектр типов запросов, а только то что было нужно в нашем проекте в большинстве случаев. За счет этого простота и скорость. Также, чрезвычайно простой API, вдохновленный подходом JQuery.


    Буду рад выслушать критику и предложения по улучшению.

    Поделиться публикацией

    Похожие публикации

    Комментарии 58

      +1
      Тесты скорости не проводились? Т.е. насколько теряется скорость при использовании библиотеки.
        0

        Скурпулезных замеров не проводил, но вот есть небольшой бенчмарк https://github.com/xonixx/jsqry/blob/master/bench.js.
        У меня 100000 прогонов запроса вида


        jsqry.one(o1, '[_.id>=2].name[_.toLowerCase()[0]==?].length', 's')

        отрабатывает за 425ms.

        +8

        За что я люблю LiveScript, так это за то, что подобные вещи можно достаточно компактно писать без библиотек.
        До:


        query([1,2,3,4,5],'[_>2]{_+10}')

        После:


        [1,2,3,4,5].filter(-> it>2).map(-> it+10)

        it это стандартная для LiveScript переменная, обозначающая первый аргумент функции. И никакого парсинга в рантайме не нужно.


        Или же ES2016, если странспайлер используется:


        [1,2,3,4,5].filter(it => it>2).map(it => it+10)
          0

          Спасибо, интересно.
          А такой юз-кейс:


          var hotel = {
              name: 'Name',
              facilities: [
                  {name:'Fac 1',
                  services: [
                      {name:'Service 1', visible:false},
                      {name:'Service 2'}
                  ]},
                  {name:'Fac 1',
                  services: [
                      {name:'Service 3'},
                      {name:'Service 4', visible:false},
                      {name:'Service 5'}
                  ]}
              ]
          };
          
          console.info(query(hotel,'facilities.services[_.visible !== false].name')); // [ 'Service 2', 'Service 3', 'Service 5' ]
          
            0
            Два пути: красивый, но с добавлением одной функции в Array.prototype
            Array.prototype.concatItems = function(){ return Array.prototype.concat.apply([], this) };
            
            hotel.facilities.map(_=>_.services).concatItems().filter(_=>_.visible !== false).map(_=>_.name); // [ 'Service 2', 'Service 3', 'Service 5' ]
            

            и не такой красивый
            [].concat.apply([], hotel.facilities.map(_=>_.services)).filter(_=>_.visible !== false).map(_=>_.name); // [ 'Service 2', 'Service 3', 'Service 5' ]
            
              +1
                +5
                hotel.facilities.map(_=>_.services).reduce((a,b)=>a.concat(b)).map(_=>_.name);
                
                  +1
                  Вот да, только хотел спросить, чем стандартные map/reduce/filter не угодили.
                    +2
                    Я подумал об этом, но только после отправки комментария.
                    Замечу, что у такого решения есть недостаток — вычислительная сложность у него O(n*m) где n — суммарное количество элементов в подмассивах, m — количество подмассивов. У concat это O(n).
                      +1
                      На самом деле тут вариантов несколько.
                      Если нужна лаконичнось, все укладывается в 1 reduce:
                      hotel.facilities.reduce(
                       (acc, f) => acc.concat(f.services.filter(s => s.visible !== false).map(s => s.name)), 
                       []
                      );
                      // ["Service 2", "Service 3", "Service 5"]
                      

                      Если нужна скорость — пожалуйста.
                      hotel.facilities.reduce((acc, f) => {
                       f.services.forEach(s => {
                        if (s.visible !== false) {
                         acc.push(s.name);
                        }
                       });
                       return acc;
                      }, []);
                      // ["Service 2", "Service 3", "Service 5"]
                      

                      Это я все к тому, что вариантов много и есть возможность использовать нужный, потому-что JS гибок. Но данная библиотека только повышает количество возможных способов, хотя краевые задачи все-равно придется решать с использованием нативных средств.
                        0
                        Но данная библиотека только повышает количество возможных способов


                        Не только. Её вариант проще Вашего лаконичного варианта, правильно обрабатывает null/undefined, работает на любом древнем JS.
                        По-моему плюсов не так и мало.
                        query(hotel,'facilities.services[_.visible !== false].name')
                          +1
                          Что значит правильно обрабатывает? Нул не должен обрабатываться никак и undefined, на самом деле, тоже. Сейчас я почитаю код библиотеки и точно скажу насколько оно правильно.
                            +1
                            Посмотрел. Функция defined плохая. Обычно требуется не проверка «a.b === undefined», а «b in a», потому что undefined может быть действительно значением поля, которое мы хотим извлечь. Если заявлена поддержка старых браузеров, то там значение undefined вообще можно переопределить, по этой причине я долго оборачивал весь код в функцию (function(undefined){ код })(), пока не перешел на void 0.
                              0
                              там значение undefined вообще можно переопределить


                              Тот кто это сделает — будет гнусным извращенцем. За то, как поведёт себя код в руках гнусного извращенца, разработчик не отвечает.
                                0
                                К сожалению, тут проблема двусторонняя… И «гнусный извращенец» может пролезть к вам в сборку с 50-ой зависимостью какого-нибудь npm-пакета :)
                                  0

                                  Среди разработчиков сколь-нибудь серьёзных библиотек извращенцев нет, а несерьёзные — проще самому написать.

                              0
                              Что значит правильно обрабатывает


                              Как Ваши варианты решения отработают на какой-то из записей facilities без поля services?
                                +1
                                Ну окей. Что, если мне нужно показать сообщение об ошибке, что с сервера пришли битые данные (без services — просто абстрактный пример)? Ваша библиотека, так сказать, прибила гвоздями проверку на undefined, и, соответственно, молча продолжит выборку, когда, используя стандартные функции, можно добавить и/или исключить все необходимые проверки.
                                Это гибко и удобно, хоть и не позволяет сэкономить лишние пару строк.
                                  +1
                                  Даже не про этот кейс, тут можно итоговый length проверить. А вот приходит с сервера [{name:1}, {name: undefined}, {name:7}] -> обычно из него хотят отрисовать вьюху где первый и последний элемент будет содержать 1 и 7, а вместо второго надпись «введите значение», с волшебной библиотекой мы не узнаем о том что значения во втором поле нет, а особенно важно что мы не узнаем что его нет именно во втором элементе. Причём я понимаю когда эта библиотека может быть удобна, но такие граничные поведения надо писать огромными буквами в документации, потому что это может вылиться в часы отладки через месяц использования библиотеки.
                                    0
                                    Ну это философский спор. Но, например, разработчики angularjs сделали так же.
                                    Forgiving: In JavaScript, trying to evaluate undefined properties generates ReferenceError or TypeError. In Angular, expression evaluation is forgiving to undefined and null
                                      +1
                                      Кстати, это очень здорово, что Вы упомянули этот момент в ангуляре. В свое время он доставил немало «удовольствия» в поиске места, куда приткнуться, чтобы понять странность получаемого результата и отладить это их «небольшое усовершенствование». Думаю, что с jsqry рано или поздно случилось бы то же самое…
                        0
                        Может автор учёл undefined элементы этой цепочки.
                          0

                          Весьма верное замечание! Вариант с Jsqry корректно прожует вариант объекта без поля services. Вариант с ES-кунг-фу скорее всего упадет с Null Pointer.

                            0
                            Не упадёт, если обратиться к несуществующему проперти, оно будет undefined. При попытке обращения к более вложенным полям несуществующего поля упало бы, на этот случай можно использовать _.get из lodash или синтаксис obj?.prop из CoffeeScript.
                              +1
                              Вся необходимая логика (да и вообщем-то любые проходы по коллекциям) так или иначе сводится к map/reduce. Собственно, вся логика выборки (в том числе и все-возможные проверки на undefined/null/коня_в_вакууме) прекрасно умещается в лямбдах для них.

                              «ES-кунг-фу» гораздо более предсказуемо, его проще и удобнее отлаживать и, что самое важное, поддерживать — это часть стандарта.
                                0
                                Я не спорю с кунг-фу, поскольку сам люблю строить развесистые комбо, но хотел обратить внимание что (возможно (не читал код этой библиотеки) ) приведённая цепочка мэп\фильтро\редьюсов не является эквивалентом.
                          +3

                          Я бы реализовал следующим образом.


                          LiveScript:


                          [].concat(...hotel.facilities.map(-> it.services.filter(-> it.visible!==false).map(-> it.name)))

                          ES2016:


                          [].concat(...hotel.facilities.map(it => it.services.filter(it => it.visible!==false).map(it => it.name)))

                          Да, этот конкретно вариант уже более многословен, тем не менее никаких библиотек не нужно и сторонний наблюдатель с относительной легкостью сможет понять что происходит. Хотя в одну строчку в реальном проекте я бы не писал:)

                            +1
                            А зачем пример на LiveScript, если пример — на ES2016 то же самое?
                              +1

                              Изначально на LS написал, вот и здесь повторил.
                              А вообще я считаю что LS незаслужено обделен вниманием. В нём много интересных решений, которых мне не хватало в CoffeeScript, возможно, мой комментарий вынудит кого-то зайти на сайт и посмотреть что он собой представляет.

                        +3
                        О, это тот редкий случай когда eval и new Function имеют право на жизнь. Либо генерировать функцию и делать кэш по строке, либо функцию которая компилирует выражение в функцию, и возвращает функцию которая принимает непосредственно объект.
                          0
                          что Вы имеете в виду?
                            +2
                            Уже посмотрел что решение использует кэширование ast, значит со скоростью всё не так плохо как я предполагал. И наличие самого AST тоже было очень приятно. Я имел в виду отдавать не результат вычислений, а функцию с замкнутой функцией соответствующей конкретному AST, после чего эта функция принимает на вход данные, а даёт результат.
                          +4
                          В первом примере нужен break:
                          var name;
                          for (var i = 0; i < users.length; i++) {
                              if (users[i].id == 123) {
                                  name = users[i].name;
                                  break;
                              }
                          }
                          
                            +3
                            А я вообще первый пример заменил бы, на простую и понятную конструкцию.
                            var name = ( users.find( u => u.id == 123 ) || {} ).name
                            
                              +1
                              лучше даже так:
                              var name = ( users.find( u => u.id == 123 ) || { name: 'anonymous' } ).name
                              
                              0

                              Да, спасибо.

                              +6
                              Нужно больше строковых конструкций! Как это дебажить?
                                –1

                                В этом смысле так же как и регулярные выражения, JQuery-селекторы или тот же SQL.

                                  +2
                                  Тогда лучше взять что-то типа lodash/fp, Ramda, воспользоваться линзами (хотя это больше для хаскеля)
                                    0

                                    А что — разве будет легче дебажить?

                                      +1
                                      Конечно, я же могу раскидать лямбды на именованные функции если надо + `tap` у lodash
                                        –2

                                        Ну когда запрос настолько сложен, что требует дебага, можно и в коде переписать.
                                        А так-то можно и в Jsqry, хотя, признаться, о таком способе использования я раньше не думал.


                                        function f1(elt) { return elt > 2 }
                                        function f2(elt) { return elt + 10 }
                                        console.info(jsqry.query([1,2,3,4,5],'[ ?(_) ]{ ?(_) }', f1, f2)); // [ 13, 14, 15 ]
                                +1
                                Натыкался на JSONSelect, 1,5k звезд на гитхабе, но к сожалению, последний апдейт 3 года назад.
                                Из приятного (что лично нравится мне) — это CSS selector-like way для выборки данных (то есть довольно интуитивно для веб-девелоперов), есть тесты, и есть подтверждение востребованности в виде большого количества девелоперов обративших на это внимание.
                                  +1
                                  Это актуально? Однако.
                                  Тогда вот мой примитивный поиск по xpath в объекте
                                    0
                                    XPath — большой стандарт в котором есть много дополнительных опций для выборки. Насколько ваш вариант совместим со стандартом?
                                      0
                                      Я не стал заморачиваться. Просто //, но чую можно довести до ума :) Потом…
                                    0
                                    ммм выглядит интересно, но реальная польза будет когда подобный синтаксис запросов будет описан на нативном языке в виде модуля ES6 или ES7.
                                      0
                                      Из подобного рекомендуемую https://github.com/davidgtonge/underscore-query — mongodb-like синтаксис как по мне гораздо лучше.
                                        +1
                                        Чем это лучше JSPath?
                                          0

                                          Библиотеки очень похожи по задумке. Jsqry значительно проще как по устройству так и по использованию за счет переиспользования обычного js для предикатов, в JSPath же изобрели свой язык предикатов со своими операторами и т.д. По функционалу, имхо 80-90% совпадает. Хотя в Jsqry срезы более продвинутые (поддерживется step параметр, как в Python), а также не нашел трансформации у них, только фильтрация. С другой стороны, в JSPath, разумеется есть и возможности, коих нет в Jsqry, например, '..' и '|' в location path. А, да, у них еще более громоздкий синтаксис подстановок — именованные вместо '?'. Впрочем, это можно расценить и как плюс.

                                          0
                                          Да, нужно больше языков, конечно!
                                          Если серьезно, то по-моему после появления arrow-functions в ES6 или typescript (в котором они были от рождения), это всё на столько бесполезно…
                                            –1
                                            как Arrow-функции помогают делать запросы к объектам? это обычные функции без собственного контекста.
                                              +2
                                              Компактностью аргументов в методах обработки массивов типа map и reduce.
                                            +1
                                            Самый серьезный минус в том, что рефакторинг и подсветка в ИДЕ не будет работать и не будет работать линт и тайпскрипт компилятор так же ничего проверить не сможет.
                                            Нечто похожее я сделал для ActionScript, но сделал это на функциях и как часть библиотеки для работы с коллекциями. В принципе, я согласен с тем, что arrow-functions эту нишу во многом закрывают.
                                              +3
                                              Может попробовать Lodash?
                                                +2
                                                Array.prototype.find и filter, имхо, вполне решают основную часть проблем (и сразу есть в стандарте ES5-ES2015). В крайнем случае, можно и lodash подключить
                                                  +1
                                                  Спасибо за проделанную работу.
                                                  Лично я планирую использовать эту библиотеку.
                                                  А то, что эту задачу можно решить в ES6 или typescript, ну так отлично. Ее можно решить используя еще множество других инструментов.
                                                  В данном случае мне нравится, что это JS изначально, а не «обертки», да и размер самой библиотеки для меня является тоже плюсом.
                                                    0
                                                    Владимир, ну чего мелочиться. До делайте до конца, то есть до E4J :) Подглядывать можно сюда https://github.com/s9tpepper/JSONTools

                                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                    Самое читаемое