Приложения Apla¶
Приложение платформы - это совокупность контрактов, таблиц и интерфейсов совместно реализующих определенный сервис. Приложение не представляет собой автономный программный модуль - элементы приложения объединены только выполнением общего функционала и обменом данными. Границы приложения не всегда строго определены поскольку его элементы могут одновременно использоваться в нескольких приложениях.
Ниже описан типичный функциональный фрагмент приложения:
- Переход на страницу, на которой отражается
- полученная из таблиц базы данных информация (функция DBFind),
- поля формы для ввода новых данных,
- кнопка (функция Button), по которой вызывается контракт.
- При вызове контракта:
- в секцию
data
передаются данные из полей формы, - в секции
conditions
проверяются права пользователя на запуск данного контракта и валидность полученных со страницы данных, при недопустимости выполнения контракта по каким-либо причинам выдается сообщение (error
,warning
илиinfo
), пользователь остается на исходной странице, - в секции
action
получаются дополнительные данные из таблиц (функция DBFind) и выполняется одна или несколько записей в базу данных (DBUpdate, DBInsert).
- После успешного выполнения контракта происходит переход на страницу, имя которой было указано в параметре Page функции функция Button, запустившей контракт, с передачей странице параметров, перечисленных в PageParams.
Страница может иметь несколько кнопок для вызова разных контрактов. Кнопки, вызывающие контракты и реализующие переход на страницы, могут встраиваться в строки таблиц, отображающих в интерфейсе данные об объекта. В этом случае в параметрах кнопок используются идентификаторы объектов, что, например, можно использовать для перехода на страницу редактирования объекта.
Таблицы и особенности хранения данных¶
Таблицы базы данных, используемые в приложении, условно можно разбить на два типа:
- таблицы реестры, в которых хранятся большие массивы структурированных данных об объектах (персонах, организациях, объектах недвижимости и пр.),
- таблицы документов, в которых фиксируются состояния реализуемых приложением бизнес или иных процессов (тип процесса, стадия, подписи и пр.) или данные о текущих операциях (оповещениях, сообщениях, записях о голосованиях, сделках).
Таблицы реестров обычно содержат актуальную информацию, то есть записи об объектах редактируются при поступлении новых данных. При работе с документами реализуется принципиально иной подход. Поскольку функции 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")
.
Права доступа¶
Важнейшей составляющей приложения является система управления правами доступа к его ресурсам. Права устанавливаются на нескольких уровнях:
- Разрешение на вызов конкретного контракта текущим пользователем. Разрешение определяется в секции
conditions
контракта логическим выражением в конструкцииIf
или вложенными контрактами, например, MainConditions, RoleConditions, в которых определяются типовые права или права представителей ролей. - Разрешение текущему пользователю изменять с помощью контрактов значения в колонках таблицы, добавлять в таблицы строки и колонки. Разрешение устанавливается функцией ContractConditions в полях Permissions колонок таблиц и в полях Write permissions / Insert / Update / New column на странице редактирования таблицы. Условия прописанные в поле Update задают права на изменение в целом всех колонок таблицы, условия в полях Permissions накладывают дополнительные ограничения для каждой колонки в отдельности.
- Разрешение на изменение значений в колонках таблицы или добавление в таблицы строк только для конкретных контрактов. Имена контрактов указывается в параметрах функции ContractAccess, которая вписывается в поля Permissions колонок таблиц и в поле Permissions / Insert на странице редактирования таблицы.
- Разрешение на редактирование элементов приложения (контрактов, страниц, меню, страничных блоков). Разрешение задается в полях 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)
}
}