data-primals-engine 1.6.5 → 1.7.1

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.
Files changed (40) hide show
  1. package/README.md +160 -113
  2. package/client/index.js +3 -0
  3. package/client/package-lock.json +8121 -8824
  4. package/client/package.json +10 -3
  5. package/client/src/AssistantChat.jsx +369 -362
  6. package/client/src/DataEditor.jsx +383 -383
  7. package/client/src/DataLayout.jsx +54 -20
  8. package/client/src/ModelList.jsx +280 -280
  9. package/client/src/ViewSwitcher.scss +0 -31
  10. package/client/src/constants.js +81 -100
  11. package/client/src/contexts/CommandContext.jsx +274 -259
  12. package/client/vite.config.js +30 -30
  13. package/doc/AI-assistance.md +93 -0
  14. package/doc/Advanced-workflows.md +90 -0
  15. package/doc/Event-system.md +79 -0
  16. package/doc/Packs-gallery.md +73 -0
  17. package/package.json +30 -16
  18. package/src/constants.js +1 -1
  19. package/src/core.js +487 -477
  20. package/src/defaultModels.js +1 -1
  21. package/src/email.js +0 -2
  22. package/src/engine.js +342 -335
  23. package/src/filter.js +348 -343
  24. package/src/migrate.js +1 -1
  25. package/src/modules/assistant/assistant.js +30 -20
  26. package/src/modules/data/data.backup.js +4 -4
  27. package/src/modules/data/data.js +311 -302
  28. package/src/modules/data/data.operations.js +79 -64
  29. package/src/modules/data/data.relations.js +2 -1
  30. package/src/modules/data/data.scheduling.js +1 -1
  31. package/src/modules/mongodb.js +16 -8
  32. package/src/modules/user.js +0 -1
  33. package/src/modules/workflow.js +1828 -1815
  34. package/src/packs.js +5701 -5697
  35. package/src/profiles.js +19 -0
  36. package/swagger-en.yml +3390 -3385
  37. package/swagger-fr.yml +3385 -3380
  38. package/test/assistant.test.js +2 -2
  39. package/test/data.backup.integration.test.js +3 -1
  40. package/test/data.history.integration.test.js +0 -1
@@ -24,7 +24,7 @@ import {
24
24
  optionsSanitizer,
25
25
  searchRequestTimeout
26
26
  } from "../../constants.js";
27
- import {anonymizeText, getFieldValueHash, getUserId, isDemoUser, isLocalUser} from "../../data.js";
27
+ import {anonymizeText, encryptValue, getFieldValueHash, getUserId, isDemoUser, isLocalUser} from "../../data.js";
28
28
  import sanitizeHtml from "sanitize-html";
29
29
  import {importJobs, modelsCache, runCryptoWorkerTask, runImportExportWorker} from "./data.core.js";
30
30
  import {
@@ -71,7 +71,7 @@ const IMPORT_CHUNK_DELAY_MS = 1000; // Délai en millisecondes entre le traiteme
71
71
 
72
72
  export const dataTypes = {
73
73
  object: {
74
- validate: (value, field) => {
74
+ validate: (value, field) => {
75
75
  return value === null || isPlainObject(value);
76
76
  }
77
77
  },
@@ -112,7 +112,13 @@ export const dataTypes = {
112
112
  const ml = Math.min(Math.max(field.maxlength, 0), m);
113
113
  return value === null || typeof value === 'string' && (!ml || value.length <= ml)
114
114
  },
115
- anonymize: anonymizeText
115
+ anonymize: anonymizeText,
116
+ filter: async (value, field) => {
117
+ if (field.encrypted && value) {
118
+ return encryptValue(value);
119
+ }
120
+ return value;
121
+ }
116
122
  },
117
123
  code: {
118
124
  validate: (value, field) => {
@@ -142,8 +148,12 @@ export const dataTypes = {
142
148
  const ml = Math.min(Math.max(field.maxlength, 0), m);
143
149
  return value === null || typeof value === 'string' && (!ml || value.length <= ml)
144
150
  },
145
- filter: async (value) => {
146
- return sanitizeHtml(value, optionsSanitizer);
151
+ filter: async (value, field) => {
152
+ const sanitized = sanitizeHtml(value, optionsSanitizer);
153
+ if (field.encrypted && sanitized) {
154
+ return encryptValue(sanitized);
155
+ }
156
+ return sanitized;
147
157
  },
148
158
  anonymize: anonymizeText
149
159
  },
@@ -152,7 +162,7 @@ export const dataTypes = {
152
162
  if (value === null)
153
163
  return true;
154
164
  const m = Config.Get('maxStringLength', maxStringLength);
155
- const ml = Math.min(Math.max(field.maxlength, 0), maxStringLength);
165
+ const ml = Math.min(Math.max(field.maxlength, 0), m);
156
166
  // La valeur peut être une chaîne de caractères...
157
167
  if (typeof value === 'string') {
158
168
  return !ml || value.length <= ml;
@@ -850,7 +860,7 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
850
860
  return {success: true, data: firstInsertedDoc, insertedIds: insertedIds.map(id => id.toString())};
851
861
 
852
862
  } catch (error) { // Attrape les erreurs de permission ou de pushDataUnsecure
853
- logger.error(`[insertData] Main error during insertion process for model ${modelName}: ${error.message}`, error.stack);
863
+ logger?.error(`[insertData] Main error during insertion process for model ${modelName}: ${error.message}`, error.stack);
854
864
  // Renvoyer une structure d'erreur cohérente
855
865
  return {
856
866
  success: false,
@@ -1474,7 +1484,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1474
1484
  };
1475
1485
 
1476
1486
  } catch (error) {
1477
- logger.error("Erreur lors de la mise à jour de la ressource :", error);
1487
+ logger?.error("Erreur lors de la mise à jour de la ressource :", error);
1478
1488
  return {success: false, error: error.message};
1479
1489
  }
1480
1490
  };
@@ -1716,9 +1726,10 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
1716
1726
  } else {
1717
1727
  logger.info(`[deleteData] No documents to delete for user ${user?.username} after permission checks or matching criteria.`);
1718
1728
  }
1719
-
1720
1729
  const res = { success: true, deletedCount };
1721
1730
 
1731
+ if (!modelNameToInvalidate)
1732
+ return res;
1722
1733
  // --- CORRECTION ---
1723
1734
  // The event payload must match what the history listener expects: { modelName, user, before }.
1724
1735
  // 'documentsToDelete' contains the full documents before they were deleted.
@@ -1845,7 +1856,6 @@ export const searchData = async (query, user) => {
1845
1856
  // Vérifier si les données sont en cache
1846
1857
  const cachedData = searchCache.get(cacheKey);
1847
1858
  if (cachedData) {
1848
- console.log('Cache hit for key:', cacheKey);
1849
1859
  return cachedData;
1850
1860
  }
1851
1861
 
@@ -3090,7 +3100,6 @@ export const exportData = async (options, user) => {
3090
3100
  * Installs pack models and data for a user or globally.
3091
3101
  * Can accept either a pack ID (to install from database) or a direct pack JSON object.
3092
3102
  *
3093
- * @param {object} logger - Logger instance
3094
3103
  * @param {string|object} packIdentifier - Either pack ID (string) or pack JSON object
3095
3104
  * @param {object|null} user - User object (if installing for user) or null (for global install)
3096
3105
  * @param {string} [lang='en'] - Language code for localized data
@@ -3099,7 +3108,6 @@ export const exportData = async (options, user) => {
3099
3108
  export async function installPack(packIdentifier, user = null, lang = 'en', options = {}) {
3100
3109
  let pack;
3101
3110
  const packsCollection = getCollection('packs');
3102
-
3103
3111
  // Determine if we're working with an ID or direct pack object
3104
3112
  if (typeof packIdentifier === 'string') {
3105
3113
  let p;
@@ -3139,12 +3147,19 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3139
3147
  throw new Error('Invalid pack identifier - must be either pack ID string or pack object');
3140
3148
  }
3141
3149
 
3150
+ // Vérifier si le pack est déjà installé pour cet utilisateur
3151
+ const existingPack = await packsCollection.findOne({ name: pack.name, _user: user?.username });
3152
+ if (existingPack) {
3153
+ logger?.warn(`Pack '${pack.name}' is already installed for user '${user?.username}'. Skipping installation.`);
3154
+ return { success: true, summary: {}, errors: [], modifiedCount: 0 };
3155
+ }
3156
+
3142
3157
  const username = user ? user.username : null;
3143
3158
  const logPrefix = username
3144
3159
  ? `Installing pack '${pack.name}' for user '${username}'`
3145
3160
  : `Installing pack '${pack.name}' globally`;
3146
3161
 
3147
- logger.info(`--- ${logPrefix} ---`);
3162
+ logger?.info(`--- ${logPrefix} ---`);
3148
3163
 
3149
3164
  const summary = {
3150
3165
  models: {installed: [], skipped: [], failed: []},
@@ -3159,9 +3174,8 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3159
3174
  if (Array.isArray(pack.models)) {
3160
3175
  // For user installs, check existing models
3161
3176
  const existingModels = user
3162
- ? await modelsCollection.find({_user: username}).toArray()
3177
+ ? await modelsCollection.find({_user: user.username}).toArray()
3163
3178
  : await modelsCollection.find({_user: {$exists: false}}).toArray();
3164
-
3165
3179
  const existingModelNames = existingModels.map(m => m.name);
3166
3180
 
3167
3181
  for (let i = 0; i < pack.models.length; i++) {
@@ -3170,8 +3184,9 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3170
3184
  const modelName = typeof modelOrName === 'string' ? modelOrName : modelOrName?.name;
3171
3185
  if (!modelName) throw new Error('Model definition in pack is missing a name.');
3172
3186
 
3173
- if (existingModelNames.includes(modelName)) {
3174
- logger.debug(`[Model Install] Skipping '${modelName}': already exists`);
3187
+ const r = typeof user?.username === 'string' && await modelsCollection.findOne({name: modelName, _user: user.username || /.*/});
3188
+ if (r ) {
3189
+ logger?.debug(`[Model Install] Skipping '${modelName}': already exists`);
3175
3190
  summary.models.skipped.push(modelName);
3176
3191
  continue;
3177
3192
  }
@@ -3198,7 +3213,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3198
3213
  await modelsCollection.insertOne(preparedModel);
3199
3214
  summary.models.installed.push(modelName);
3200
3215
  } catch (e) {
3201
- logger.error(e);
3216
+ logger?.error(e);
3202
3217
  const modelName = typeof modelOrName === 'string' ? modelOrName : modelOrName?.name || 'unknown';
3203
3218
  errors.push(`Failed to install model '${modelName}': ${e.message}`);
3204
3219
  summary.models.failed.push(modelName);
@@ -3209,8 +3224,8 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3209
3224
  // --- PHASE 2: DATA INSTALLATION ---
3210
3225
  const dataToInstall = {...pack.data?.all, ...pack.data?.[lang]};
3211
3226
  if (!dataToInstall || Object.keys(dataToInstall).length === 0) {
3212
- logger.warn(`Pack '${pack.name}' has no data to install.`);
3213
- return {success: false, summary, errors, modifiedCount: 0};
3227
+ logger?.warn(`Pack '${pack.name}' has no data to install.`);
3228
+ return {success: errors.length === 0, summary, errors, modifiedCount: 0};
3214
3229
  }
3215
3230
 
3216
3231
  // Process link references (same as original)
@@ -3243,49 +3258,49 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3243
3258
  const documents = dataToInstall[modelName];
3244
3259
  if (documents.length === 0) continue;
3245
3260
 
3246
- const docsToInsert = [];
3247
- const modelDefForHash = await getModel(modelName, user);
3261
+ try {
3262
+ const docsToInsert = [];
3263
+ const modelDefForHash = await getModel(modelName, user);
3248
3264
 
3249
- for (const docSource of documents) {
3250
- let docForInsert = {...docSource};
3265
+ for (const docSource of documents) {
3266
+ let docForInsert = {...docSource};
3251
3267
 
3252
- // Clear $link fields for first pass
3253
- for (const key in docForInsert) {
3254
- if (isPlainObject(docForInsert[key]) && docForInsert[key].$link) {
3255
- docForInsert[key] = null;
3268
+ // Clear $link fields for first pass
3269
+ for (const key in docForInsert) {
3270
+ if (isPlainObject(docForInsert[key]) && docForInsert[key].$link) {
3271
+ docForInsert[key] = null;
3272
+ }
3256
3273
  }
3257
- }
3258
3274
 
3259
- const tempId = docForInsert._temp_pack_id;
3260
- delete docForInsert._id;
3261
- delete docForInsert._temp_pack_id;
3275
+ const tempId = docForInsert._temp_pack_id;
3276
+ delete docForInsert._id;
3277
+ delete docForInsert._temp_pack_id;
3262
3278
 
3263
- if (user) docForInsert._user = username;
3264
-
3265
- docForInsert._model = modelName;
3266
- docForInsert._hash = getFieldValueHash(modelDefForHash, docForInsert);
3279
+ if (user) docForInsert._user = username;
3267
3280
 
3268
- // Check for existing document
3269
- const existingQuery = {
3270
- _hash: docForInsert._hash,
3271
- _model: modelName
3272
- };
3281
+ docForInsert._model = modelName;
3282
+ docForInsert._hash = getFieldValueHash(modelDefForHash, docForInsert);
3273
3283
 
3274
- if (docForInsert._env) existingQuery._env = docForInsert._env;
3275
- if (user) existingQuery._user = username;
3284
+ // Check for existing document
3285
+ const existingQuery = {
3286
+ _hash: docForInsert._hash,
3287
+ _model: modelName
3288
+ };
3276
3289
 
3277
- const existingDoc = await collection.findOne(existingQuery, {projection: {_id: 1}});
3278
- if (existingDoc) {
3279
- tempIdToNewIdMap[tempId] = existingDoc._id;
3280
- summary.datas.skipped++;
3281
- } else {
3282
- docForInsert._temp_pack_id_for_mapping = tempId;
3283
- docsToInsert.push(docForInsert);
3290
+ if (docForInsert._env) existingQuery._env = docForInsert._env;
3291
+ if (user) existingQuery._user = username;
3292
+
3293
+ const existingDoc = await collection.findOne(existingQuery, {projection: {_id: 1}});
3294
+ if (existingDoc) {
3295
+ tempIdToNewIdMap[tempId] = existingDoc._id;
3296
+ summary.datas.skipped++;
3297
+ } else {
3298
+ docForInsert._temp_pack_id_for_mapping = tempId;
3299
+ docsToInsert.push(docForInsert);
3300
+ }
3284
3301
  }
3285
- }
3286
3302
 
3287
- if (docsToInsert.length > 0) {
3288
- try {
3303
+ if (docsToInsert.length > 0) {
3289
3304
  const finalDocsToInsert = docsToInsert.map(d => {
3290
3305
  const doc = {...d};
3291
3306
  delete doc._temp_pack_id_for_mapping;
@@ -3303,22 +3318,22 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3303
3318
  tempIdToNewIdMap[doc._temp_pack_id_for_mapping] = result.insertedIds[index];
3304
3319
  }
3305
3320
  });
3306
- } catch (e) {
3307
- summary.datas.failed += docsToInsert.length;
3308
- errors.push(`Error inserting batch for ${modelName}: ${e.message}`);
3309
- logger.error(`[Pack Install] Error on insertMany for model ${modelName}:`, e);
3310
3321
  }
3322
+ } catch (e) {
3323
+ summary.datas.failed += documents.length;
3324
+ errors.push(`Error processing model ${modelName}: ${e.message}`);
3325
+ logger?.error(`[Pack Install] Error processing model ${modelName}:`, e);
3311
3326
  }
3312
3327
  }
3313
3328
 
3314
3329
  // --- PASS 2: REFERENCE LINKING ---
3315
- logger.info(`[Pack Install] Starting Pass 2: Linking ${linkQueue.length} references`);
3330
+ logger?.info(`[Pack Install] Starting Pass 2: Linking ${linkQueue.length} references`);
3316
3331
  for (const linkOp of linkQueue) {
3317
3332
  const {sourceTempId, sourceModelName, fieldName, linkSelector} = linkOp;
3318
3333
  const sourceId = tempIdToNewIdMap[sourceTempId];
3319
3334
 
3320
3335
  if (!sourceId) {
3321
- logger.warn(`[LINK FAILED] Could not find newly inserted document for temp ID ${sourceTempId}. Skipping link.`);
3336
+ logger?.warn(`[LINK FAILED] Could not find newly inserted document for temp ID ${sourceTempId}. Skipping link.`);
3322
3337
  continue;
3323
3338
  }
3324
3339
 
@@ -3330,7 +3345,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3330
3345
  const fieldDef = sourceModelDef.fields.find(f => f.name === fieldName);
3331
3346
 
3332
3347
  if (!fieldDef) {
3333
- logger.warn(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}'`);
3348
+ logger?.warn(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}'`);
3334
3349
  errors.push(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}'`);
3335
3350
  summary.datas.failed++;
3336
3351
  continue;
@@ -3344,7 +3359,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3344
3359
 
3345
3360
  if (!targetDocs || targetDocs.length === 0) {
3346
3361
  const errorMsg = `[LINK FAILED] No target found for ${JSON.stringify(linkSelector)}`;
3347
- logger.warn(errorMsg);
3362
+ logger?.warn(errorMsg);
3348
3363
  errors.push(errorMsg);
3349
3364
  summary.datas.failed++;
3350
3365
  continue;
@@ -3363,7 +3378,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3363
3378
 
3364
3379
  } catch (e) {
3365
3380
  const errorMsg = `[LINK CRITICAL] Error linking ${sourceModelName}.${fieldName}: ${e.message}`;
3366
- logger.error(errorMsg, e.stack);
3381
+ logger?.error(errorMsg, e.stack);
3367
3382
  errors.push(errorMsg);
3368
3383
  summary.datas.failed++;
3369
3384
  }
@@ -3372,7 +3387,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3372
3387
  if (options.installForUser && user?.username) {
3373
3388
  if (pack.name)
3374
3389
  await packsCollection.deleteOne({name: pack.name, _user: user.username});
3375
- logger.info(`--- Creating pack '${pack.name}' for user... ---`);
3390
+ logger?.info(`--- Creating pack '${pack.name}' for user... ---`);
3376
3391
  const packToCreate = {...pack, _id: undefined, private: true, _user: user.username};
3377
3392
  await packsCollection.insertOne(packToCreate);
3378
3393
  }
@@ -3383,7 +3398,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3383
3398
  }
3384
3399
 
3385
3400
  const modifiedCount = summary.datas.inserted + summary.datas.updated;
3386
- logger.info(`--- ${logPrefix} completed ---`);
3401
+ logger?.info(`--- ${logPrefix} completed ---`);
3387
3402
  return {
3388
3403
  success: errors.length === 0,
3389
3404
  summary,
@@ -581,7 +581,8 @@ export const checkHash = async (me, model, hash, excludeId = null) => {
581
581
  const query = {
582
582
  _model: model.name,
583
583
  _hash: hash,
584
- ...(excludeId && {_id: {$ne: new ObjectId(excludeId)}})
584
+ ...(excludeId && {_id: {$ne: new ObjectId(excludeId)}}),
585
+ _user: me.username
585
586
  };
586
587
 
587
588
  console.log("Query being executed:", JSON.stringify(query, null, 2));
@@ -21,7 +21,7 @@ export function onInit(defaultEngine) {
21
21
 
22
22
  export const cancelAlerts = async (user) => {
23
23
 
24
- const datasCollection = getCollection('datas'); // Alerts are in the global collection
24
+ const datasCollection = getCollection(Config.Get('dataCollection', 'datas')); // Alerts are in the global collection
25
25
 
26
26
  // 1. Fetch the latest state of the alert
27
27
  const alertDocs = await datasCollection.find({_user: user.username, _model: 'alert'}).toArray();
@@ -3,31 +3,36 @@ import {Logger} from "../gameObject.js";
3
3
  import {MongoDatabase} from "../engine.js";
4
4
  import {ObjectId} from "mongodb";
5
5
  import {isLocalUser} from "../data.js";
6
+ import {Event} from "../events.js";
7
+ import {Config} from "../config.js";
6
8
 
7
9
  export let modelsCollection, datasCollection, filesCollection, packsCollection;
8
10
 
9
11
  export {ObjectId};
10
12
 
11
- let engine, logger;
13
+ let engine, logger, currentDb
14
+
12
15
  let colls= [];
13
16
  export async function onInit(defaultEngine) {
14
17
  engine = defaultEngine;
15
18
  logger = engine.getComponent(Logger);
16
19
 
20
+ currentDb = MongoDatabase();
17
21
  modelsCollection = getCollection("models");
18
- datasCollection = getCollection("datas");
22
+ datasCollection = getCollection(Config.Get('dataCollection', 'datas'));
19
23
  filesCollection = getCollection("files");
20
24
  packsCollection = getCollection("packs");
21
25
 
22
- colls = await MongoDatabase.listCollections().toArray();
26
+ colls = await currentDb.listCollections().toArray();
23
27
 
28
+ await Event.Trigger("OnDatabaseLoaded", "system", "calls", engine)
24
29
  logger.info("MongoDB collections loaded.");
25
30
  }
26
31
 
27
32
  export const getCollections= async (forceRefresh)=>{
28
33
  if( !forceRefresh )
29
34
  return colls;
30
- colls = await MongoDatabase.listCollections().toArray();
35
+ colls = await currentDb.listCollections().toArray();
31
36
  }
32
37
 
33
38
  export const createCollection = async (coll)=>{
@@ -35,7 +40,7 @@ export const createCollection = async (coll)=>{
35
40
  if( found){
36
41
  return getCollection(coll);
37
42
  }
38
- return await MongoDatabase.createCollection(coll);
43
+ return await currentDb.createCollection(coll);
39
44
  }
40
45
 
41
46
  export const isObjectId = (id) => {
@@ -44,15 +49,18 @@ export const isObjectId = (id) => {
44
49
 
45
50
 
46
51
  export const getCollection = (str) => {
47
- return MongoDatabase.collection(str);
52
+ return currentDb.collection(str);
48
53
  }
49
54
 
50
-
55
+ export const getDatabase = () => {
56
+ return currentDb;
57
+ }
51
58
 
52
59
  // New function to determine the collection name for a user
53
60
  export const getUserCollectionName = async (user) => {
54
61
  const feat = await engine.userProvider.hasFeature(user, 'indexes');
55
- return feat ? (isLocalUser(user) ? `datas_${user._user}` :`datas_${user.username}` ) : 'datas';
62
+ const dataCollectionName = Config.Get('dataCollection', 'datas');
63
+ return feat ? (isLocalUser(user) ? `${dataCollectionName}_${user._user}` :`${dataCollectionName}_${user.username}` ) : dataCollectionName;
56
64
  };
57
65
 
58
66
 
@@ -1,5 +1,4 @@
1
1
  import i18n from "../i18n.js";
2
- import {MongoDatabase} from "../engine.js";
3
2
  import {
4
3
  createCollection,
5
4
  getCollection,