[JavaScript, Node.JS, PostgreSQL, ReactJS] Javascript платформа Objectum
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Если вам нужен простой способ создавать веб-приложения, используя только javascript (full-stack), то предлагаю вам ознакомиться с платформой objectum. Новая версия платформы является результатом опыта работы над предыдущей версией, которая используется 10 лет. Обе версии используются в разработке различных информационных систем — это региональные решения и системы для организаций. Платформа новой версии уже используется на продакшн серверах и будет развиваться длительное время. Далее подробности.
Скрины из существующих разработок
Пример веб-приложения:
Пример сайта (без серверного рендера):
Пакеты платформы
Платформа состоит из следующих 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 вы можете установить отсюда.
Установка платформы
Устанавливаем утилиту командной строки:
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
Инструменты разработчика
Меню "Objectum" содержит пункты:
- Модели — создание моделей и свойств
- Запросы — создание SQL запросов для выборки данных
- Меню — конструктор меню для ролей пользователей
- Роли — здесь задается список ролей пользователей
- Пользователи — добавление пользователей
Пакетное добавление данных в хранилище
Структура хранилища формируется с помощью UI, но для быстрого добавления большого количества данных лучше использовать objectum-cli.
Импорт JSON
cd /opt/objectum/projects/catalog
objectum-cli --import-json scripts/catalog-cli.json --file-directory scripts/files
Здесь скрипт catalog-cli.json
SPL
{
"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
SPL
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 — рендер поля. Статичный метод для новой записи и обычный метод для существующей записи.
Модифицированная форма:
Сервер
Подключение в index.js:
import ItemModel from "./src/models/ItemServerModel.js";
proxy.register ("item", ItemModel);
Методы и работа с хранилищем store такая же. Сессии между пользователями разделены.
ItemServerModel.js
SPL
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
SPL
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
SPL
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. В планах разработка дополнительных пакетов по аналитике и другим нужным направлениям.
Спасибо за внимание.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка под Linux, Разработка на Raspberry Pi, Компьютерное железо, Интернет вещей, DIY или Сделай сам] Встраиваемый компьютер AntexGate + 3G-модем. Полезные настройки для более стабильного интернет-соединения
- [Производство и разработка электроники, Электроника для начинающих] О том, как на завод министр приезжал
- [ReactJS, Разработка веб-сайтов] Улучшаем useReducer
- [Игры и игровые приставки, Киберспорт, Разработка игр] Чего не хватает современным шутерам?
- [Unity, Игры и игровые приставки, Разработка игр, Разработка мобильных приложений] Сказ о разработке амбициозного проекта 16-ти летним парнем (file547)
- [IT-инфраструктура, Системное администрирование] Как мигрировать Zabbix с MySQL на PostgreSQL с минимальным downtime
- [JavaScript, Разработка веб-сайтов] Чем «фрагменты» могут помочь в Веб-разработке на примере Malina.js (перевод)
- [DIY или Сделай сам, Беспроводные технологии, Производство и разработка электроники, Разработка систем связи, Стандарты связи] Прием всего Bluetooth разом на SDR с CUDA? Легко
- [Angular] Прокачай свой CLI
- [JavaScript, Node.JS, VueJS] Как мы проводили офлайн мероприятие в онлайн формате
Теги для поиска: #_javascript, #_node.js, #_postgresql, #_reactjs, #_objectum, #_platforma (платформа), #_razrabotka (разработка), #_javascript, #_node.js, #_postgresql, #_reactjs
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:54
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Если вам нужен простой способ создавать веб-приложения, используя только javascript (full-stack), то предлагаю вам ознакомиться с платформой objectum. Новая версия платформы является результатом опыта работы над предыдущей версией, которая используется 10 лет. Обе версии используются в разработке различных информационных систем — это региональные решения и системы для организаций. Платформа новой версии уже используется на продакшн серверах и будет развиваться длительное время. Далее подробности. Скрины из существующих разработок Пример веб-приложения: Пример сайта (без серверного рендера): Пакеты платформы Платформа состоит из следующих 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. Выполняет следующие задачи:
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 вы можете установить отсюда. Установка платформы Устанавливаем утилиту командной строки: 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 Инструменты разработчика Меню "Objectum" содержит пункты:
Пакетное добавление данных в хранилище Структура хранилища формируется с помощью UI, но для быстрого добавления большого количества данных лучше использовать objectum-cli. Импорт JSON cd /opt/objectum/projects/catalog
objectum-cli --import-json scripts/catalog-cli.json --file-directory scripts/files Здесь скрипт catalog-cli.jsonSPL{
"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" } ] } Что всё это значит:
Импорт 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:
Исходный код моделей Исходный код 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" }); Что это дает:
ItemModel.jsSPLimport 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; В моделях зарезервированы названия методов для решения различных задач:
Модифицированная форма: Сервер Подключение в index.js: import ItemModel from "./src/models/ItemServerModel.js";
proxy.register ("item", ItemModel); Методы и работа с хранилищем store такая же. Сессии между пользователями разделены. ItemServerModel.jsSPLimport 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.jsSPLlet 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 }; Любой запрос к хранилищу можно запретить или ограничить. Доступные методы:
Действия можно запретить или ограничить, например разрешать изменять только набор свойств определенной роли. Создание, изменение, удаление моделей, свойств, запросов и столбцов доступно только суперпользователю admin. Действия администратора Иногда нужно выполнить серверное действие с максимальными правами. Это может быть регистрация пользователя или какая-то обратная связь. Подключение в index.js: import adminMethods from "./src/modules/admin.js";
proxy.registerAdminMethods (adminMethods); admin.jsSPLimport 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 props:
Grid Для выборки данных компоненту нужно указать запрос (query) или модель (model). Предоставляет следующие функции:
Form Форма группирует поля. Позволяет сохранить изменения, и загрузить файлы. Кнопка "Изменения" в форме показывает таблицу изменений по выбранному полю. Видно какой пользователь, когда и с какого IP-адреса внес изменения. Action Компонент предоставляет удобный запуск функций:
Отчеты Для построения несложных отчетов используется 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 } }); Где:
Развертывание Платформа поддерживает виртуализацию хранилищ. База любого проекта экспортируется в файл и импортируется в другую базу. В новой базе у ресурсов создаются ссылки на оригинальную базу, по которым виртуализированная база обновляется в дальнейшем. Модели, свойства, запросы, записи создаются, изменяются или удаляются. Примеры использования:
Экспорт схемы 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 По столбцам:
Скрипт test.js Заключение Дополнительную информацию смотрите на домашних страницах пакетов на github. Каюсь, информация там скудная, буду стараться дополнять. Лицензия платформы MIT. В планах разработка дополнительных пакетов по аналитике и другим нужным направлениям. Спасибо за внимание. =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:54
Часовой пояс: UTC + 5