Рецепты nginx: виджет Государственного Адресного Реестра

от автора

Для приготовления виджета Государственного Адресного Реестра, кроме базы, нам также понадобится nginx и его плагины postgres и json. Можно воспользоваться готовым образом.

В базе была определена большая функция gar_select от json и возвращающая json. Вот как раз она и нужна для использования через nginx.

Итак, сначала добавляем (в http-секцию вне server-секций) апстрим для подключения к базе:

upstream gar {   # подключаемся к хосту postgres   # на порт 5432   # к базе gar   # под пользователем gar     postgres_server host=postgres port=5432 dbname=gar user=gar;   # задаём лог для апстрима     postgres_log /var/log/nginx/gar.err;   # будем держать по одному постоянному соединению с базой   # (на каждый рабочий процеесс)     postgres_keepalive 1;   # будем держать по одному подготовленному оператору   # (на каждое постоянное соединение с базой)     postgres_prepare 1 overflow=deallocate; }

а в server-секцию добавляем использование

location =/gar_select.json {   # задаём логи     access_log /var/log/nginx/gar_select.json.log main;     error_log /var/log/nginx/gar_select.json.err;   # направляем запрос на определённый выше апстрим     postgres_pass gar;   # разрешаем использование подготовленных операторов     postgres_prepare true;   # задаём собственно запрос   # (просто вызывая ту самую функцию gar_select,   # передавая ей в качестве аргумента GET-параметры   # в виде json)   # ** сразу хочу предупредить: здесь НЕТ sql-инъекций! **     postgres_query "select gar_select($json_get_vars::json)";   # в результате выдаём json     postgres_output json; }

Т.о. получаем что-то типа REST интерфейса к таблице gar, который будем использовать из JavaScript.

Сначала загрузим стили и скрипты в index.html

<html>     <head>         <meta http-equiv="content-type" content="text/html; charset=utf-8" />         <title>ГАР</title>         <link rel="stylesheet" href="/static/bootstrap-5.1.1/css/bootstrap.css" />         <link rel="stylesheet" href="/static/select2-4.1.0-rc.0/css/select2.css" />         <script src="/static/jquery-3.4.1/jquery-3.4.1.js"></script>         <script src="/static/bootstrap-5.1.1/js/bootstrap.js"></script>         <script src="/static/select2-4.1.0-rc.0/js/select2.js"></script>         <script src="/static/select2-4.1.0-rc.0/js/i18n/ru.js"></script>         <script src="index.js"></script>     </head>     <body>         <form action="gar_select.html" method="get"><table class="table"><tr>             <td>gar-select:</td>             <td><select id="id_label_single" class="gar-select" name="id" data-width="100%" data-placeholder="Адрес" data-initial="9ae64229-9f7b-4149-b27a-d1f6ec74b5ce" data-id="[b5701907-1537-4e52-b93a-d566a47086f7,daa79543-d4e1-4cf7-bd3a-5936e670aea8]"/></td>             <td><input class="btn btn-primary" type="submit" value="OK"/></td>         </tr></table></form>     </body> </html>

а вот сам скрипт index.js

// после загрузки страницы $(document).ready(function() {   // для каждого селекта с соответствующим классом     $('select.gar-select').each(function(number, select) {       // определяем функцию от элемента         function data(item) {return {           // возвращающую строку для select2             id: $.isArray(item) ? item[item.length - 1].id : item.id,             item: item,             text: $.isArray(item) ? item.map(function(item) {return item.text}).join(', ') : item.text,         }}       // а также функцию от элемента для его применения в select2         function select2(item) {$(select).data('select2').trigger('select', {data: data(item)})}       // инициализируем select2         $(select).select2({           // будем делать запросы             ajax: {               // формируем GET-параметры для функции gar_select                 data: function(params) {return {                     full: !$.isEmptyObject(select.value) || ($.isEmptyObject(params.term) && $.isEmptyObject(select.value) && !$.isEmptyObject(select.dataset.id)),                     id: $.isEmptyObject(params.term) && $.isEmptyObject(select.value) && !$.isEmptyObject(select.dataset.id) ? select.dataset.id : undefined,                     limit: select.dataset.limit || 10,                     offset: ((params.page || 1) - 1) * (select.dataset.limit || 10),                     parent: !$.isEmptyObject(select.value) ? select.value : undefined,                     text: params.term,                 }},                 dataType: 'json', // задаём тип результата                 delay: 300, // и задержку               // задаём функцию обработки результата                 processResults: function(response, params) {return {                     results: response.data.map(data),                     pagination: {more: response.offset < response.count}                 }},               // адрес можно задать прямо в теге                 url: select.dataset.url || 'gar_select.json',             },             allowClear: true, // показываем кнопку очистки             closeOnSelect: false, // не закрываем при выборе             dropdownParent: $(select).parent(), // располагаем в родителе             language: 'ru', // использеум русский язык         })       // при нажатии на кнопку очистки         $(select).on('select2:clear', function(event) {           // получаем элемент             var item = event.params.data[0].item             if ($.isArray(item)) {               // и если ещё не дошли до верха                 item = item.slice(0, -1)               // то показываем родителя                 if (item.length) {select2(item)}             }           // открываем выбор             setTimeout(function() {$(select).select2('open')})         })       // при выборе         $(select).on('select2:select', function(event) {           // запускаем поиск             $(select).data('select2').trigger('query', {})           // очищаем строку поиска             $(select).parent().find('.select2-search__field').val('').trigger('focus')         })       // если задан первоначальный уид         if (select.dataset.initial) {$.ajax({           // выполняем запрос             url: $(select).data('select2').options.options.ajax.url,             dataType: $(select).data('select2').options.options.ajax.dataType,             data: {id: select.dataset.initial}         }).done(function(response) {           // и выбираем результат             select2(response.data)           // закрываем выбор             $(select).select2('close')         })}     }) })

Ещё, с помощью плагинов evaluate и mustach можно организовать html-интерфейс:

location =/gar_select.html {   # задаём логи     access_log /var/log/nginx/gar_select.html.log main;     error_log /var/log/nginx/gar_select.html.err;   # вычисляем соответсвующие под-запросы     # в соотвествующие переменные     evaluate $json /gar_select.json;     evaluate $template /gar_select.template;   # задаём данные для шаблонизатора     mustach_json $json;   # задаём шаблон     mustach_template $template;   # задаём тип результата     mustach_content text/html; } location =/gar_select.template {     access_log /var/log/nginx/gar_select.template.log main;     error_log /var/log/nginx/gar_select.template.err;     internal; }

Собственно сам шаблон gar_select.template

<html>     <head>         <meta http-equiv="content-type" content="text/html; charset=utf-8" />         <title>ГАР</title>         <link rel="stylesheet" href="/static/bootstrap-5.1.1/css/bootstrap.min.css" />         <script src="/static/jquery-3.4.1/jquery-3.4.1.min.js"></script>         <script src="/static/bootstrap-5.1.1/js/bootstrap.min.js"></script>         <script>             var limit = {{limit}};             var offset = {{offset}};             var count = {{count}};         </script>         <script src="gar_select.js"></script>     </head>     <body>         <form action="" method="get">             <input type="hidden" name="child" value="true"/>             <table class="table table-bordered table-striped table-hover table-condensed text-nowrap">                 <thead>                     <th class="id" title="id"><input style="width:100%" class="offset" placeholder="id" name="id" value='{{query.id}}'/></th>                     <th class="parent" title="parent"><input style="width:100%" class="offset" placeholder="parent" name="parent" value="{{query.parent}}"/></th>                     <th class="name" title="name"><input style="width:100%" class="offset" placeholder="name" name="name" value="{{query.name}}"/></th>                     <th class="short" title="short"><input style="width:100%" class="offset" placeholder="short" name="short" value="{{query.short}}"/></th>                     <th class="type" title="type"><input style="width:100%" class="offset" placeholder="type" name="type" value="{{query.type}}"/></th>                     <th class="post" title="post"><input style="width:100%" class="offset" placeholder="post" name="post" value="{{query.post}}"/></th>                     <th class="object" title="object"><input style="width:100%" class="offset" placeholder="object" name="object" value="{{query.object}}"/></th>                     <th class="region" title="region"><input style="width:100%" class="offset" placeholder="region" name="region" value="{{query.region}}"/></th>                     <th class="text" title="text"><input style="width:100%" class="offset" placeholder="text" name="text" value="{{query.text}}"/></th>                     <th class="child" title="child"><input style="width:100%" class="" type="submit" value="child"/></th>                 </thead>                 {{#data}}                 <tr>                     <td class="id" title="id">{{id}}</td>                     <td class="parent" title="parent"><a href="gar_select.html?child=true&id={{parent}}">{{parent}}</a></td>                     <td class="name" title="name">{{name}}</td>                     <td class="short" title="short">{{short}}</td>                     <td class="type" title="type">{{type}}</td>                     <td class="post" title="post">{{post}}</td>                     <td class="object" title="text">{{object}}</td>                     <td class="region" title="text">{{region}}</td>                     <td class="text" title="text">{{text}}</td>                     <td class="child" title="child"><input name="child" type="button" value="{{child}}" parent="{{id}}"/></td>                 </tr>                 {{/data}}             </table>             <table class="table"><tr>                 <td id="pagination"/>                 <td>limit: <input title="limit" class="offset" placeholder="limit" name="limit" value="{{limit}}"/></td>                 <td>offset: <input title="offset" class="" placeholder="offset" name="offset" value="{{offset}}"/></td>                 <td id="record"/><td>Всего {{count}}</td>             </tr></table>         </form>     </body> </html>

и скрипт для пагинации gar_select.js

// после загрузки страницы document.addEventListener('DOMContentLoaded', function() {   // вычисляем количество страниц     var pages = Math.ceil(count / limit)     // вычисляем текущую страницу     var page = Math.ceil(offset / limit) + 1     // если все резульаты помещяются на одну страницу     if (count <= limit) {       // то скрываем пагинацию         $('#pagination').parent().parent().hide()     }   // если есть результаты     if (count > 0) {       // то добавляем информацию об этом         $('#record').append('Записи с ' + (offset + 1) + ' по ' + Math.min(offset + limit, count))     } else {       // иначе - скрываем         $('#record').hide()     }   // рассчитываем кнопки пагинации     for (var pag = 1, more = false; pag <= pages; pag++) {         if (pages <= 15 || pag <= 3 || pag >= pages - 2 || (page - 1 <= pag && pag <= page + 1)) {             $('#pagination').append(                 $('<input>', {                     type: 'button',                     value: pag,                     disabled: pag == page,                 }).click(function(event) {                     $('input[name=offset]').val((this.value - 1) * limit)                     $('form').submit()                 })             )             more = false         } else if (!more && (pag == 4 || pag == pages - 3)) {             $('#pagination').append($('<span>').text('...'))             more = true         }     }   // при изменении в верхних фильтрах     $('input.offset').change(function(event) {       // сбрасываем офсет         $('input[name=offset]').val(0)     })   // по клике на дочерних     $('input[name=child]').click(function(event) {       // сбрасываем офсет         $('input[name=offset]').val(0)       // и всё, кроме количества         $('input.offset').not('[name=limit]').val('')       // задаём родителя         $('input[name=parent]').val($(this).attr('parent'))       // и отправляем форму         $('form').submit()     }) })

Как пример того, что плагин evaluate может обрабатывать вложенные под-запросы, и в качестве примера работы плагина htmldoc можно html преобразовать в pdf:

location =/gar_select.pdf {   # задаём логи     access_log /var/log/nginx/gar_select.pdf.log main;     error_log /var/log/nginx/gar_select.pdf.err;   # вычисляем под-запрос в переменную     evaluate $html /gar_select.html;   # преобразуем html в pdf     html2pdf $html; }

Т.о., можно в любом месте, где нужен выбор адреса использовать виджет. Его же можно ипользовать и для поиска. Например, если нужно «найти все услуги на такой-то улице», то пользователь выбирает улицу в виджете, бакенд получает уид этой улицы, а по нему с помощью готовой функции gar_select_child получет уиды всех дочерних элементов (домов, квартир, комнат, …) с помощью которых уже может фильтровать в своей базе.

-- создаём или меняем функцию от ... CREATE OR REPLACE FUNCTION gar_select_child(parent uuid, name text, short text, type text, post text, region text) RETURNS SETOF gar_view LANGUAGE sql STABLE AS $body$     with _ as (         with recursive _ as ( -- рекурсивно           -- начиная сверху или с заданного элемента             select gar.*, 0 as i from gar where (gar_select_child.parent is null and parent is null) or id = gar_select_child.parent             union           -- и продолжая вниз             select gar.*, _.i + 1 as i from gar inner join _ on _.id = gar.parent         ) select * from _ where i > 0       -- добавляя если нужно различные фильтры         and (gar_select_child.name is null or name ilike gar_select_child.name||'%' or name ilike '% '||gar_select_child.name||'%' or name ilike '%-'||gar_select_child.name||'%' or name ilike '%.'||gar_select_child.name||'%')         and (gar_select_child.short is null or short ilike gar_select_child.short)         and (gar_select_child.type is null or case when gar_select_child.type ilike '{%}' then type = any(gar_select_child.type::text[]) else type ilike gar_select_child.type||'%' end)         and (gar_select_child.post is null or post ilike gar_select_child.post||'%')         and (gar_select_child.region is null or case when gar_select_child.region ilike '{%}' then region = any(gar_select_child.region::smallint[]) else region = gar_select_child.region::smallint end)         order by i     ) select id, parent, name, short, type, post, region, gar_text(name, short, type) AS text from _ order by to_number('0'||name, '999999999'), name; $body$;


ссылка на оригинал статьи https://habr.com/ru/articles/585476/