META

Жизненный цикл запроса

Введение

Окружение запроса

  • env - объект-окружение запроса к серверу.
  • vm - объект для удобной работы с ответом клиенту. На лего странцах отдельный vm на каждый script.
  • pvm - только на LEGO-страницах. Отвечает за хранение данных на всех странце, содержит в себе vm script.
  • DataResult - низкоуровневый класс, отвечающий за ответ с данными по EntityPage. Его нельзя получить так как он никуда не передается, существует вне script - над ним. Создается вручную при необходимости только в meta/js скриптах. Содержит в себе pvm, который в свою очередь и передается в страницы. В общем случае не рекомендуетсяим пользоваться без крайней необходимости, хотя он может быть полезен для построении таблиз из js на основе данных какого-либо api без сохранения в БД.

Состояния страниц

  • env.state - id текущего состояния страницы. Это можно сравнить с action в контроллере MVC. При первоначальной загрузке страница загружается с state=default, если у script или elem не указан states, то meta попытается их выполнить, если будут удовлетворены зависимости, указанные через depends
  • env.sp - параметры состояния

Выполенение url типа /page. Пример для env.sp

Рассмотрим жизненный цикл на основе примера с обычной странице с таблицей и фильтром - https://apps.devision.io/page?p=3398&a=35

Итак, что происходит, когда пользоатель заходит на страницу:

  • Backend получает указание загрузить pageId=3398 и applicationId=35 (Мы можем видеть это из url - параметры p и a соответственно /page?p=3398&a=35)
  • Backend проверяет соответствие ACL страницы и текущего пользователя
    • Если требуется авторизация, то пользователя редиректит на форму входа (или oauth с параметром retpath, который говорит куда нужно перебросить пользователя в случае успешной авторизации)
    • Если авторизация не требуется, и ACL не удовлетворяет, то выдается 403
  • Если все ок, то мета отдает html страницу в теле которой указаны данные пользователя и список страниц текущего приложения (именно поэтому сейчас требуется f5 при изменении параметров или набора страниц)
  • UI инициализирует компонент me-page с pageId и пр
  • me-page начинает разрешать зависимости по текущему env и запрашивает данные путем вызова backend api /api/meta/page (вы можете наблюдать вызовы в chrome debugger)
  • После получения данных me-page начинает рисовать root lego страницы. Это то, что страница вернула через описание через script и elem
  • Надо сразу упомянуть, что после отрисовки root lego, если он имеет зависимости refPvmData, me-page по аналогиис первичным запуском будет пытаться разрешить и их до тех пор, пока все не будет разрешено или не вернется ошибка или пустой ответ
<!-- 
Это скрипт с типом sql, который:
 - имеет id=period (т.е. будет доступен в env.sp.period)
 - запрашивает данные из БД с альясом meta_samples
 - будет оторажаться как me-input с типом daterange (elem="me-input" elem-attrs='{"type":"daterange"}') - это элемент пользовательского ввода в виде периода дат
 - имеет дефолт {"from":"2014-12-01", "to":"2015-03-01"} . Т.е. при первоначальной загрузке страницы уже будет иметь значение. Это очень удобно для отчетов
 - Имеет приоритет вывода 10 (order)
 - Занимает 4 ячейки в горизонтальной сетке (span)
 - live-reload говорит о том, что при изменении значения инмупа страница будет обновлена с новым значением env.sp.period. Обычно это заменятеся атрибутом page-search, но для этой страницы он не нужен
-->
<script type="meta/sql" id="period" db-alias="meta_samples" 
    default='{"from":"2014-12-01", "to":"2015-03-01"}' elem="me-input" order="10" span="4"
    elem-attrs='{"type":"daterange"}' live-reload>
  SELECT 1
</script>

<!-- 
Теги elem используется для простых пользовательских шаблонов.
Обычно это не рекомендуется делать, так как переход на другие ui фреймворки будет усложнено, но иногда так проще всего

UI template.
Вы можете наблюдать вот такой вызов в шаблоне {{env.sp|json}} эта строка будет выводить env.sp в виде json через систему шаблонов angularjs. 
Работает на frontend. В общем случае так делать не рекомендуется, так как поддержка angularjs скоро прекращается и придется переписывать такие вызовы.

Backenf template.
Это ${env.sp} работает freemarker - java шаблонизатор, работает на backend до того, как root lego будет возвращен на frontend.
-->
<elem order="11" span="3">
  <tpl>
    <p>
      Тут в тесте мы проверяем, что stateParams теперь всегда и везде приходят как env.sp
    </p>
    <table class="table table-bordered" style="max-width: 500px">
      <tr>
        <td>Frontend angularjs state params</td>
        <td class="angular-sp">{{env.sp|json}}</td>
      </tr>
      <tr>
        <td>Backend JS state params</td>
        <td class="js-sp">{{jsout.test|json}}</td>
      </tr>
      <tr>
        <td>Template state params</td>
        <td class="tpl-sp">${env.sp}</td>
      </tr>
    </table>
  </tpl>
  <script type="meta/js" id="jsout">
    function main(vm, env) {
      vm.test = env.sp;
    }
  </script>
</elem>

<!--
Тут хорошо видно, что поле date приводится к типу date средствами postgresql и фильтруется данными из env.sp.period (:env.sp.period.from::date AND :env.sp.period.to::date)
Это как раз те данные из элемента period, объявленного выше. 
Поскольку атрибута elem нет, то данные по умолчанию выводятся в виде таблицы (то же самое было бы, если мы поставим elem="me-table")

Если вам надо скрыть элемент, но вернуть данные на frontend используйте elem="hidden"
Если данные не должны уходить с сервера используйте атрибут internal
-->
<script type="meta/sql" db-alias="meta" depends="period" id="tbl" order="50">
SELECT * FROM generate_series('2010-03-01 00:00'::timestamp,
                '2016-03-04 12:00', '10 hours') as date
WHERE date::date BETWEEN :env.sp.period.from::date AND :env.sp.period.to::date
ORDER BY date
${pager}
</script>

После того, как пользователь выбирает период дат в элементе period frontend посылает запрос в /api/meta/page с новым окружением, получает ответ и перерендеривает страницу. Все это пока меняет env.sp, но не env.state.

Выполенение url типа /card. Пример для env.state

Хороший пример тут - https://apps.devision.io/page?p=5181&a=35

На странице видно список объектов, вы можете создать новый и отредактировать старый. Редактирование доступно как при клике на название объекта, так и при клике в контестном меню по пункту “Быстрое редактирование” при клике правой клавишей мыши на строке объекта.

На странице редактирования объекта описано все поведение меты для выполнения запроса https://apps.devision.io/card?e=example_entity&o=08f3395b-31f2-42c9-917e-a5f2b7bd4a95&a=35

<!-- 
Объявляем валидатор с depends от name. Параметр name в случае этой конкретной формы будет приходить как имя объекта.
Такая валидация не обяъательна, это будет зависеть от конкретной формы. Часто это просто не надо.
-->
<script id="name_validator" type="meta/sql" db-alias="meta_samples" depends="name">
SELECT 'name_validator_unique' as id, 'Имя уже используется' as name FROM public.example_entity WHERE name = :env.sp.name LIMIT 1
</script>

<!-- 
Для упрощения разработки форм мы договорились, что данные объекта сужности мы всегда извлеваем в скрипте с id=info с атрибутом internal (данный атрибут не дает данным уходить на frontend) 
-->
<script type="meta/sql" db-alias="meta_samples" id="info" internal states="default">
SELECT id, name, creation_time, last_user_id
FROM public.example_entity
WHERE id=NULLIF(:env.objectId, '0')::uuid
LIMIT 1
</script>

<elem states="default" order="190">
    <tpl>
        <!-- Форма при отправке будет метять env.state на save и поэтому будут исполняться скрипты6 которые описаны ниже и имеют states="save" -->
        <form name="editGroupForm" ng-submit="changeState('save', {obj:env.sp.obj})">
            <!-- 
            Атрибут формы output важно делать именно env.sp.obj. 
            В старом коде вы можете встретить другие значения, но в новых формах нужно всегда такой.
            Это позволяет передавать в форму параметры через env.sp с внешних кнопок и других форм для первичного заполнения и в целом стандартизирует разработку.
             -->
            <me-lego elems="editCard.elems" output="env.sp.obj"></me-lego>
        </form>
    </tpl>
</elem>

<script type="meta/js" id="editCard" elem="hidden" states="default">
  function main(vm, pvm, env) {
    // Получаем инфу из бд, если редактирвание или указываем значение по умолчанию
    var info = pvm.data.info.notEmpty ? pvm.data.info.rows[0] : {};
    // Не добавляейте в env секретные поля - эти данные уйдут в интерфейс
    // Говорим мете, что при первичной загрузке страницы в env sp нужно положить данные из info в obj
    // Далее они будут доступны как env.sp.obj
    pvm.initEnvSp(env, {obj: info});

    vm.elems = [
      {
        id: "name",
        label: "Название",
        span: 12,
        name: "me-input",
        // Таким образом мы настраиваем валидатор на поле. 
        // Подробный пример о валидаторах - https://apps.devision.io/page?p=3443&a=35
        refPvmValidator: {id: "name_validator"},
        attrs: {
          required: true,
          min: 2,
          max: 30
        }
      },

      // Просто перенос строки
      {name: "newrow"},
      
      // Кнопка отправки формы
      {
        id: "submit",
        name: "me-submit",
        attrs: {
          value: '${i18n("common.saveButton")}'
        }
      }
    ];
  }
</script>

<!-- 
В Postgres почти всегда хорошо делать INSERT ON CONFLICT DO UPDATE если вы не боитель роста инкремента в id (если он у вас имеет тип serial или bigserial)
-->
<script type="meta/sql" db-alias="meta_samples" elem="hidden" states="save" id="upsert">
INSERT INTO example_entity (id, name, last_user_id)
VALUES ( COALESCE(NULLIF(:env.objectId, '0')::uuid, uuid_generate_v4()), :env.sp.obj.name, :env.userId )
ON CONFLICT (id) DO UPDATE SET
  name = EXCLUDED.name
RETURNING id;
</script>

<!-- 
elem="hidden" делает скрипт невидимым

states может содержать список стейтов разделенных запятой, в нашем случае и обычно это не нужно. Если states не указан, что скрипт работает на всех стейтах
states="save" говорит о том, что скрипт будет выполнятся только при выполнении условия, что env.state IN (states)
-->
<script type="meta/js" elem="hidden" states="save">
function main(pvm, vm, env, ObjectLogService) {
  // Это простой относительно новый способ узнать работаем ли мы с уже созданным объектом или с новым. 
  // Ранее использовалось сравнение с нулем - env.objectId == '0', но теперь в этом нет необходимости и можно использовать более явное условие
  var isNew = !env.hasObjectId;
  // Получаем ранее вставленный id или id из URL
  var objectId = isNew ? pvm.data.upsert.rows[0].id : env.objectId;

  if (isNew) {
    // Это не обязательно, но обычно менеджер просит заредиректить пользователя на URL карточки свежесозданного объекта
    pvm.redirect = {
      url: "/card?e=" + env.entityId + "&o=" + objectId + "&a=" + env.applicationId
    };
  } else {
    // Говорит о том, что надо вернуться к первоначальному состоянию страницы
    // state=default
    // firstLoading = true
    pvm.toInitialState();
    // Если страница будет открыта в модальном окне эта инструкция будет указывть мете, что окно надо закрыть
    pvm.closeModal();
  }
  // Показать popup. В принципе popup, как и все эти инструкции кроме ObjectLogService.logValue можно ставить в любом порядке, 
  // так как они будут реально выполнены ли на frontend или как toInitialState в конце обработки script
  pvm.popup = {level: "success", message: "${i18n('common.saveSuccess')}"};
  // Залогировать действие над объектом сущности. Будет отгружено в БД и в логи, из логов обычно перемещается в BigQuery
  ObjectLogService.logValue(env.entityId, objectId, isNew ? 'ADD' : 'SET', env.sp.obj);
}
</script>

Код примера

Надо сказать, что обычно state меняется при сабмите формы, просто через html yе меняем, но этот пример может показать что происходит.

Вот простой код для примера. Тут три ссылки, которые меняют состояние страницы. В script-ах указаны states, в которых script будет участвовать (будет выполняться). Если states для script или elem не указан, то script или elem участвует во всех state.

<elem>
<tpl>
  <a ng-click="changeState('first', {'foo':1})">to first</a> | 
  <a ng-click="changeState('two', {'bar':2})">to two</a> |
  <a ng-click="changeState('three', env.sp)">to three</a> |

  <hr>    
  state: {{env.state}}
  <br>
  <pre>{{env.sp|json}}</pre>
</tpl>
</elem>

<script type="meta/sql" db-alias="adplatform" span="4" states="first">
select 1
</script>

<script type="meta/sql" db-alias="adplatform" span="4" states="two">
select 2
</script>

<script type="meta/sql" db-alias="adplatform" span="4" states="first, two">
select 3
</script>

<script type="meta/js" id="w" states="three">
function main(vm, pvm, env) {
  // Говорит мете, что страницу надо вернуть в изначальное состояние с параметрами по умолчанию
  pvm.toInitialState();
}
</script>