data-primals-engine 1.1.5 → 1.1.7-rc1

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.
@@ -11,7 +11,9 @@ on:
11
11
 
12
12
  jobs:
13
13
  build:
14
-
14
+ permissions:
15
+ contents: read
16
+ pull-requests: write
15
17
  runs-on: ubuntu-latest
16
18
  container:
17
19
  image: node:20 # Specify the Docker version you needx
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
@@ -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'+getRandom(1, 99);
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' + getRandom(1, 99);
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'+getRandom(0, 99);
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
  }
@@ -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
- if (
263
- value &&
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
- current[pathArray[pathArray.length - 1]] = value;
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.5",
3
+ "version": "1.1.7-rc1",
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
@@ -1,3 +1,5 @@
1
+ import {event_trigger} from "./core.js";
2
+
1
3
  /**
2
4
  * Enables auto-installation at startup
3
5
  * @type {boolean}
@@ -214,7 +216,25 @@ export const maxTotalPrivateFilesSize = 250 * megabytes;
214
216
  */
215
217
  export const timeoutVM = 5000;
216
218
 
217
- export const defaultMaxRequestData = 50000;
219
+ /**
220
+ * Default maximum number of data per request
221
+ * @type {number}
222
+ */
223
+ export const defaultMaxRequestData = 500;
224
+
225
+ /**
226
+ * Database connections pool size for MongoDB access
227
+ * @type {number}
228
+ */
229
+ export const databasePoolSize = 30;
230
+
231
+ /**
232
+ * TLS options for MongoDB access
233
+ * @type {boolean}
234
+ */
235
+ export const tlsAllowInvalidCertificates = false;
236
+ export const tlsAllowInvalidHostnames = false;
237
+
218
238
  /**
219
239
  * Options for the HTML sanitizer
220
240
  * @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,54 @@ function splitmix32(a) {
176
181
 
177
182
  export const getRand = () =>splitmix32(seed);
178
183
 
179
- export const getRandom = (minInclusive,maxInclusive) => {
180
- return Math.floor(Math.random() * (maxInclusive - minInclusive + 1) + minInclusive);
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
+ const cr = (window.crypto || window.msCrypto);
228
+ cr?.getRandomValues(randomBuffer);
229
+ const r = cr ? ( randomBuffer[0] / (0xffffffff + 1) ) : getRand();
230
+ return Math.floor(r * (maxInclusive - minInclusive + 1) + minInclusive);
231
+ }
182
232
 
183
233
  export function randomDate(start, end) {
184
234
  return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
package/src/engine.js CHANGED
@@ -4,7 +4,12 @@ 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 {
8
+ cookiesSecret,
9
+ databasePoolSize,
10
+ dbName,
11
+ tlsAllowInvalidCertificates, tlsAllowInvalidHostnames
12
+ } from "./constants.js";
8
13
  import http from "http";
9
14
  import cookieParser from "cookie-parser";
10
15
  import requestIp from 'request-ip';
@@ -13,13 +18,40 @@ import {defaultModels} from "./defaultModels.js";
13
18
  import {DefaultUserProvider} from "./providers.js";
14
19
  import formidableMiddleware from 'express-formidable';
15
20
  import sirv from "sirv";
21
+ import * as tls from "node:tls";
16
22
 
17
23
  // Constants
18
24
  const isProduction = process.env.NODE_ENV === 'production'
19
25
 
26
+ let caFile, certFile, keyFile;
27
+ try {
28
+ if (process.env.CA_CERT)
29
+ caFile = fs.readFileSync(process.env.CA_CERT);
30
+ } catch (e) {}
31
+ try {
32
+ if (process.env.CERT)
33
+ certFile = fs.readFileSync(process.env.CERT);
34
+ }catch (e) {}
35
+ try{
36
+ if (process.env.CERT_KEY)
37
+ keyFile = fs.readFileSync(process.env.CERT_KEY);
38
+ } catch (e) {}
39
+
40
+ const secureContext = tls.createSecureContext({
41
+ ca: caFile, cert: certFile, key: keyFile
42
+ });
43
+
44
+ const isTlsActive = !(!process.env.TLS || ["0", "false"].includes(process.env.TLS.toLowerCase()));
45
+
20
46
  // Connection URL
21
47
  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: 20 });
48
+ export const MongoClient = new InternalMongoClient(dbUrl, {
49
+ maxPoolSize: databasePoolSize,
50
+ tls: isTlsActive,
51
+ secureContext,
52
+ tlsAllowInvalidCertificates,
53
+ tlsAllowInvalidHostnames
54
+ });
23
55
 
24
56
  // Database Name
25
57
  export const MongoDatabase = MongoClient.db(dbName);
@@ -150,7 +182,7 @@ export const Engine = {
150
182
  throw err;
151
183
  }
152
184
  });
153
- process.exit(0);
185
+ process.exit(1);
154
186
  });
155
187
  }
156
188
 
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"; // Pour une sortie plus lisible. Assurez-vous que 'chalk' est dans vos dépendances.
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(), 'server', 'src', 'migrations');
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();
@@ -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 || '200').json(result);
1638
+ return res.status(result.statusCode || 200).json(result);
1639
1639
  }else{
1640
- return res.status(result.statusCode || '500').json(result);
1640
+ return res.status(result.statusCode || 500).json(result);
1641
1641
  }
1642
1642
  });
1643
1643
 
@@ -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
- };