Si vous souhaitez créer facilement des applications Web en utilisant uniquement javascript (full-stack), je vous suggÚre de vous familiariser avec la plate-forme objectum. La nouvelle version de la plate-forme est le fruit d'une expérience de travail sur la version précédente, utilisée depuis 10 ans. Les deux versions sont utilisées dans le développement de divers systÚmes d'information - ce sont des solutions régionales et des systÚmes pour les organisations. La plate-forme de la nouvelle version est déjà utilisée sur les serveurs de production et se développera encore longtemps. Plus de détails.
Captures d'écran des développements existants
Exemple d'application Web:
Exemple de site (pas de rendu de serveur):
Packages de plateforme
La plate-forme se compose des packages npm suivants: 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- .
Conclusion
Consultez les pages d'accueil du package sur github pour plus d'informations. J'avoue que les informations sont rares, je vais essayer de les compléter. Licence de plate-forme MIT. Il est prévu de développer des packages supplémentaires pour l'analyse et d'autres domaines nécessaires.
Merci de votre attention.