data-primals-engine 1.5.0 → 1.5.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 (52) hide show
  1. package/README.md +35 -0
  2. package/client/src/AddWidgetTypeModal.jsx +47 -43
  3. package/client/src/App.jsx +2 -6
  4. package/client/src/App.scss +12 -0
  5. package/client/src/AssistantChat.jsx +363 -323
  6. package/client/src/AssistantChat.scss +27 -10
  7. package/client/src/Dashboard.jsx +480 -396
  8. package/client/src/Dashboard.scss +1 -1
  9. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  10. package/client/src/DashboardView.jsx +654 -569
  11. package/client/src/DataEditor.jsx +10 -3
  12. package/client/src/DataLayout.jsx +805 -755
  13. package/client/src/DataLayout.scss +14 -0
  14. package/client/src/DataTable.jsx +39 -75
  15. package/client/src/Dialog.scss +1 -1
  16. package/client/src/Field.jsx +2057 -1825
  17. package/client/src/FlexViewCard.jsx +44 -0
  18. package/client/src/HistoryDialog.jsx +47 -14
  19. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  20. package/client/src/HtmlViewBuilderModal.scss +18 -0
  21. package/client/src/HtmlViewCard.jsx +44 -0
  22. package/client/src/HtmlViewCard.scss +35 -0
  23. package/client/src/KanbanCard.jsx +1 -2
  24. package/client/src/ModelCreator.jsx +5 -4
  25. package/client/src/ModelCreatorField.jsx +51 -4
  26. package/client/src/ModelList.jsx +92 -53
  27. package/client/src/Notification.jsx +136 -136
  28. package/client/src/Notification.scss +0 -18
  29. package/client/src/Pagination.jsx +5 -3
  30. package/client/src/RelationField.jsx +354 -258
  31. package/client/src/RelationSelectorWidget.jsx +173 -0
  32. package/client/src/contexts/ModelContext.jsx +10 -1
  33. package/client/src/contexts/UIContext.jsx +72 -63
  34. package/client/src/filter.js +262 -212
  35. package/client/src/hooks/useValidation.js +75 -0
  36. package/client/src/translations.js +24 -24
  37. package/package.json +2 -1
  38. package/src/constants.js +1 -1
  39. package/src/defaultModels.js +1596 -1544
  40. package/src/i18n.js +710 -10
  41. package/src/modules/assistant/assistant.js +148 -18
  42. package/src/modules/bucket.js +2 -1
  43. package/src/modules/data/data.core.js +118 -92
  44. package/src/modules/data/data.history.js +531 -492
  45. package/src/modules/data/data.js +3 -53
  46. package/src/modules/data/data.operations.js +77 -26
  47. package/src/modules/data/data.relations.js +686 -686
  48. package/src/modules/data/data.routes.js +1879 -1821
  49. package/src/modules/data/data.validation.js +81 -2
  50. package/src/modules/file.js +247 -238
  51. package/src/packs.js +5482 -5478
  52. package/test/data.integration.test.js +1115 -1060
@@ -159,56 +159,6 @@ export async function onInit(defaultEngine) {
159
159
 
160
160
  // Triggers
161
161
 
162
- Event.Listen("OnValidateModelStructure", async (modelStructure) =>{
163
-
164
- const objectKeys = Object.keys(modelStructure);
165
-
166
- if( objectKeys.find(o => !["name", "_user", "icon", "history", "locked", "_id", "description", "maxRequestData", "fields"].includes(o)) ){
167
- throw new Error(i18n.t('api.model.invalidStructure'));
168
- }
169
-
170
- // Vérification du type de name
171
- if (typeof modelStructure.name !== 'string' || !modelStructure.name) {
172
- throw new Error(i18n.t("api.validate.requiredFieldString", ["name"]));
173
- }
174
-
175
- // Vérification du type de description
176
- if (typeof modelStructure.description !== 'string') {
177
- throw new Error(i18n.t("api.validate.fieldString", ["description"]));
178
- }
179
-
180
- // Vérification de la présence et du type du tableau fields
181
- if (!Array.isArray(modelStructure.fields)) {
182
- throw new Error(i18n.t('api.validate.fieldArray', ["fields"]));
183
- }
184
-
185
- // Vérification de chaque champ dans le tableau fields
186
- for (const field of modelStructure.fields) {
187
- validateField(field);
188
- }
189
-
190
- if (modelStructure.constraints) {
191
- if (!Array.isArray(modelStructure.constraints)) {
192
- throw new Error('Model "constraints" property must be an array.');
193
- }
194
- const fieldNames = new Set(modelStructure.fields.map(f => f.name));
195
- for (const constraint of modelStructure.constraints) {
196
- if (constraint.type === 'unique') {
197
- if (!constraint.name || !Array.isArray(constraint.keys) || constraint.keys.length === 0) {
198
- throw new Error('Unique constraint must have a "name" and a non-empty "keys" array.');
199
- }
200
- for (const key of constraint.keys) {
201
- if (!fieldNames.has(key)) {
202
- throw new Error(`Constraint key "${key}" in constraint "${constraint.name}" does not exist as a field in the model.`);
203
- }
204
- }
205
- }
206
- }
207
- }
208
-
209
- return true; // La structure du modèle est valide
210
- }, "event", "system");
211
-
212
162
  }
213
163
 
214
164
 
@@ -327,13 +277,13 @@ export async function handleDemoInitialization(req, res) {
327
277
 
328
278
  logger.info(`[Demo Init] Installing dynamically generated pack with models: [${models.join(', ')}].`);
329
279
 
280
+ // Create and install pack
281
+ const result = await installPack(packToInstall, user, req.query.lang || 'en');
282
+
330
283
  await sequential(packs.map(p => {
331
284
  return () => installPack(p, user, req.query.lang || 'en');
332
285
  }));
333
286
 
334
- // Create and install pack
335
- const result = await installPack(packToInstall, user, req.query.lang || 'en');
336
-
337
287
  if (result.success || result.modifiedCount > 0) {
338
288
 
339
289
  await Event.Trigger('OnDemoUserAdded', "event", "system", req.me.username);
@@ -49,11 +49,11 @@ import {
49
49
  checkHash,
50
50
  convertDataTypes,
51
51
  handleFilesIfNeeded,
52
- processDocuments,
53
- processFileArray
52
+ processDocuments
54
53
  } from "./data.relations.js";
55
54
  import crypto from 'crypto';
56
55
  import cronstrue from 'cronstrue/i18n.js';
56
+ import util from "node:util";
57
57
 
58
58
  const delay = ms => new Promise(res => setTimeout(res, ms));
59
59
  const IMPORT_CHUNK_SIZE = 100; // Nombre d'enregistrements à traiter par lot
@@ -361,10 +361,11 @@ export const dataTypes = {
361
361
  return false; // Invalid type
362
362
  },
363
363
  filter: async (value, field, reqFile) => {
364
- if (typeof value !== 'object') {
365
- return null;
366
- }
367
-
364
+ // The value is expected to be a GUID string at this point,
365
+ // which was generated by the `addFile` function.
366
+ // This filter should not modify it.
367
+ // The previous implementation returned null for non-objects,
368
+ // which caused the GUID to be erased.
368
369
  return value;
369
370
  },
370
371
  anonymize: () => null
@@ -815,6 +816,29 @@ export const pushDataUnsecure = async (data, modelName, me, files = {}) => {
815
816
  if (datas.length === 0) {
816
817
  return [];
817
818
  }
819
+ console.log(util.inspect(files, false,8, true));
820
+
821
+ // Traitement des fichiers avant l'insertion.
822
+ // Cette logique suppose que si des fichiers sont présents, il s'agit d'une insertion unique.
823
+ if (Object.keys(files).length > 0) {
824
+
825
+ const docToModify = datas[0];
826
+ const fileFields = model.fields.filter(f => f.type === 'file' || (f.type === 'array' && f.itemsType === 'file'));
827
+
828
+ for (const field of fileFields) {
829
+ const fileData = Object.keys(files).filter(f => f.startsWith(field.name+'[')).map(k => files[k]);
830
+ if (fileData && fileData.length > 0) {
831
+ if (field.type === 'file') {
832
+ const fileRef = await addFile(fileData[0], me);
833
+ docToModify[field.name] = fileRef;
834
+ } else if (field.type === 'array' && field.itemsType === 'file') {
835
+ const fileArray = Array.isArray(fileData) ? fileData : [fileData];
836
+ const fileRefs = await Promise.all(fileArray.map(f => addFile(f, me)));
837
+ docToModify[field.name] = fileRefs;
838
+ }
839
+ }
840
+ }
841
+ }
818
842
 
819
843
  // 2. Vérification des limites (en parallèle avec les contraintes)
820
844
  const [_, violations] = await Promise.all([
@@ -1037,21 +1061,49 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1037
1061
  delete updateData._model;
1038
1062
  delete updateData._user;
1039
1063
 
1040
- // Traitement des fichiers (inchangé)
1064
+ // Traitement des fichiers
1041
1065
  const fileFields = model.fields.filter(f => f.type === 'file' || (f.type === 'array' && f.itemsType === 'file'));
1042
1066
  for (const field of fileFields) {
1043
- if (files?.[field.name + '[0]']) {
1044
- if (field.type === 'file') {
1045
- updateData[field.name] = await addFile(files[field.name + '[0]'][0], user);
1046
- } else if (field.type === 'array' && field.itemsType === 'file') {
1047
- const currentFiles = existingDocs[0]?.[field.name] || [];
1048
- const newFiles = await processFileArray(files[field.name + '[0]'], currentFiles, user);
1049
- updateData[field.name] = newFiles;
1067
+ const fieldName = field.name;
1068
+ const uploadedFileOrFiles = files?.[fieldName];
1069
+
1070
+ if (field.type === 'file') {
1071
+ if (uploadedFileOrFiles) { // Un nouveau fichier a été uploadé, on remplace l'ancien
1072
+ const newFile = Array.isArray(uploadedFileOrFiles) ? uploadedFileOrFiles[0] : uploadedFileOrFiles;
1073
+ const newFileRef = await addFile(newFile, user);
1074
+ updateData[fieldName] = newFileRef;
1075
+
1076
+ // Supprimer l'ancien fichier s'il existait
1077
+ const oldFileGuid = existingDocs[0]?.[fieldName];
1078
+ if (oldFileGuid) {
1079
+ await removeFile(oldFileGuid, user);
1080
+ }
1081
+ } else if (data[fieldName] === null && existingDocs[0]?.[fieldName]) {
1082
+ // Le champ a été vidé, on supprime l'ancien fichier
1083
+ await removeFile(existingDocs[0][fieldName], user);
1084
+ }
1085
+ } else if (field.type === 'array' && field.itemsType === 'file') {
1086
+ // Les GUIDs des fichiers existants à conserver sont passés dans `data`
1087
+ const filesToKeepGuids = data[fieldName] || [];
1088
+ const currentGuidsInDb = existingDocs[0]?.[fieldName] || [];
1089
+ let finalGuids = [...filesToKeepGuids];
1090
+
1091
+ if (uploadedFileOrFiles) { // Ajout des nouveaux fichiers uploadés
1092
+ const newFilesArray = Array.isArray(uploadedFileOrFiles) ? uploadedFileOrFiles : [uploadedFileOrFiles];
1093
+ const newFileRefs = await Promise.all(newFilesArray.map(f => addFile(f, user)));
1094
+ finalGuids.push(...newFileRefs);
1050
1095
  }
1096
+
1097
+ // Déterminer quels fichiers ont été retirés et les supprimer du stockage
1098
+ const filesToKeepSet = new Set(filesToKeepGuids);
1099
+ const filesToRemoveGuids = currentGuidsInDb.filter(guid => !filesToKeepSet.has(guid));
1100
+ await Promise.all(filesToRemoveGuids.map(guid => removeFile(guid, user)));
1101
+
1102
+ updateData[fieldName] = finalGuids;
1051
1103
  }
1052
1104
  }
1053
1105
 
1054
- // 4. Validation adaptée pour patch ou edit (inchangé)
1106
+ // 4. Validation adaptée pour patch ou edit
1055
1107
  if (!isPatch) {
1056
1108
  const dataToValidate = {...existingDocs[0], ...updateData};
1057
1109
  await validateModelData(dataToValidate, model, false);
@@ -1059,7 +1111,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1059
1111
  await validateModelData(updateData, model, true);
1060
1112
  }
1061
1113
 
1062
- // 5. Vérification des champs uniques (inchangé)
1114
+ // 5. Vérification des champs uniques
1063
1115
  const uniqueFields = model.fields.filter(f => f.unique);
1064
1116
  for (const field of uniqueFields) {
1065
1117
  if (updateData[field.name] !== undefined) {
@@ -1078,7 +1130,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1078
1130
  }
1079
1131
  }
1080
1132
 
1081
- // 6. Traitement des relations (inchangé)
1133
+ // 6. Traitement des relations
1082
1134
  const relationFields = model.fields.filter(f => f.type === 'relation');
1083
1135
  for (const field of relationFields) {
1084
1136
  if (updateData[field.name] !== undefined) {
@@ -1102,12 +1154,12 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1102
1154
  }
1103
1155
  }
1104
1156
 
1105
- // 7. Application des filtres de champ (ex: hashage de mot de passe) (inchangé)
1157
+ // 7. Application des filtres de champ (ex: hashage de mot de passe)
1106
1158
  for (const field of model.fields) {
1107
- if (updateData[field.name] !== undefined && dataTypes[field.type]?.filter) {
1159
+ // On saute les champs 'file' car ils ont déjà été traités
1160
+ if (field.type !== 'file' && field.itemsType !== 'file' && updateData[field.name] !== undefined && dataTypes[field.type]?.filter) {
1108
1161
  updateData[field.name] = await dataTypes[field.type].filter(
1109
- field.type === 'file' ? null : updateData[field.name],
1110
- field
1162
+ updateData[field.name], field
1111
1163
  );
1112
1164
  }
1113
1165
  }
@@ -1828,7 +1880,7 @@ export const searchData = async (query, user) => {
1828
1880
  [fi.name + "_details_temp"]: 0
1829
1881
  }
1830
1882
  });
1831
- } else if (fi.type === 'array' && fi.itemsType === 'file' && depthParam !== 1) {
1883
+ } else if (fi.type === 'array' && fi.itemsType === 'file') {
1832
1884
  pipelinesLookups.push(
1833
1885
  {
1834
1886
  $lookup: {
@@ -2858,11 +2910,10 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
2858
2910
  ? await modelsCollection.find({_user: username}).toArray()
2859
2911
  : await modelsCollection.find({_user: {$exists: false}}).toArray();
2860
2912
 
2861
- console.log("EXISTING", existingModels);
2862
-
2863
2913
  const existingModelNames = existingModels.map(m => m.name);
2864
2914
 
2865
- for (const modelOrName of pack.models) {
2915
+ for (let i = 0; i < pack.models.length; i++) {
2916
+ const modelOrName = pack.models[i];
2866
2917
  try {
2867
2918
  const modelName = typeof modelOrName === 'string' ? modelOrName : modelOrName?.name;
2868
2919
  if (!modelName) throw new Error('Model definition in pack is missing a name.');
@@ -2894,8 +2945,8 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
2894
2945
  await validateModelStructure(preparedModel);
2895
2946
  await modelsCollection.insertOne(preparedModel);
2896
2947
  summary.models.installed.push(modelName);
2897
-
2898
2948
  } catch (e) {
2949
+ logger.error(e);
2899
2950
  const modelName = typeof modelOrName === 'string' ? modelOrName : modelOrName?.name || 'unknown';
2900
2951
  errors.push(`Failed to install model '${modelName}': ${e.message}`);
2901
2952
  summary.models.failed.push(modelName);