Javascript платформа Objectum

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Если вам нужен простой способ создавать веб-приложения, используя только javascript (full-stack), то предлагаю вам ознакомиться с платформой objectum. Новая версия платформы является результатом опыта работы над предыдущей версией, которая используется 10 лет. Обе версии используются в разработке различных информационных систем — это региональные решения и системы для организаций. Платформа новой версии уже используется на продакшн серверах и будет развиваться длительное время. Далее подробности.


logo


Скрины из существующих разработок


Пример веб-приложения:




Пример 1




Пример сайта (без серверного рендера):




Пример 2




Пакеты платформы


Платформа состоит из следующих npm пакетов: objectum, objectum-client, objectum-proxy, objectum-react, objectum-cli


objectum


Собственно сама платформа, сервер приложений. Реализован ORM для работы с PostgreSQL. Работает достаточно быстро благодаря набору триггеров и функций (Objectum Database Engine), написанных на PL/pgSQL. Кэширование в Redis. Может работать в node cluster. Изменения данных журналируются и в случае чего виновника найти не проблема.


objectum-client


Изоморфный клиент для взаимодействия с objectum-proxy или objectum. Исходный код, работающий с хранилищем, без изменений можно запускать на клиенте или сервере. Клиент удобный, но если нужно, то взаимодействовать с сервером можно без клиента т.к. запросы, ответы в JSON.


objectum-proxy


Прокси расположен между objectum и objectum-client. Выполняет следующие задачи:


  • Выполнение серверных методов моделей
  • Выполнение серверных методов с правами администратора
  • Контроль доступа к базе данных. Отслеживаются все действия — это действия CRUD и выборка данных через SQL.
  • Загрузка и получение файлов

objectum-react


Набор react компонентов UI. Используются bootstrap и fontawesome. Библиотеки redux, mobx не используются.
Оформление подключается в App.js. Под каждый проект использую разные bootstrap.css:


import "objectum-react/lib/css/bootstrap.css";
import "objectum-react/lib/css/objectum.css";
import "objectum-react/lib/fontawesome/css/all.css";

objectum-cli


Утилита командной строки. Выполняет сервисные действия с проектами, включая базы данных. Удобен для быстрого добавления моделей и записей.


Разработка


Чтобы не быть многословным, в этой статье напишу о самом простом подходе, где не нужно создавать свои компоненты React. В своих проектах я стараюсь по максимуму всю систему сделать таким способом. Для сложных интерфейсов нужно писать свои компоненты, где удобно подключать компоненты из objectum-react.
Теперь по порядку как создать проект objectum. Перед началом убедитесь, что у вас установлены NodeJS, PostgreSQL и Redis.
Готовый демо-проект catalog вы можете установить отсюда.




catalog




Установка платформы


Устанавливаем утилиту командной строки:


npm i -g objectum-cli

Устанавливаем платформу в папку /opt/objectum


mkdir /opt/objectum
objectum-cli --create-platform --path /opt/objectum

где параметры по умолчанию:


--redis-host 127.0.0.1 - хост и порт сервера Redis
--redis-port 6379
--objectum-port 8200 - порт, где будет работать objectum

Создание проекта


Создаем проект "catalog":


objectum-cli --create-project catalog --path /opt/objectum

Параметры по умолчанию:


--project-port 3100 - порт, где будет работать проект
--db-host 127.0.0.1 - хост и порт сервера PostgreSQL
--db-port 5432
--db-dbPassword 1 - пароль пользователя catalog
--db-dbaPassword 12345 - пароль администратора postgres
--password admin - пароль администратора проекта

Папка проекта создается инструментом create-react-app. На клиенте и сервере модули подключаются как ES Modules.


Запуск


Запуск платформы:


cd /opt/objectum/server 
node index-8200.js

Запуск проекта:


cd /opt/objectum/projects/catalog 
node index-3100.js
npm start

Должна открыться ссылка http://localhost:3000
В окне авторизации в полях логин, пароль вводим: admin




auth




Инструменты разработчика


Меню "Objectum" содержит пункты:


  • Модели — создание моделей и свойств
  • Запросы — создание SQL запросов для выборки данных
  • Меню — конструктор меню для ролей пользователей
  • Роли — здесь задается список ролей пользователей
  • Пользователи — добавление пользователей



auth




Пакетное добавление данных в хранилище


Структура хранилища формируется с помощью UI, но для быстрого добавления большого количества данных лучше использовать objectum-cli.


Импорт JSON


cd /opt/objectum/projects/catalog  
objectum-cli --import-json scripts/catalog-cli.json --file-directory scripts/files

Здесь скрипт catalog-cli.json
{
    "createModel": [
        {
            "name": "Item", 
            "code": "item"
        },
        {
            "name": "Item", 
            "code": "item",
            "parent": "d"
        },
        {
            "name": "Type", 
            "code": "type",
            "parent": "d.item"
        },
        {
            "name": "Item", 
            "code": "item",
            "parent": "t"
        },
        {
            "name": "Comment", 
            "code": "comment",
            "parent": "t.item"
        }
    ],
    "createProperty": [
        {
            "model": "d.item.type", 
            "name": "Name", 
            "code": "name",
            "type": "string"
        },
        {
            "model": "t.item.comment", 
            "name": "Item", 
            "code": "item",
            "type": "item"
        },
        {
            "model": "t.item.comment", 
            "name": "Date",
            "code": "date",
            "type": "date"
        },
        {
            "model": "t.item.comment",
            "name": "Text",
            "code": "text",
            "type": "string"
        },
        {
            "model": "item", 
            "name": "Date", 
            "code": "date",
            "type": "date"
        },
        {
            "model": "item", 
            "name": "Name", 
            "code": "name",
            "type": "string"
        },
        {
            "model": "item",
            "name": "Description",
            "code": "description",
            "type": "string",
            "opts": {
                "wysiwyg": true
            }
        },
        {
            "model": "item", 
            "name": "Cost", 
            "code": "cost",
            "type": "number",
            "opts": {
                "min": 0
            }
        },
        {
            "model": "item", 
            "name": "Type", 
            "code": "type",
            "type": "d.item.type"
        },
        {
            "model": "item", 
            "name": "Photo", 
            "code": "photo",
            "type": "file",
            "opts": {
                "image": {
                    "width": 300,
                    "height": 200,
                    "aspect": 1.5
                }
            }
        }
    ],
    "createQuery": [
        {
            "name": "Item",
            "code": "item"
        },
        {
            "name": "List",
            "code": "list",
            "parent": "item",
            "query": [
                "{\"data\": \"begin\"}",
                "select",
                "    {\"prop\": \"a.id\", \"as\": \"id\"},",
                "    {\"prop\": \"a.name\", \"as\": \"name\"},",
                "    {\"prop\": \"a.description\", \"as\": \"description\"},",
                "    {\"prop\": \"a.cost\", \"as\": \"cost\"},",
                "    {\"prop\": \"a.date\", \"as\": \"date\"},",
                "    {\"prop\": \"a.photo\", \"as\": \"photo\"},",
                "    {\"prop\": \"a.type\", \"as\": \"type\"}",
                "{\"data\": \"end\"}",
                "",
                "{\"count\": \"begin\"}",
                "select",
                "    count (*) as num",
                "{\"count\": \"end\"}",
                "",
                "from",
                "    {\"model\": \"item\", \"alias\": \"a\"}",
                "",
                "{\"where\": \"empty\"}",
                "",
                "{\"order\": \"empty\"}",
                "",
                "limit {\"param\": \"limit\"}",
                "offset {\"param\": \"offset\"}"
            ]
        },
        {
            "name": "Item",
            "code": "item",
            "parent": "t"
        },
        {
            "name": "Comment",
            "code": "comment",
            "parent": "t.item",
            "query": [
                "{\"data\": \"begin\"}",
                "select",
                "    {\"prop\": \"a.id\", \"as\": \"id\"},",
                "    {\"prop\": \"a.item\", \"as\": \"item\"},",
                "    {\"prop\": \"a.date\", \"as\": \"date\"},",
                "    {\"prop\": \"a.text\", \"as\": \"text\"}",
                "{\"data\": \"end\"}",
                "",
                "{\"count\": \"begin\"}",
                "select",
                "    count (*) as num",
                "{\"count\": \"end\"}",
                "",
                "from",
                "    {\"model\": \"t.item.comment\", \"alias\": \"a\"}",
                "",
                "{\"where\": \"empty\"}",
                "",
                "{\"order\": \"empty\"}",
                "",
                "limit {\"param\": \"limit\"}",
                "offset {\"param\": \"offset\"}"
            ]
        }
    ],
    "createRecord": [
        {
            "_model": "d.item.type",
            "name": "Videocard",
            "_ref": "videocardType"
        },
        {
            "_model": "d.item.type",
            "name": "Processor"
        },
        {
            "_model": "d.item.type",
            "name": "Motherboard"
        },
        {
            "_model": "objectum.menu",
            "name": "User",
            "code": "user",
            "_ref": "userMenu"
        },
        {
            "_model": "objectum.menuItem",
            "menu": {
                "_ref": "userMenu"
            },
            "name": "Items",
            "icon": "fas fa-list",
            "order": 1,
            "path": "/model_list/item"
        },
        {
            "_model": "objectum.menuItem",
            "menu": {
                "_ref": "userMenu"
            },
            "name": "Dictionary",
            "icon": "fas fa-book",
            "order": 2,
            "_ref": "dictionaryMenuItem"
        },
        {
            "_model": "objectum.menuItem",
            "menu": {
                "_ref": "userMenu"
            },
            "name": "Item type",
            "icon": "fas fa-book",
            "parent": {
                "_ref": "dictionaryMenuItem"
            },
            "order": 1,
            "path": "/model_list/d_item_type"
        },
        {
            "_model": "objectum.role",
            "name": "User",
            "code": "user",
            "menu": {
                "_ref": "userMenu"
            },
            "_ref": "userRole"
        },
        {
            "_model": "objectum.user",
            "name": "User",
            "login": "user",
            "password": "user",
            "role": {
                "_ref": "userRole"
            }
        },
        {
            "_model": "objectum.menu",
            "name": "Guest",
            "code": "guest",
            "_ref": "guestMenu"
        },
        {
            "_model": "objectum.menuItem",
            "menu": {
                "_ref": "guestMenu"
            },
            "name": "Items",
            "icon": "fas fa-list",
            "order": 1,
            "path": "/model_list/item"
        },
        {
            "_model": "objectum.role",
            "name": "Guest",
            "code": "guest",
            "menu": {
                "_ref": "guestMenu"
            },
            "_ref": "guestRole"
        },
        {
            "_model": "objectum.user",
            "name": "Guest",
            "login": "guest",
            "password": "guest",
            "role": {
                "_ref": "guestRole"
            }
        },
        {
            "_model": "item",
            "name": "RTX 2080",
            "description": [
                "<ul>",
                "<li>11GB GDDR6</span></li>",
                "<li>CUDA Cores: 4352</span></li>",
                "<li>Display Connectors: DisplayPort, HDMI, USB Type-C</span></li>",
                "<li>Maximum Digital Resolution: 7680x4320</span></li>",
                "</ul>"
            ],
            "date": "2020-06-03T19:27:38.292Z",
            "type": {
                "_ref": "videocardType"
            },
            "cost": "800",
            "photo": "rtx2080.png"
        }
    ]
}

Что всё это значит:


  • createModel — создание моделей, где name — наименование, code — текстовый идентификатор, parent — родитель. Иерархия используется для группировки моделей.
    • В примере создаются модели item, d.item.type, t.item.comment.
    • Модель "d.item.type" — это справочник. В модели "item" ссылка на справочник из свойства "type". Все справочники рекомендуется добавлять в узел "d".
    • Модель "t.item.comment" — это табличная часть. Комментарии имеют ссылку на "item". Все табличные части рекомендуется добавлять в узел "t".
  • createProperty — создание свойств моделей, где name — наименование, code — текстовый идентификатор, model — модель, type — тип данных в т.ч. ссылка на любую модель, opts — дополнительные параметры свойства, например, отображение текстового поля как wysiwyg редактор.
  • createQuery — создание SQL запросов с JSON вставками блоков, параметров, моделей, свойств.
    • [] — с помощью массива добавляется читаемый многострочный текст.
    • блоки {"...": "begin"}...{"...": "end"} помогают парсеру строить SQL запросы для различных целей: выборка (data), расчет кол-ва записей (count), фильтрация (where), сортировка (order), расчет кол-ва дочерних узлов (tree).
    • модель {"model": "item", "alias": "a"} конвертируется в название таблицы "код_id a".
    • свойство {"prop": "a.name"} конвертируется в название столбца "a.код_id".
    • параметр {"prop": "limit"} должен быть передан запросу при выполнении.
  • createRecord — добавление записей моделей.
    • _model — модель
    • name — сохраняем любые свойства
    • [] — с помощью массива добавляется читаемый многострочный текст.
    • _ref — с помощью этой команды добавляем ссылки на другие записи т.к. заранее id записи неизвестно.
    • JSON конвертируется в текст.
    • "photo": "rtx2080.png" — добавление файлов. В photo запишется "rtx2080.png" и загрузится файл из папки scripts/files.

Импорт CSV


cd /opt/objectum/projects/catalog  
objectum-cli --import-csv scripts/stationery.csv --model item --file-directory/script/files --handler scripts/csv-handler.js
objectum-cli --import-csv scripts/tv.csv --model item --file-directory/script/files --handler scripts/csv-handler.js

Возможности импорта CSV:


  • Импортирует строки из CSV как записи в указанную модель (item)
  • Импортирует файлы (изображения) из указанного каталога
  • С помощью обработчика загрузки меняются записи при добавлении (csv-handler.js):
    • Скрипт дополняет параметры в справочники в т.ч. в древовидный (категория)

Исходный код моделей


Исходный код ItemModel подключается на клиенте и сервере. Для добавления ReactJS или NodeJS кода нужно разделить код на ItemClientModel, ItemServerModel или на ItemModel, ItemClientModel extends ItemModel, ItemServerModel extends ItemModel.


Клиент


Подключение в App.js:


import ItemModel from "./models/ItemModel";

store.register ("item", ItemModel);

Теперь создаваемые записи модели "item" будут экземплярами класса ItemModel:


let record = await store.createRecord ({
    _model: "item",
    name: "Foo"
});

Что это дает:


  • Прямое обращение к свойствам record.name = "Bar";
  • Ссылка на хранилище record.store
  • Сохранение изменений await record.sync ()

ItemModel.js
import React from "react";
import {Record, factory} from "objectum-client";
import {Action} from "objectum-react";

class ItemModel extends Record {
    static _renderGrid ({grid, store}) {
        return React.cloneElement (grid, {
            label: "Items", // grid label
            query: "item.list", // grid query
            onRenderTable: ItemModel.onRenderTable, // grid table custom render
            children: store.roleCode === "guest" ? null : <div className="d-flex">
                {grid.props.children}
                <Action label="Server action: getComments" onClickSelected={async ({progress, id}) => {
                    let recs = await store.remote ({
                        model: "item",
                        method: "getComments",
                        id,
                        progress
                    });
                    return JSON.stringify (recs)
                }} />
            </div>
        });
    }

    static onRenderTable ({grid, cols, colMap, recs, store}) {
        return (
            <div className="p-1">
                {recs.map ((rec, i) => {
                    let record = factory ({rsc: "record", data: Object.assign (rec, {_model: "item"}), store});

                    return (
                        <div key={i} className={`row border-bottom my-1 p-1 no-gutters ${grid.state.selected === i ? "bg-secondary text-white" : ""}`} onClick={() => grid.onRowClick (i)} >
                            <div className="col-6">
                                <div className="p-1">
                                    <div>
                                        <strong className="mr-1">Name:</strong>{rec.name}
                                    </div>
                                    <div>
                                        <strong className="mr-1">Date:</strong>{rec.date && rec.date.toLocaleString ()}
                                    </div>
                                    <div>
                                        <strong className="mr-1">Type:</strong>{rec.type && store.dict ["d.item.type"][rec.type].name}
                                    </div>
                                    <div>
                                        <strong className="mr-1">Cost:</strong>{rec.cost}
                                    </div>
                                    <div>
                                        <strong>Description:</strong>
                                    </div>
                                    <div dangerouslySetInnerHTML={{__html: `${record.description || ""}`}} />
                                </div>
                            </div>
                            <div className="col-6 text-right">
                                {record.photo && <div>
                                     <img src={record.getRef ("photo")} className="img-fluid" width={400} height={300} alt={record.photo} />
                                </div>}
                            </div>
                        </div>
                    );
                })}
            </div>
        );
    }

    // item form layout
    static _layout () {
        return {
            "Information": [
                "id",
                [
                    "name", "date"
                ],
                [
                    "type", "cost"
                ],
                [
                    "description"
                ],
                [
                    "photo"
                ],
                [
                    "t.item.comment"
                ]
            ]
        };
    }

    static _renderForm ({form, store}) {
        return React.cloneElement (form, {
            defaults: {
                date: new Date ()
            }
        });
    }

    // new item render
    static _renderField ({field, store}) {
        if (field.props.property === "date") {
            return React.cloneElement (field, {showTime: true});
        } else {
            return field;
        }
    }

    // item render
    _renderField ({field, store}) {
        return ItemModel._renderField ({field, store});
    }
};

export default ItemModel;

В моделях зарезервированы названия методов для решения различных задач:


  • _renderGrid — модель "item" имеет отображение по умолчанию в маршруте /model_list/item. Отображает компонент ModelList, который вызывает данный метод при рендере Grid.
  • _layout — по маршруту /model_record/:id запись отображает компонент ModelRecord, который вызывает данный метод для получения разметки. Если метод не определен, то макет формы используется стандартный, где все поля по одной на строку, а табличные части в отдельных закладках формы.
  • _renderForm — рендер формы
  • _renderField — рендер поля. Статичный метод для новой записи и обычный метод для существующей записи.

Модифицированная форма:




auth




Сервер


Подключение в index.js:


import ItemModel from "./src/models/ItemServerModel.js";

proxy.register ("item", ItemModel);

Методы и работа с хранилищем store такая же. Сессии между пользователями разделены.


ItemServerModel.js
import objectumClient from "objectum-client";
const {Record} = objectumClient;

function timeout (ms = 500) {
    return new Promise (resolve => setTimeout (() => resolve (), ms));
};

class ItemModel extends Record {
    async getComments ({progress}) {
        for (let i = 0; i < 10; i ++) {
            await timeout (1000);
            progress ({label: "processing", value: i + 1, max: 10});
        }
        return await this.store.getRecs ({
            model: "t.item.comment",
            filters: [
                ["item", "=", this.id]
            ]
        });
    }
};

export default ItemModel;

С клиента серверные методы вызываются так:


getComments () {
    return await store.remote ({
        model: "item",
        method: "getComments",
        myArg: "" 
    });
}

Доступ


Подключение в index.js:


import accessMethods from "./src/modules/access.js";

proxy.registerAccessMethods (accessMethods);

access.js
let map = {
    "guest": {
        "data": {
            "model": {
                "item": true, "d.item.type": true, "t.item.comment": true
            },
            "query": {
                "objectum.userMenuItems": true
            }
        },
        "read": {
            "objectum.role": true, "objectum.user": true, "objectum.menu": true, "objectum.menuItem": true
        }
    }
};
async function _init ({store}) {
};

function _accessData ({store, data}) {
    if (store.roleCode == "guest") {
        if (data.model) {
            return map.guest.data.model [store.getModel (data.model).getPath ()];
        }
        if (data.query) {
            return map.guest.data.query [store.getQuery (data.query).getPath ()];
        }
    } else {
        return true;
    }
};

function _accessFilter ({store, model, alias}) {
};

function _accessCreate ({store, model, data}) {
    return store.roleCode != "guest";
};

function _accessRead ({store, model, record}) {
    let modelPath = model.getPath ();

    if (store.roleCode == "guest") {
        if (modelPath == "objectum.user") {
            return record.login == "guest";
        }
        return map.guest.read [modelPath];
    }
    return true;
};

function _accessUpdate ({store, model, record, data}) {
    return store.roleCode != "guest";
};

function _accessDelete ({store, model, record}) {
    return store.roleCode != "guest";
};

export default {
    _init,
    _accessData,
    _accessFilter,
    _accessCreate,
    _accessRead,
    _accessUpdate,
    _accessDelete
};

Любой запрос к хранилищу можно запретить или ограничить. Доступные методы:


  • _init — инициализация модуля.
  • _accessCreate — создание записей.
  • _accessRead — чтение записей.
  • _accessUpdate — изменений записей.
  • _accessDelete — удаление записей.
  • _accessData — выборка данных методом getData
  • _accessFilter — для каждой модели в SQL запросе вызывается этот метод. Например ограничиваем конкретному пользователю выборку записей. Т.о. в любом новом запросе ограничения будут работать.

Действия можно запретить или ограничить, например разрешать изменять только набор свойств определенной роли.
Создание, изменение, удаление моделей, свойств, запросов и столбцов доступно только суперпользователю admin.


Действия администратора


Иногда нужно выполнить серверное действие с максимальными правами. Это может быть регистрация пользователя или какая-то обратная связь.
Подключение в index.js:


import adminMethods from "./src/modules/admin.js";

proxy.registerAdminMethods (adminMethods);

admin.js
import fs from "fs";
import util from "util";

fs.readFileAsync = util.promisify (fs.readFile);

function timeout (ms = 500) {
    return new Promise (resolve => setTimeout (() => resolve (), ms));
};

async function readFile ({store, progress, filename}) {
    for (let i = 0; i < 10; i ++) {
        await timeout (1000);
        progress ({label: "processing", value: i + 1, max: 10});
    }
    return await fs.readFileAsync (filename, "utf8");
};

async function increaseCost ({store, progress}) {
    await store.startTransaction ("demo");

    let records = await store.getRecords ({model: "item"});

    for (let i = 0; i < records.length; i ++) {
        let record = records [i];

        record.cost = record.cost + 1;
        await record.sync ();
    }
    await store.commitTransaction ();

    return "ok";
};

export default {
    readFile,
    increaseCost
};

Как видно из admin.js. Здесь читаем файлы и меняем данные из под любой учетной записи пользователя (guest).
С клиента вызов такой:


await store.remote ({
    model: "admin",
    method: "readFile",
    filename: "package.json"
});

Компоненты React


Библиотека содержит компоненты:


  • ObjectumApp — веб-приложение
  • ObjectumRoute — маршрут
  • Auth — форма авторизации
  • Grid — таблица
    • Параметр tree включает древовидную таблицу
  • Form — форма
  • Tabs, Tab — закладки
  • Fields — поля для разных типов данных
    • StringField — строка. Есть опции: textarea, wysiwyg
    • NumberField — число
    • BooleanField — чекер
    • DateField — дата. Параметр showTime добавляет время.
    • FileField — файл (изображение). Есть обрезание изображения перед загрузкой.
    • DictField — выбор из справочника
    • SelectField — выбор из списка
    • ChooseField — ссылка на запись. Выбор элемента из другого компонента
    • JsonField — составное поле. Содержит в себе любое количество полей различного типа. Значения сохраняются в виде строки JSON
    • Field — используется внутри формы. Тип поля выбирается автоматически
    • JsonEditor — многострочный редактор свойств JSON
  • Tooltip — всплывающая подсказка
  • Fade — анимация отображения
  • Action — кнопка для выполнения функции

ObjectumApp


props:


  • locale — локализация. Есть "ru".
  • onCustomRender — свой рендер приложения
  • username, password — автоматическая авторизация под выбранным пользователем (guest).

Grid


Для выборки данных компоненту нужно указать запрос (query) или модель (model). Предоставляет следующие функции:


  • Сортировка по выбранному столбцу
  • Фильтры по любым столбцам в соответствии с типом данных столбца
    • Сохранение настроенных фильтров в localStorage
  • Выбор отображаемых столбцов

Form


Форма группирует поля. Позволяет сохранить изменения, и загрузить файлы. Кнопка "Изменения" в форме показывает таблицу изменений по выбранному полю. Видно какой пользователь, когда и с какого IP-адреса внес изменения.


Action


Компонент предоставляет удобный запуск функций:


  • Параметр confirm — подтверждение выполнения
  • Функция может быть синхронной или асинхронной
  • Во время выполнения:
    • Отображение прогресса выполнения, включая выполнение на сервере
    • Запрос подтверждения во время выполнения
    • В случае исключения показывает ошибку
  • Показывает результат если функция вернула строку

Отчеты


Для построения несложных отчетов используется createReport, который строит XLSX отчет. Пример:


import {createReport} from "objectum-react";

let recs = await store.getRecs ({model: "item"});
let rows = [
    [
        {text: "Список", style: "border_center", colSpan: 3}
    ],
    [
        {text: "Наименование", style: "border"},
        {text: "Дата", style: "border"},
        {text: "Стоимость", style: "border"}
    ],
    ...recs.map (rec => {
        return [
            {text: rec.name, style: "border"},
            {text: rec.date.toLocaleString (), style: "border"},
            {text: rec.cost, style: "border"}            
        ];
    })
];
createReport ({
    rows,
    columns: [40, 10, 10],
    font: {
        name: "Arial",
        size: 10
    }
});

Где:


  • rows — это строки (row) отчета
  • colSpan, rowSpan — работают как в HTML таблице
  • columns — ширина колонок

Развертывание


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


  • Я использую такую схему
    • БД catalog_dev — разработка
    • БД catalog_test (импорт из catalog_dev) — тестирование
    • БД catalog_"идентификатор клиента" (импорт из catalog_dev) — продакшн
  • Общие и региональные справочники
    • БД region_dev — разработка и общие справочники
    • БД region_"идентификатор региона" (импорт из region_dev) — модификация общих справочников и добавление региональных параметров в справочники
    • БД region"идентификатор клиента региона" (импорт из region"идентификатор региона") — продакшн
  • В одну БД можете импортировать несколько других БД, но в реальности мне это не понадобилось

Экспорт схемы catalog:


let $o = require ("../../server/objectum");

$o.db.execute ({
    "code": "catalog",
    "fn": "export",
    "exceptRecords": ["item"],
    "file": "../schema/schema-catalog.json"
});

Параметр exceptRecords отключает экспорт записей по выбранным моделям, включая дочерние модели.


Импорт схемы:


let $o = require ("../../../server/objectum");

$o.db.execute ({
    "code": "catalog_test",
    "fn": "import",
    "file": "../schema/schema-catalog.json"
});

Производительность


В следующей таблице результаты тестирования наиболее трудоемкой функции — создание записей. Тестировалось на MacBook Pro Mid 2014 (MGX82).


Журналируемые модели (model.unlogged: false):


Свойства 100 записей (сек.) 1000 записей (сек.) Записей в сек.
Кол-во: 1, Число: 1 0.5 4.9 204
Кол-во: 1, Строка: 1 0.5 4.6 215
Кол-во: 1, Дата: 1 0.5 4.4 227
Кол-во: 3, Число: 1, Строка: 1, Дата: 1 0.5 4.8 209
Кол-во: 10, Число: 10 0.6 5.8 172
Кол-во: 10, Строка: 10 0.6 7.1 140
Кол-во: 10, Дата: 10 0.6 10.1 98
Кол-во: 30, Число: 10, Строка: 10, Дата: 10 1.2 14.7 68
Кол-во: 100 Число: 100 2.3 27.3 37
Кол-во: 100 Строка: 100 2.4 24.1 42
Кол-во: 100 Дата: 100 2.3 24.6 40
Кол-во: 300 Число: 100, Строка: 100, Дата: 100 8.9 88.3 11

Нежурналируемые модели (model.unlogged: true):


Свойства 100 записей (сек.) 1000 записей (сек.) Записей в сек.
Кол-во: 1, Число: 1 0.5 4.3 233
Кол-во: 1, Строка: 1 0.4 4.1 244
Кол-во: 1, Дата: 1 0.4 3.7 268
Кол-во: 3, Число: 1, Строка: 1, Дата: 1 0.5 3.8 261
Кол-во: 10, Число: 10 0.5 4.1 243
Кол-во: 10, Строка: 10 0.4 4.0 251
Кол-во: 10, Дата: 10 0.4 4.2 239
Кол-во: 30, Число: 10, Строка: 10, Дата: 10 0.5 4.9 202
Кол-во: 100 Число: 100 0.6 12.4 81
Кол-во: 100 Строка: 100 0.7 6.1 162
Кол-во: 100 Дата: 100 0.9 7.2 140
Кол-во: 300 Число: 100, Строка: 100, Дата: 100 1.1 11.1 90


По столбцам:


  • в 1-м столбце указано количество свойств модели и типы данных
  • во 2-м и 3-м столбце указана продолжительность добавления записей
  • в 4-м столбце количество создаваемых записей в секунду.

Скрипт test.js


Заключение


Дополнительную информацию смотрите на домашних страницах пакетов на github. Каюсь, информация там скудная, буду стараться дополнять. Лицензия платформы MIT. В планах разработка дополнительных пакетов по аналитике и другим нужным направлениям.
Спасибо за внимание.

Источник: https://habr.com/ru/post/513648/


Интересные статьи

Интересные статьи

Известный, но не очень популярный способ внедрения зависимостей. Попытка реализовать этот способ в популярных DI npm пакетах. Еще один свой DI. Читать дальше → ...
Мне было необходимо делать 2 раза в сутки бэкап сайта на «1С-Битрикс: Управление сайтом» (файлов и базы mysql) и хранить историю изменений за 90 дней. Сайт расположен на VDS под уп...
Доброго времени суток, друзья! Представляю Вашему вниманию перевод статьи «What JavaScript Developers Should Know About Curl» автора Valery Karpov. Curl — это популярный инс...
Доброго времени суток, друзья! Простые одностраничные приложения, основанные на React, Vue или чистом JavaScript, окружают нас повсюду. Хороший «одностраничник» предполагает соответствующи...
Вначале была функция и вызывали ее в одном месте. Потом мы захотели вызвать ее в другом месте с новыми возможностями и обновили ее. Нам эта ф-ия так понравилась, что мы вызвали ее в третьем месте...