Приложения Apla

Приложение платформы - это совокупность контрактов, таблиц и интерфейсов совместно реализующих определенный сервис. Приложение не представляет собой автономный программный модуль - элементы приложения объединены только выполнением общего функционала и обменом данными. Границы приложения не всегда строго определены поскольку его элементы могут одновременно использоваться в нескольких приложениях.

Ниже описан типичный функциональный фрагмент приложения:

  1. Переход на страницу, на которой отражается
  • полученная из таблиц базы данных информация (функция DBFind),
  • поля формы для ввода новых данных,
  • кнопка (функция Button), по которой вызывается контракт.
  1. При вызове контракта:
  • в секцию data передаются данные из полей формы,
  • в секции conditions проверяются права пользователя на запуск данного контракта и валидность полученных со страницы данных, при недопустимости выполнения контракта по каким-либо причинам выдается сообщение (error, warning или info), пользователь остается на исходной странице,
  • в секции action получаются дополнительные данные из таблиц (функция DBFind) и выполняется одна или несколько записей в базу данных (DBUpdate, DBInsert).
  1. После успешного выполнения контракта происходит переход на страницу, имя которой было указано в параметре Page функции функция Button, запустившей контракт, с передачей странице параметров, перечисленных в PageParams.

Страница может иметь несколько кнопок для вызова разных контрактов. Кнопки, вызывающие контракты и реализующие переход на страницы, могут встраиваться в строки таблиц, отображающих в интерфейсе данные об объекта. В этом случае в параметрах кнопок используются идентификаторы объектов, что, например, можно использовать для перехода на страницу редактирования объекта.

Таблицы и особенности хранения данных

Таблицы базы данных, используемые в приложении, условно можно разбить на два типа:

  1. таблицы реестры, в которых хранятся большие массивы структурированных данных об объектах (персонах, организациях, объектах недвижимости и пр.),
  2. таблицы документов, в которых фиксируются состояния реализуемых приложением бизнес или иных процессов (тип процесса, стадия, подписи и пр.) или данные о текущих операциях (оповещениях, сообщениях, записях о голосованиях, сделках).

Таблицы реестров обычно содержат актуальную информацию, то есть записи об объектах редактируются при поступлении новых данных. При работе с документами реализуется принципиально иной подход. Поскольку функции DBFind языков Simvolio и Protypo могут обращаться только к одной таблице (то есть не поддерживают JOIN), то при создании таблиц, хранящих документы, следует записывать в них исчерпывающую информацию (названия, имена, изображения). К примеру, обращаясь к таблице, хранящей сообщения, мы должны получить не id пославшего сообщение пользователя, а полную информацию, необходимую для отображения сообщения на странице, включая имя и аватарку пользователя. Данные в таблицах документов не должны изменяться. Следует особо отметить, что отказ от нормализации данных в таблицах, связан не только с техническими ограничениями на использование JOIN, а в большей степени, предписывается самой идеологией блокчейна как темпоральной базой данных, призванной хранить полную историю событий. Это означает, что документ (скажем, сообщение), подписанный и сохраненный в таблице не должен ни при каких условиях быть изменен в будущем, скажем, при смене имени его автора в реестре пользователей (что неизбежно при реляционной схеме данных).

Навигация

Переходы между отдельными приложениями осуществляется либо с помощью разделов, отображаемых в программном клиенте вкладками, либо внутри одного раздела с помощью вложенных меню. При клике на вкладке раздела осуществляется переход на главную страницу раздела, которая задается в административной зоне.

Навигация внутри приложения прежде всего осуществляется с помощью меню (к каждой странице привязывается одно меню). Возможен переход между страницам по ссылкам (LinkPage) или кликами на кнопках (Button). В обоих случаях доступна передача параметров PageParams целевой странице Page. Вызов новой страницы также возможен и после успешного выполнения контракта. Для этого в параметрах функции Button, с помощью которой вызывается контракт, необходимо указать имя страницы перехода Page и передаваемые параметры PageParams.

В многопользовательских приложениях удобно организовывать навигацию с помощью установленных по умолчанию в экосистемах приложений Notification и Roles. Сообщения приложени Notification для конкретного пользователя или представителей заданной роли отображаются в программном клиенте Molis. Сообщение содержит заголовок и ссылку на заданную страницу. Также возможно передать адресной странице параметры PageParams в формате, принимаемом функциями LinkPage и Button. Нередки ситуации, когда на адресную страницу, например, на которой пользователю надо принять некоторое решение, возможно попасть только через оповещение - никакие ссылки из меню или с других страниц на нее не ведут. Оповещение формируется контрактами Notifications_Single_Send или Notifications_Roles_Send, которые запускаются из контракта, завершающего некоторый функциональный этап приложения. После получения пользователем оповещения, перехода на адресную страницу и выполнения им необходимого действия, то есть запуска требуемого контракта, оповещение гасится вызываемыми в этом контракте специальными контрактами Notifications_Single_Close или Notifications_Roles_Finishing. Список и статус оповещений отображается на специальной странице приложения Notification.

Интерфейсные страницы

Структура страницы формируется с помощью условного оператора If(Condition){Body } .ElseIf(Condition){Body} .Else{Body}, в котором в условиях используются параметры PageParam передаваемые странице при ее вызове функциями LinkPage и Button. При необходимости использовать на многих страницах идентичные фрагменты кода, их необходимо записывать в страничные блоки. Вызываются блоки функцией Include.

Переменные, названия колонок и языковые ресурсы

Значительно ускоряет программирование приложений и упрощает чтение кода унификация имен переменных (на страницах и в контрактах), идентификаторов полей страничных форм, имен колонок таблиц и лейблов языковых ресурсов. Если имя поля формы username совпадает с именем переменной username в секции data контракта, в которую передается значение из данного поля, то эту пару (username=username) не обязательно указывать в параметрах Params в функции Button. Совпадение имен переменных и имен колонок упрощает написание функций DBInsert и DBUpdate, например, DBUpdate("member", $id, {username: $username}). Совпадение имен переменных и лейбла языкового ресурса удобно при выводе названий колонок интерфейсных таблиц Table(mysrc,"ID=id,$username$=username").

Права доступа

Важнейшей составляющей приложения является система управления правами доступа к его ресурсам. Права устанавливаются на нескольких уровнях:

  1. Разрешение на вызов конкретного контракта текущим пользователем. Разрешение определяется в секции conditions контракта логическим выражением в конструкции If или вложенными контрактами, например, MainConditions, RoleConditions, в которых определяются типовые права или права представителей ролей.
  2. Разрешение текущему пользователю изменять с помощью контрактов значения в колонках таблицы, добавлять в таблицы строки и колонки. Разрешение устанавливается функцией ContractConditions в полях Permissions колонок таблиц и в полях Write permissions / Insert / Update / New column на странице редактирования таблицы. Условия прописанные в поле Update задают права на изменение в целом всех колонок таблицы, условия в полях Permissions накладывают дополнительные ограничения для каждой колонки в отдельности.
  3. Разрешение на изменение значений в колонках таблицы или добавление в таблицы строк только для конкретных контрактов. Имена контрактов указывается в параметрах функции ContractAccess, которая вписывается в поля Permissions колонок таблиц и в поле Permissions / Insert на странице редактирования таблицы.
  4. Разрешение на редактирование элементов приложения (контрактов, страниц, меню, страничных блоков). Разрешение задается в полях Change conditions в редакторах элементов. Делается это с помощью функции ContractConditions, которой в качестве параметра передается имя контракта, проверяющего права текущего пользователя.

Пример приложения SendTokens

Приложение реализует пересылку токенов с одного пользовательского аккаунта на другой. Суммы токенов на аккаунтах фиксируются в таблицах keys (колонка amount), устанавливаемых в экосистемах по умолчанию. В примере подразумевается, что токены уже распределены по аккаунтам.

Системный контракт

Основным для этого приложения является контракт TokenTransfer, которому предоставляется исключительное право изменять значения в колонке amount таблицы keys. Для реализации этого права в поля Permissions колонки записывается функция ContractAccess("TokenTransfer"). Теперь все операции с токенами возможны только через вызов TokenTransfer.

Чтобы избежать вызов контракта TokenTransfer внутри другого контракта незаметно от владельца аккаунта, TokenTransfer должен быть оформлен как контракт с подтверждением, то есть в секции data у него должна быть строка Signature string "optional hidden", а на странице Контракты с подтверждением административного раздела Molis должны быть введены параметры подтверждения: текст, выводимый в сплывающем окне, и отображаемые в окне параметры (подробнее см. Контракты с подтверждением).

contract TokenTransfer {
data {
    Amount money
    Sender_AccountId int
    Recipient_AccountId int
    Signature string "optional hidden"
}
conditions {
    //check the sender
    $sender = DBFind("keys").Where("id=$", $Sender_AccountId)
    if(Len($sender) == 0){
        error Sprintf("Sender %s is invalid", $Sender_AccountId)
    }
    $vals_sender = $sender[0]

    //check the recipient
    $recipient = DBFind("keys").Where("id=$", $Recipient_AccountId)
    if(Len($recipient) == 0){
        error Sprintf("Recipient %s is invalid", $Recipient_AccountId)
    }
    $vals_recipient = $recipient[0]

    //check amount
    if $Amount == 0 {
        error "Amount is zero"
    }

    //check balance
    var sender_balance money
    sender_balance = Money($vals_sender["amount"])
    if $Amount > sender_balance {
        error Sprintf("Money is not enough %v < %v", sender_balance, $Amount)
    }
}
action {
    DBUpdate("keys", $Sender_AccountId, {"-amount": $Amount})
    DBUpdate("keys", $Recipient_AccountId, {"+amount": $Amount})
}
}

В секции conditions контракта TokenTransfer проверяется наличие аккаунтов, неравенство нулю переводимого количества токенов и баланс аккаунта, с которого производится перевод. В секции action производится изменение значений в колонке amount аккаунтов отправителя и получателя.

Форма отправки токенов

Форма для отправки токенов содержит поля для ввода суммы токенов и адреса аккаунта получателя.

Div(Class: panel panel-default){
  Form(){
    Div(Class: list-group-item text-center){
      Span(Class: h3, Body: LangRes(SendTokens))
    }
    Div(Class: list-group-item){
      Div(Class: row df f-valign){
        Div(Class: col-md-3 mt-sm text-right){
          Label(For: Recipient_Account){
            Span(Body: LangRes(Recipient_Account))
          }
        }
        Div(Class: col-md-9 mb-sm text-left){
          Input(Name: Recipient_Account, Type: text, Placeholder: "xxxx-xxxx-xxxx-xxxx")
        }
      }
      Div(Class: row df f-valign){
        Div(Class: col-md-3 mt-sm text-right){
          Label(For: Amount){
            Span(Body: LangRes(Amount))
          }
        }
        Div(Class: col-md-9 mc-sm text-left){
          Input(Name: Amount, Type: text, Placeholder: "0", Value: "5000000")
        }
      }
    }
    Div(Class: panel-footer clearfix){
      Div(Class: pull-right){
        Button(Body: LangRes(send), Contract: SendTokens, Class: btn btn-default)
      }
    }
  }
}

В функции Button возможно было бы сразу вызвать контракт TokenTransfer с передачей ему адреса аккаунта текущего пользователя, который переводит токены, но для демонстрации работы контрактов с подтверждением создадим промежуточный пользовательский контракт SendTokens. Отметим, что поскольку названия данных в секции data контракта и имена полей формы совпадают, то в функции Button не указаны передаваемые параметры Params.

Форма может быть размещена на любой странице в программного клиента. После выполнения контракта пользователь останется на текущей странице (в Button не указана адресная страница Page).

Пользовательский контракт

Поскольку TokenTransfer определен как контракт с подтверждением, то для его вызова из другого контракта необходимо в секции data иметь строку Signature string «signature:TokenTransfer». В секции conditions контракта SendTokens проверяется наличие аккаунта, а в action вызывается контракт TokenTransfer с передачей ему параметров.

contract SendTokens {
    data {
        Amount money
        Recipient_Account string
        Signature string "signature:TokenTransfer"
    }

    conditions {
        $recipient = AddressToId($Recipient_Account)
        if $recipient == 0 {
            error Sprintf("Recipient %s is invalid", $Recipient_Account)
        }
    }

    action {
        TokenTransfer("Amount,Sender_AccountId,Recipient_AccountId,Signature", $Amount, $key_id, $recipient, $Signature)
    }
}