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.
- package/README.md +35 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +2 -6
- package/client/src/App.scss +12 -0
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +27 -10
- package/client/src/Dashboard.jsx +480 -396
- package/client/src/Dashboard.scss +1 -1
- package/client/src/DashboardHtmlViewItem.jsx +147 -0
- package/client/src/DashboardView.jsx +654 -569
- package/client/src/DataEditor.jsx +10 -3
- package/client/src/DataLayout.jsx +805 -755
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +39 -75
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +2057 -1825
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +47 -14
- package/client/src/HtmlViewBuilderModal.jsx +91 -0
- package/client/src/HtmlViewBuilderModal.scss +18 -0
- package/client/src/HtmlViewCard.jsx +44 -0
- package/client/src/HtmlViewCard.scss +35 -0
- package/client/src/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +5 -4
- package/client/src/ModelCreatorField.jsx +51 -4
- package/client/src/ModelList.jsx +92 -53
- package/client/src/Notification.jsx +136 -136
- package/client/src/Notification.scss +0 -18
- package/client/src/Pagination.jsx +5 -3
- package/client/src/RelationField.jsx +354 -258
- package/client/src/RelationSelectorWidget.jsx +173 -0
- package/client/src/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +262 -212
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +24 -24
- package/package.json +2 -1
- package/src/constants.js +1 -1
- package/src/defaultModels.js +1596 -1544
- package/src/i18n.js +710 -10
- package/src/modules/assistant/assistant.js +148 -18
- package/src/modules/bucket.js +2 -1
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +531 -492
- package/src/modules/data/data.js +3 -53
- package/src/modules/data/data.operations.js +77 -26
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +1879 -1821
- package/src/modules/data/data.validation.js +81 -2
- package/src/modules/file.js +247 -238
- package/src/packs.js +5482 -5478
- package/test/data.integration.test.js +1115 -1060
package/src/modules/data/data.js
CHANGED
|
@@ -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
|
-
|
|
365
|
-
|
|
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
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
1157
|
+
// 7. Application des filtres de champ (ex: hashage de mot de passe)
|
|
1106
1158
|
for (const field of model.fields) {
|
|
1107
|
-
|
|
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
|
-
|
|
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'
|
|
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 (
|
|
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);
|