data-primals-engine 1.1.5 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/node.js.yml +3 -1
- package/README.md +4 -0
- package/client/src/App.jsx +4 -4
- package/client/src/DataEditor.jsx +3 -1
- package/client/src/Field.jsx +3 -4
- package/client/src/FlexBuilder.jsx +3 -1
- package/package.json +1 -1
- package/src/constants.js +13 -1
- package/src/core.js +52 -3
- package/src/engine.js +24 -3
- package/src/migrate.js +2 -3
- package/src/modules/data.js +2 -2
- package/src/modules/mongodb.js +0 -9
- package/src/migrations/20240522143000-content.js +0 -20
package/README.md
CHANGED
|
@@ -58,6 +58,10 @@ MONGO_DB_URL=mongodb://127.0.0.1:27017
|
|
|
58
58
|
| SMTP_PORT | SMTP server port. | 587 |
|
|
59
59
|
| SMTP_USER | Username for SMTP authentication. | user@example.com |
|
|
60
60
|
| SMTP_PASS | Password for SMTP authentication. | password |
|
|
61
|
+
| TLS | Encrypted connection (TLS) mode. Disabled by default | 0/1 false/true |
|
|
62
|
+
| CERT | Path to cert file. | certs/ca.crt |
|
|
63
|
+
| CA_CERT | Path to CA cert file. | certs/cert.pem |
|
|
64
|
+
| CERT_KEY | Path to the key file for your certificate. | certs/key.pem |
|
|
61
65
|
|
|
62
66
|
Start the server:
|
|
63
67
|
```bash
|
package/client/src/App.jsx
CHANGED
|
@@ -53,7 +53,7 @@ import {Helmet} from "react-helmet";
|
|
|
53
53
|
import {useCookies, CookiesProvider} from "react-cookie";
|
|
54
54
|
|
|
55
55
|
import { translations as allTranslations} from "../../src/i18n.js";
|
|
56
|
-
import {getRandom} from "../../src/core.js";
|
|
56
|
+
import {getBrowserRandom, getRandom} from "../../src/core.js";
|
|
57
57
|
import {getUserHash} from "../../src/data.js";
|
|
58
58
|
import {seoTitle} from "./constants.js";
|
|
59
59
|
import {host, useAI} from "../../src/constants.js";
|
|
@@ -254,7 +254,7 @@ function Layout ({header, translationMutation, routes, body, footer}) {
|
|
|
254
254
|
const menu=<>{(!me) && !loc.pathname.startsWith("/user/demo") && (<Button className={"btn btn-nav btn-ellipsis btn-primary btn-big"} onClick={() => {
|
|
255
255
|
gtag("event", "nav to demo");
|
|
256
256
|
setCurrentProfile(null);
|
|
257
|
-
const username = 'demo'+
|
|
257
|
+
const username = 'demo'+getBrowserRandom(1, 99);
|
|
258
258
|
setMe({username});
|
|
259
259
|
setCookie('username', username, { path : "/", domain: isProd ? host : 'localhost'});
|
|
260
260
|
nav('/user/'+username, { state: { startTour: true } });
|
|
@@ -276,7 +276,7 @@ function Layout ({header, translationMutation, routes, body, footer}) {
|
|
|
276
276
|
|
|
277
277
|
useEffect(() => {
|
|
278
278
|
if( !cookies.username) {
|
|
279
|
-
const username ='demo' +
|
|
279
|
+
const username ='demo' + getBrowserRandom(1, 99);
|
|
280
280
|
setCookie("username", username, { path : "/", domain: isProd ? host : 'localhost'});
|
|
281
281
|
onAuthenticated({username}, true);
|
|
282
282
|
}
|
|
@@ -360,7 +360,7 @@ function Layout ({header, translationMutation, routes, body, footer}) {
|
|
|
360
360
|
const handleGenerateClick = (p) => {
|
|
361
361
|
gtag('event', 'homepage model generation');
|
|
362
362
|
setCurrentTour(null);
|
|
363
|
-
const username = 'demo'+
|
|
363
|
+
const username = 'demo'+getBrowserRandom(0, 99);
|
|
364
364
|
setMe({username});
|
|
365
365
|
|
|
366
366
|
setPromptResult(null);
|
|
@@ -8,7 +8,7 @@ import RelationField from "./RelationField.jsx";
|
|
|
8
8
|
import {
|
|
9
9
|
CheckboxField,
|
|
10
10
|
CodeField,
|
|
11
|
-
ColorField,
|
|
11
|
+
ColorField, EmailField,
|
|
12
12
|
EnumField,
|
|
13
13
|
FileField, ModelField, NumberField,
|
|
14
14
|
PhoneField,
|
|
@@ -470,6 +470,8 @@ export const DataEditor = forwardRef(function MyDataEditor({
|
|
|
470
470
|
return <FileField help={t('field_'+model.name+'_'+field.name+'_hint', field.hint || '')} key={field.name} name={field.name} maxSize={field.maxSize} mimeTypes={field.mimeTypes} value={value} onChange={(file) => handleChange({ name: name, value: file ? file.name : null })} />
|
|
471
471
|
case 'color':
|
|
472
472
|
return <ColorField help={t('field_'+model.name+'_'+field.name+'_hint', field.hint || '')} key={field.name} name={field.name} value={value} onChange={handleChange} />
|
|
473
|
+
case 'email':
|
|
474
|
+
return <EmailField onFocus={() => setFocusedField(field)} onBlur={() => setFocusedField(null)} help={focusedField?.name === field.name ? t('field_'+model.name+'_'+field.name+'_hint', field.hint || '') : ''} key={field.name} type={getInputType(field.type)} {...inputProps} onChange={(e) => handleChange({name: field.name, value: e.target.value})} />
|
|
473
475
|
default:
|
|
474
476
|
return <TextField onFocus={() => setFocusedField(field)} onBlur={() => setFocusedField(null)} help={focusedField?.name === field.name ? t('field_'+model.name+'_'+field.name+'_hint', field.hint || '') : ''} key={field.name} type={getInputType(field.type)} {...inputProps} onChange={(e) => handleChange({name: field.name, value: e.target.value})} />
|
|
475
477
|
}
|
package/client/src/Field.jsx
CHANGED
|
@@ -35,6 +35,7 @@ import { PhoneInput } from 'react-international-phone';
|
|
|
35
35
|
import 'react-international-phone/style.css';
|
|
36
36
|
import CodeMirror, {basicSetup} from "@uiw/react-codemirror";
|
|
37
37
|
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
38
|
+
import {maxStringLength} from "data-primals-engine/constants";
|
|
38
39
|
|
|
39
40
|
export const Form = ({
|
|
40
41
|
name,
|
|
@@ -259,10 +260,8 @@ const EmailField = forwardRef(
|
|
|
259
260
|
if (maxlength !== undefined && maxlength > 0 && value && value.trim().length > maxlength) {
|
|
260
261
|
errs.push("Value length must be <= to " + maxlength);
|
|
261
262
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
!value.match(/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/)
|
|
265
|
-
) {
|
|
263
|
+
|
|
264
|
+
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
266
265
|
errs.push("Invalid email");
|
|
267
266
|
}
|
|
268
267
|
setErrors(errs);
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
clearMappingsRecursive
|
|
20
20
|
} from './FlexTreeUtils.js';
|
|
21
21
|
import {Dialog, DialogProvider} from "./Dialog.jsx";
|
|
22
|
+
import {safeAssignObject} from "data-primals-engine/core";
|
|
22
23
|
|
|
23
24
|
const FlexBuilder = ({ initialConfig = null, models = [], onChange, data = [], lang = 'fr' }) => {
|
|
24
25
|
const { me: user } = useAuthContext();
|
|
@@ -104,7 +105,8 @@ const FlexBuilder = ({ initialConfig = null, models = [], onChange, data = [], l
|
|
|
104
105
|
current[pathArray[i]] = current[pathArray[i]] || {};
|
|
105
106
|
current = current[pathArray[i]];
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
+
const key = pathArray[pathArray.length - 1];
|
|
109
|
+
safeAssignObject(current, key, value);
|
|
108
110
|
return newNode;
|
|
109
111
|
};
|
|
110
112
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "data-primals-engine is the package responsible from handling large amount of data in a practical and performant way. It can handle large amount of data in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"type": "module",
|
package/src/constants.js
CHANGED
|
@@ -214,7 +214,19 @@ export const maxTotalPrivateFilesSize = 250 * megabytes;
|
|
|
214
214
|
*/
|
|
215
215
|
export const timeoutVM = 5000;
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Default maximum number of data per request
|
|
219
|
+
* @type {number}
|
|
220
|
+
*/
|
|
221
|
+
export const defaultMaxRequestData = 500;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Database connections pool size for MongoDB access
|
|
225
|
+
* @type {number}
|
|
226
|
+
*/
|
|
227
|
+
export const databasePoolSize = 30;
|
|
228
|
+
|
|
229
|
+
|
|
218
230
|
/**
|
|
219
231
|
* Options for the HTML sanitizer
|
|
220
232
|
* @type {{allowedSchemesByTag: {}, selfClosing: string[], allowedSchemes: string[], enforceHtmlBoundary: boolean, disallowedTagsMode: string, allowProtocolRelative: boolean, allowedAttributes: {a: string[], img: string[], code: string[]}, allowedTags: string[], allowedSchemesAppliedToAttributes: string[]}}
|
package/src/core.js
CHANGED
|
@@ -11,6 +11,11 @@ export function escapeRegex(string) {
|
|
|
11
11
|
|
|
12
12
|
export const isDate = dt => String(new Date(dt)) !== 'Invalid Date'
|
|
13
13
|
|
|
14
|
+
export const safeAssignObject = (obj, key, value) => {
|
|
15
|
+
if( !["__proto__", "constructor"].includes(key)){
|
|
16
|
+
obj[key] = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
14
19
|
export function debounce(callback, delay=300){
|
|
15
20
|
var timer;
|
|
16
21
|
return function(){
|
|
@@ -176,9 +181,53 @@ function splitmix32(a) {
|
|
|
176
181
|
|
|
177
182
|
export const getRand = () =>splitmix32(seed);
|
|
178
183
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
const MAX_RANGE_SIZE = 2n ** 64n
|
|
185
|
+
const buffer = new BigUint64Array(512)
|
|
186
|
+
let offset = buffer.length
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Returns a cryptographically secure random integer between min and max, inclusive.
|
|
190
|
+
*
|
|
191
|
+
* @param {number} min - the lowest integer in the desired range (inclusive)
|
|
192
|
+
* @param {number} max - the highest integer in the desired range (inclusive)
|
|
193
|
+
* @returns {number} Random number
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
export function getRandom(min, max) {
|
|
197
|
+
if (!(Number.isSafeInteger(min) && Number.isSafeInteger(max))) {
|
|
198
|
+
throw Error("min and max must be safe integers")
|
|
199
|
+
}
|
|
200
|
+
if (min > max) {
|
|
201
|
+
throw Error("min must be less than or equal to max")
|
|
202
|
+
}
|
|
203
|
+
const bmin = BigInt(min)
|
|
204
|
+
const rangeSize = BigInt(max) - bmin + 1n
|
|
205
|
+
const rejectionThreshold = MAX_RANGE_SIZE - (MAX_RANGE_SIZE % rangeSize)
|
|
206
|
+
let result;
|
|
207
|
+
do {
|
|
208
|
+
if (offset >= buffer.length) {
|
|
209
|
+
crypto.getRandomValues(buffer)
|
|
210
|
+
offset = 0
|
|
211
|
+
}
|
|
212
|
+
result = buffer[offset++]
|
|
213
|
+
} while (result >= rejectionThreshold)
|
|
214
|
+
return Number(bmin + result % rangeSize)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Returns a cryptographically secure random integer between min and max, inclusive.
|
|
219
|
+
*
|
|
220
|
+
* @param {number} minInclusive - the lowest integer in the desired range (inclusive)
|
|
221
|
+
* @param {number} maxInclusive - the highest integer in the desired range (inclusive)
|
|
222
|
+
* @returns {number} Random number
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
export function getBrowserRandom(minInclusive, maxInclusive) {
|
|
226
|
+
const randomBuffer = new Uint32Array(1);
|
|
227
|
+
(window.crypto || window.msCrypto).getRandomValues(randomBuffer);
|
|
228
|
+
const r = ( randomBuffer[0] / (0xffffffff + 1) );
|
|
229
|
+
return Math.floor(r * (maxInclusive - minInclusive + 1) + minInclusive);
|
|
230
|
+
}
|
|
182
231
|
|
|
183
232
|
export function randomDate(start, end) {
|
|
184
233
|
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
|
package/src/engine.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'node:fs'
|
|
|
4
4
|
import express from 'express'
|
|
5
5
|
import {MongoClient as InternalMongoClient} from 'mongodb'
|
|
6
6
|
import process from "process";
|
|
7
|
-
import {cookiesSecret, dbName} from "./constants.js";
|
|
7
|
+
import {cookiesSecret, databasePoolSize, dbName} from "./constants.js";
|
|
8
8
|
import http from "http";
|
|
9
9
|
import cookieParser from "cookie-parser";
|
|
10
10
|
import requestIp from 'request-ip';
|
|
@@ -13,13 +13,34 @@ import {defaultModels} from "./defaultModels.js";
|
|
|
13
13
|
import {DefaultUserProvider} from "./providers.js";
|
|
14
14
|
import formidableMiddleware from 'express-formidable';
|
|
15
15
|
import sirv from "sirv";
|
|
16
|
+
import * as tls from "node:tls";
|
|
16
17
|
|
|
17
18
|
// Constants
|
|
18
19
|
const isProduction = process.env.NODE_ENV === 'production'
|
|
19
20
|
|
|
21
|
+
let caFile, certFile, keyFile;
|
|
22
|
+
try {
|
|
23
|
+
if (process.env.CA_CERT)
|
|
24
|
+
caFile = fs.readFileSync(process.env.CA_CERT || './ca');
|
|
25
|
+
} catch (e) {}
|
|
26
|
+
try {
|
|
27
|
+
if (process.env.CERT)
|
|
28
|
+
certFile = fs.readFileSync(process.env.CERT || '');
|
|
29
|
+
}catch (e) {}
|
|
30
|
+
try{
|
|
31
|
+
if (process.env.CERT_KEY)
|
|
32
|
+
keyFile = fs.readFileSync(process.env.CERT_KEY || './k');
|
|
33
|
+
} catch (e) {}
|
|
34
|
+
|
|
35
|
+
const secureContext = tls.createSecureContext({
|
|
36
|
+
ca: caFile, cert: certFile, key: keyFile
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const isTlsActive = !(!process.env.TLS || ["0", "false"].includes(process.env.TLS.toLowerCase()));
|
|
40
|
+
|
|
20
41
|
// Connection URL
|
|
21
42
|
export const dbUrl = process.env.CI ? 'mongodb://mongodb:27017' : (process.env.MONGO_DB_URL || 'mongodb://127.0.0.1:27017');
|
|
22
|
-
export const MongoClient = new InternalMongoClient(dbUrl, { maxPoolSize:
|
|
43
|
+
export const MongoClient = new InternalMongoClient(dbUrl, { maxPoolSize: databasePoolSize, tls: isTlsActive, secureContext });
|
|
23
44
|
|
|
24
45
|
// Database Name
|
|
25
46
|
export const MongoDatabase = MongoClient.db(dbName);
|
|
@@ -150,7 +171,7 @@ export const Engine = {
|
|
|
150
171
|
throw err;
|
|
151
172
|
}
|
|
152
173
|
});
|
|
153
|
-
process.exit(
|
|
174
|
+
process.exit(1);
|
|
154
175
|
});
|
|
155
176
|
}
|
|
156
177
|
|
package/src/migrate.js
CHANGED
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
// FIX: Import 'pathToFileURL' instead of 'fileURLToPath'
|
|
7
6
|
import { pathToFileURL } from 'node:url';
|
|
8
7
|
import { Engine, MongoDatabase } from "./engine.js";
|
|
9
8
|
import { Logger } from "./gameObject.js";
|
|
10
9
|
import { Config } from "./config.js";
|
|
11
|
-
import chalk from "chalk";
|
|
10
|
+
import chalk from "chalk";
|
|
12
11
|
|
|
13
12
|
// Configuration de base
|
|
14
13
|
Config.Set("modules", ["mongodb"]);
|
|
15
|
-
const MIGRATIONS_DIR = path.resolve(process.cwd(), '
|
|
14
|
+
const MIGRATIONS_DIR = path.resolve(process.cwd(), 'migrations');
|
|
16
15
|
const MIGRATIONS_COLLECTION = 'migrations_log';
|
|
17
16
|
|
|
18
17
|
const engine = await Engine.Create();
|
package/src/modules/data.js
CHANGED
|
@@ -1635,9 +1635,9 @@ export async function onInit(defaultEngine) {
|
|
|
1635
1635
|
engine.put('/api/model/:id', [middlewareAuthenticator, userInitiator, setTimeoutMiddleware(15000)], async (req, res) => {
|
|
1636
1636
|
const result = await editModel(req.me, req.params.id, req.fields);
|
|
1637
1637
|
if( result.success){
|
|
1638
|
-
return res.status(result.statusCode ||
|
|
1638
|
+
return res.status(result.statusCode || 200).json(result);
|
|
1639
1639
|
}else{
|
|
1640
|
-
return res.status(result.statusCode ||
|
|
1640
|
+
return res.status(result.statusCode || 500).json(result);
|
|
1641
1641
|
}
|
|
1642
1642
|
});
|
|
1643
1643
|
|
package/src/modules/mongodb.js
CHANGED
|
@@ -13,15 +13,6 @@ export async function onInit(defaultEngine) {
|
|
|
13
13
|
|
|
14
14
|
const isProduction = process.env.NODE_ENV === 'production'
|
|
15
15
|
|
|
16
|
-
let ca, cert, key;
|
|
17
|
-
try {
|
|
18
|
-
ca = fs.readFileSync('certs/mongodb-cert.crt');
|
|
19
|
-
cert = fs.readFileSync('certs/mongodb.pem');
|
|
20
|
-
key = fs.readFileSync(`certs/mongodb-cert.key`);
|
|
21
|
-
} catch (e) {
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
16
|
modelsCollection = MongoDatabase.collection("models");
|
|
26
17
|
datasCollection = MongoDatabase.collection("datas");
|
|
27
18
|
filesCollection = MongoDatabase.collection("files");
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Migration: setting content field to 'fr' in 'content' model
|
|
3
|
-
* Created at: 2024-05-21T10:00:00.000Z
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const up = async (db) => {
|
|
7
|
-
const datasCollection = db.collection("datas");
|
|
8
|
-
const content = await datasCollection.find({ _model:"content"}).toArray();
|
|
9
|
-
await Promise.all(content.map(async (doc) => {
|
|
10
|
-
if (typeof(doc.html)==='string') {
|
|
11
|
-
const c = { "fr" : doc.html };
|
|
12
|
-
await datasCollection.updateOne({_id: doc._id}, {$set: {html: c}});
|
|
13
|
-
}
|
|
14
|
-
}));
|
|
15
|
-
console.log("Migration UP: setting content field to 'fr' in 'content' model");
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const down = async (db) => {
|
|
19
|
-
console.log("Migration DOWN: noting to do");
|
|
20
|
-
};
|