Jika Anda menginginkan cara mudah membuat aplikasi web hanya dengan menggunakan javascript (full-stack), maka saya sarankan Anda membiasakan diri dengan platform objectum. Versi baru dari platform ini adalah hasil dari pengalaman mengerjakan versi sebelumnya, yang telah digunakan selama 10 tahun. Kedua versi tersebut digunakan dalam pengembangan berbagai sistem informasi - ini adalah solusi dan sistem regional untuk organisasi. Platform versi baru sudah digunakan di server produksi dan akan berkembang untuk waktu yang lama. Keterangan lebih lanjut.

Tangkapan layar dari perkembangan yang ada
Contoh aplikasi web:

Contoh situs (tidak ada server yang merender):

Paket platform
Platform ini terdiri dari paket npm berikut: 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
{
"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 ()
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 . .
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);
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);
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- .
Kesimpulan
Lihat halaman beranda paket di github untuk informasi lebih lanjut. Saya akui, informasi yang ada di sana sedikit, saya akan mencoba melengkapinya. Lisensi platform MIT. Ada rencana untuk mengembangkan paket tambahan untuk analitik dan area lain yang diperlukan.
Terima kasih atas perhatian Anda.