data-primals-engine 1.2.1 → 1.2.3
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 +69 -9
- package/client/src/DataEditor.jsx +1 -202
- package/client/src/DataLayout.jsx +4 -21
- package/client/src/DataTable.jsx +4 -2
- package/client/src/RestoreConfirmationModal.jsx +0 -137
- package/client/src/constants.js +1 -1
- package/client/src/hooks/useTutorials.jsx +3 -3
- package/package.json +2 -2
- package/src/constants.js +2 -2
- package/src/data.js +4 -4
- package/src/defaultModels.js +1 -14
- package/src/email.js +8 -6
- package/src/engine.js +12 -8
- package/src/events.js +26 -4
- package/src/filter.js +221 -0
- package/src/modules/bucket.js +115 -5
- package/src/modules/data.js +174 -101
- package/src/modules/file.js +98 -57
- package/src/modules/test +147 -0
- package/src/modules/workflow.js +266 -100
- package/src/packs.js +245 -7
- package/test/data.backup.integration.test.js +5 -2
- package/test/data.integration.test.js +10 -4
- package/test/events.test.js +202 -0
- package/test/file.test.js +193 -0
- package/test/import_export.integration.test.js +23 -19
- package/test/model.integration.test.js +20 -19
- package/test/user.test.js +30 -23
- package/test/vm.test.js +51 -0
- package/test/workflow.integration.test.js +6 -1
- package/test/workflow.robustness.test.js +6 -1
package/README.md
CHANGED
|
@@ -18,15 +18,18 @@
|
|
|
18
18
|
|
|
19
19
|
## 🚀 Key Features
|
|
20
20
|
|
|
21
|
-
- **Dynamic
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
21
|
+
- **Dynamic Data Modeling**: Define and update schemas using JSON, no migrations required.
|
|
22
|
+
- **Custom API Endpoints**: Create server-side logic and new API endpoints directly from the UI in a secure, sandboxed environment.
|
|
23
|
+
- **Automation Workflows**: Trigger complex actions based on data events (create, update, delete) or schedules (cron).
|
|
24
|
+
- **Advanced Querying & Aggregation**: Go beyond simple filters with deep relation expansion, complex lookups, and dynamic calculated fields.
|
|
25
|
+
- **Integrated Backup & Restore**: Secure, encrypted user data backups with rotation policies, supporting both local and AWS S3 storage.
|
|
26
|
+
- **Event-Driven & Extensible**: A core event system allows for deep customization and the easy creation of new modules or plugins.
|
|
27
|
+
- **Authentication & Authorization**: Robust role-based access control (RBAC) and pluggable user providers.
|
|
28
|
+
- **Built-in File Management**: Handle file uploads seamlessly with integrated support for AWS S3 storage.
|
|
29
|
+
- **🧠 AI Integration**: Natively supports OpenAI and Google Gemini models via LangChain for content generation, analysis, and more.
|
|
30
|
+
- **🌐 Internationalization (i18n)**: Fully supports multilingual interfaces and user-specific translated data.
|
|
31
|
+
- **📦 Starter Packs**: Quickly bootstrap applications with pre-built data packs for CRM, e-commerce, and more.
|
|
32
|
+
- **📄Auto-Generated API Documentation**: Interactive API documentation available via the interface or at `/api-docs`.
|
|
30
33
|
|
|
31
34
|
|
|
32
35
|
## 🌟 Why Choose data-primals-engine?
|
|
@@ -603,11 +606,68 @@ Expected response :
|
|
|
603
606
|
},
|
|
604
607
|
"publishedPosts": 15
|
|
605
608
|
}
|
|
609
|
+
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
## Extensibility
|
|
614
|
+
|
|
615
|
+
### Events (Triggers) Table
|
|
616
|
+
> You can use the events below to access the engine and manipulate API responses.
|
|
617
|
+
> It is useful for custom modules or middlewares for your application.
|
|
618
|
+
|
|
619
|
+
Just use
|
|
620
|
+
|
|
621
|
+
```javascript
|
|
622
|
+
Event.Listen("OnDataAdded", (data) => {
|
|
623
|
+
my_callback()
|
|
624
|
+
}, "event", "user");
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
or the system version
|
|
628
|
+
```javascript
|
|
629
|
+
Event.Listen("OnDataAdded", (engine, data) => {
|
|
630
|
+
my_callback()
|
|
631
|
+
}, "event", "system");
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
| Event | Description | Scope | Triggered by | Arguments (Payload) |
|
|
635
|
+
| :--- |:------------------------------------------------------------------------| :--- | :--- | :--- |
|
|
636
|
+
| OnServerStart | Triggered once the HTTP server is started and listening. | System | engine.start() | engine |
|
|
637
|
+
| OnServerStop | Triggered right after the HTTP server is stopped. | System | engine.stop() | engine |
|
|
638
|
+
| OnModelsLoaded | Triggered after the initial models are loaded and validated at startup. | System | setupInitialModels() | engine, dbModels |
|
|
639
|
+
| OnModelsDeleted | Triggered after all models are deleted via the reset function. | System | engine.resetModels() | engine |
|
|
640
|
+
| OnUserDataDumped | Triggered after a user's data has been backed up (dumped). | System | jobDumpUserData() | engine |
|
|
641
|
+
| OnDataRestored | Triggered after a user's data has been restored from a backup. | System | loadFromDump() | (none) |
|
|
642
|
+
| OnPackInstalled | Triggered after a data pack has been successfully installed. | System | installPack() | pack |
|
|
643
|
+
| OnModelEdited | Triggered after a model definition has been modified. | System & User | editModel() | System: engine, newModel (Pipeline*)<br>User: newModel (or the version modified by the system) |
|
|
644
|
+
| OnDataAdded | Triggered after new data has been inserted. | System & User | insertData() | System: engine, insertedIds (Pipeline*)<br>User: insertedIds (or the version modified by the system) |
|
|
645
|
+
| OnDataDeleted | Triggered just after data is actually deleted. | System & User | deleteData() | System: engine, {model, filter} (Pipeline*)<br>User: {model, filter} |
|
|
646
|
+
| OnDataSearched | Triggered after a data search. | System & User | searchData() | System: engine, {data, count} (Pipeline*)<br>User: {data, count} (or the version modified by the system) |
|
|
647
|
+
| OnDataExported | Triggered after a data export. | System & User | exportData() | System: engine, exportResults, modelsToExport (Pipeline*)<br>User: exportResults, modelsToExport (or the version modified by the system) |
|
|
648
|
+
|
|
649
|
+
### Triggering events
|
|
650
|
+
|
|
651
|
+
If you want to provide your own hooks, you can call :
|
|
652
|
+
```javascript
|
|
653
|
+
const result = Event.Trigger("OnMyCustomEvent", "event", "user", ...args);
|
|
606
654
|
```
|
|
655
|
+
Results are merged together if multiple events are triggered.
|
|
656
|
+
- strings are concatenated
|
|
657
|
+
- numbers are added
|
|
658
|
+
- booleans are ANDed
|
|
659
|
+
- arrays are concatenated
|
|
660
|
+
- objects are merged using spread operator
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
607
664
|
## 🤝 Contributing
|
|
608
665
|
|
|
666
|
+
Find the issues available for [contributions here](https://github.com/anonympins/data-primals-engine/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22%20no%3Aassignee)
|
|
667
|
+
|
|
609
668
|
1. Fork the repo
|
|
610
669
|
2. Create your feature branch: `git checkout -b feature/your-feature`
|
|
670
|
+
3. Launch ```npm run dev``` and make your changes with hot-reload on local port
|
|
611
671
|
3. Commit changes: `git commit -m "Add new feature"`
|
|
612
672
|
4. Push to your branch: `git push origin feature/your-feature`
|
|
613
673
|
5. Open a pull request
|
|
@@ -25,6 +25,7 @@ import Draggable from "./Draggable.jsx";
|
|
|
25
25
|
import CronBuilder from "./CronBuilder.jsx";
|
|
26
26
|
import RTETrans from "./RTETrans.jsx";
|
|
27
27
|
import uniqid from "uniqid";
|
|
28
|
+
import {isConditionMet} from "data-primals-engine/filter";
|
|
28
29
|
|
|
29
30
|
// ... (fonction getInputType) ...
|
|
30
31
|
// Fonction pour obtenir le type d'input HTML basé sur le type de champ du modèle
|
|
@@ -52,208 +53,6 @@ const getInputType = (fieldType) => {
|
|
|
52
53
|
}
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
/**
|
|
56
|
-
* Evaluates a single condition against form data.
|
|
57
|
-
* @param {object} currentModelDef - The definition of the current model.
|
|
58
|
-
* @param {object} condition - The condition to evaluate.
|
|
59
|
-
* @param {object} formData - The form data.
|
|
60
|
-
* @param {object[]} allModels - An array of all model definitions.
|
|
61
|
-
* @param {object} user - The current user.
|
|
62
|
-
* @returns {boolean} - True if the condition is met, false otherwise.
|
|
63
|
-
*/
|
|
64
|
-
const evaluateSingleCondition = (currentModelDef, condition, formData, allModels, user) => {
|
|
65
|
-
// Condition est directement un filtre MongoDB, donc on l'applique
|
|
66
|
-
// en utilisant les opérateurs et les valeurs qu'il contient.
|
|
67
|
-
|
|
68
|
-
if (!condition || typeof condition !== 'object') {
|
|
69
|
-
console.warn("[Client Eval] Condition is not an object:", condition);
|
|
70
|
-
return true; // Permissive default
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Si la condition contient des opérateurs logiques, on les gère ici
|
|
74
|
-
if (condition.$and || condition.$or || condition.$not || condition.$nor) {
|
|
75
|
-
console.warn("[Client Eval] Condition logique détectée dans evaluateSingleCondition, ce n'est pas attendu. Devrait être géré par isConditionMet.");
|
|
76
|
-
return true; // Permissive default
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (condition.$find) {
|
|
80
|
-
const fieldName = Object.keys(condition)[0];
|
|
81
|
-
const fieldValue = formData[fieldName];
|
|
82
|
-
const findCondition = condition.$find;
|
|
83
|
-
|
|
84
|
-
if (!Array.isArray(fieldValue)) return false;
|
|
85
|
-
|
|
86
|
-
return fieldValue.some(item => {
|
|
87
|
-
// Gestion spéciale pour la syntaxe $eq: ["$$this.field", value]
|
|
88
|
-
if (findCondition.$eq && Array.isArray(findCondition.$eq)) {
|
|
89
|
-
const [fieldPath, value] = findCondition.$eq;
|
|
90
|
-
if (fieldPath.startsWith("$$this.")) {
|
|
91
|
-
const fieldToCheck = fieldPath.replace("$$this.", "");
|
|
92
|
-
return item[fieldToCheck] == value;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Sinon, évaluation normale
|
|
97
|
-
const tempData = { ...item };
|
|
98
|
-
return evaluateSingleCondition(currentModelDef, findCondition, tempData, allModels, user);
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Si la condition contient un opérateur $exists, on le gère ici
|
|
103
|
-
if (condition.$exists !== undefined) {
|
|
104
|
-
const fieldName = Object.keys(condition)[0]; // Récupérer le nom du champ
|
|
105
|
-
const shouldExist = condition.$exists; // Récupérer la valeur de $exists (true ou false)
|
|
106
|
-
const exists = Object.prototype.hasOwnProperty.call(formData, fieldName) && formData[fieldName] !== undefined && formData[fieldName] !== null;
|
|
107
|
-
return exists === shouldExist;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Si la condition contient un opérateur $find, on le gère ici
|
|
111
|
-
if (condition.$find) {
|
|
112
|
-
// Récupérer le nom du champ
|
|
113
|
-
const fieldName = Object.keys(condition)[0];
|
|
114
|
-
const fieldValue = formData[fieldName];
|
|
115
|
-
try {
|
|
116
|
-
// Assuming evaluateSingleCondition handles $find
|
|
117
|
-
return evaluateSingleCondition(currentModelDef, condition.$find, formData, allModels, user);
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.error("Error evaluating $find condition:", condition, error);
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Récupérer le nom du champ et la condition
|
|
125
|
-
const fieldName = Object.keys(condition)[0];
|
|
126
|
-
const fieldValue = condition[fieldName];
|
|
127
|
-
|
|
128
|
-
// Récupérer la définition du champ
|
|
129
|
-
const fieldDef = currentModelDef?.fields.find(f => f.name === fieldName);
|
|
130
|
-
|
|
131
|
-
// Si la définition du champ n'est pas trouvée, on retourne true
|
|
132
|
-
if (!fieldDef) {
|
|
133
|
-
console.warn(`[Client Eval] Field definition not found for field: ${fieldName}`);
|
|
134
|
-
return true; // Permissive default
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
let targetValue = formData[fieldName];
|
|
138
|
-
let processedConditionValue = fieldValue;
|
|
139
|
-
|
|
140
|
-
// 1. Handle $exists (on the first field)
|
|
141
|
-
// 2. Convert condition value based on operator's expected input type
|
|
142
|
-
const fieldType = fieldDef?.type; // Type of the first field
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
processedConditionValue = convertValueType(fieldValue, fieldType);
|
|
146
|
-
} catch (e) {
|
|
147
|
-
logClientEvalWarning(`Error converting value type: ${e.message}`, condition);
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return evaluateComparison(fieldValue, targetValue, processedConditionValue, condition);
|
|
152
|
-
|
|
153
|
-
function logClientEvalWarning(message, details) {
|
|
154
|
-
console.warn(`[Client Eval] ${message}:`, details);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function convertValueType(value, inputType) {
|
|
158
|
-
switch (inputType) {
|
|
159
|
-
case 'number':
|
|
160
|
-
const numValue = parseFloat(value);
|
|
161
|
-
if (isNaN(numValue)) {
|
|
162
|
-
throw new Error(`Invalid number value: ${value}`);
|
|
163
|
-
}
|
|
164
|
-
return numValue;
|
|
165
|
-
case 'boolean':
|
|
166
|
-
return String(value).toLowerCase() === 'true';
|
|
167
|
-
case 'csv':
|
|
168
|
-
return String(value).split(',').map(item => item.trim()).filter(Boolean);
|
|
169
|
-
case 'text':
|
|
170
|
-
default:
|
|
171
|
-
return String(value);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function evaluateComparison(operator, targetValue, processedConditionValue, condition) {
|
|
176
|
-
try {
|
|
177
|
-
switch (typeof operator === 'object' ? Object.keys(operator)[0] : null) {
|
|
178
|
-
case '$eq': return targetValue == processedConditionValue;
|
|
179
|
-
case '$ne': return targetValue != processedConditionValue;
|
|
180
|
-
case '$gt': return targetValue > processedConditionValue;
|
|
181
|
-
case '$lt': return targetValue < processedConditionValue;
|
|
182
|
-
case '$gte': return targetValue >= processedConditionValue;
|
|
183
|
-
case '$lte': return targetValue <= processedConditionValue;
|
|
184
|
-
case '$regex':
|
|
185
|
-
if (typeof targetValue !== 'string') return false;
|
|
186
|
-
if (typeof processedConditionValue !== 'string') return false;
|
|
187
|
-
try {
|
|
188
|
-
const regex = new RegExp(processedConditionValue, 'i');
|
|
189
|
-
return regex.test(targetValue);
|
|
190
|
-
} catch (e) {
|
|
191
|
-
logClientEvalWarning(`Invalid regex pattern: ${processedConditionValue}`, condition);
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
case '$in':
|
|
195
|
-
return Array.isArray(processedConditionValue) && processedConditionValue.includes(String(targetValue));
|
|
196
|
-
case '$nin':
|
|
197
|
-
return !Array.isArray(processedConditionValue) || !processedConditionValue.includes(String(targetValue));
|
|
198
|
-
default:
|
|
199
|
-
logClientEvalWarning(`Unhandled operator in client evaluation logic: ${operator}`, condition);
|
|
200
|
-
return true; // Permissive default
|
|
201
|
-
}
|
|
202
|
-
} catch (evalError) {
|
|
203
|
-
logClientEvalWarning(`Error during client condition evaluation: ${operator}, targetValue=${targetValue}, processedConditionValue=${processedConditionValue}`, condition);
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
export const isConditionMet = (model, cond, formData, allModels, user) => {
|
|
210
|
-
const isNode = (v) => typeof v === 'object' && v !== null;
|
|
211
|
-
|
|
212
|
-
const condition = cond;
|
|
213
|
-
|
|
214
|
-
if (!condition) return true;
|
|
215
|
-
|
|
216
|
-
if (condition.$and && Array.isArray(condition.$and)) {
|
|
217
|
-
if (condition.$and.length === 0) return true;
|
|
218
|
-
return condition.$and.every(sub => isConditionMet(model, sub, formData, allModels, user));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (condition.$or && Array.isArray(condition.$or)) {
|
|
222
|
-
if (condition.$or.length === 0) return false;
|
|
223
|
-
return condition.$or.some(sub => isConditionMet(model, sub, formData, allModels, user));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (condition.$not) {
|
|
227
|
-
return !isConditionMet(model, condition.$not, formData, allModels, user);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (condition.$nor && Array.isArray(condition.$nor)) {
|
|
231
|
-
if (condition.$nor.length === 0) return true;
|
|
232
|
-
return !condition.$nor.some(sub => isConditionMet(model, sub, formData, allModels, user));
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (condition.$find) {
|
|
236
|
-
try {
|
|
237
|
-
// Assuming evaluateSingleCondition handles $find
|
|
238
|
-
return evaluateSingleCondition(model, condition, formData, allModels, user);
|
|
239
|
-
} catch (error) {
|
|
240
|
-
console.error("Error evaluating $find condition:", condition, error);
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (condition.path && condition.op) {
|
|
246
|
-
try {
|
|
247
|
-
return evaluateSingleCondition(model, condition, formData, allModels, user);
|
|
248
|
-
} catch (error) {
|
|
249
|
-
console.error("Error evaluating condition:", condition, error);
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
console.warn("Unknown condition format:", condition);
|
|
255
|
-
return true;
|
|
256
|
-
};
|
|
257
56
|
|
|
258
57
|
export const DataEditor = forwardRef(function MyDataEditor({
|
|
259
58
|
isLoading,
|
|
@@ -2,35 +2,19 @@ import React, {forwardRef, useCallback, useEffect, useMemo, useReducer, useRef,
|
|
|
2
2
|
|
|
3
3
|
import "./App.scss";
|
|
4
4
|
import {useMutation, useQuery, useQueryClient} from "react-query";
|
|
5
|
-
import {CheckboxField, FileField, NumberField, SelectField, TextField} from "./Field.jsx";
|
|
6
5
|
import ModelCreator from "./ModelCreator.jsx";
|
|
7
6
|
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
8
|
-
import {FaMagnifyingGlass, FaPencil} from "react-icons/fa6";
|
|
9
7
|
import {Dialog, DialogProvider} from "./Dialog.jsx";
|
|
10
8
|
import {Pagination} from "./Pagination.jsx";
|
|
9
|
+
import {Event} from "../../src/events.js";
|
|
10
|
+
|
|
11
11
|
import {
|
|
12
|
-
elementsPerPage, kilobytes,
|
|
13
|
-
mainFieldsTypes,
|
|
14
|
-
maxBytesPerSecondThrottleData,
|
|
15
|
-
maxFileSize,
|
|
16
|
-
maxRequestData, metaModels
|
|
17
|
-
} from "../../src/constants.js";
|
|
18
|
-
import {
|
|
19
|
-
FaArrowDown,
|
|
20
|
-
FaArrowUp, FaBell,
|
|
21
|
-
FaBook, FaCopy,
|
|
22
|
-
FaEdit,
|
|
23
|
-
FaFileExport, FaFileImport,
|
|
24
12
|
FaFilter, FaInfo,
|
|
25
|
-
FaLanguage,
|
|
26
|
-
FaLock,
|
|
27
|
-
FaPlus,
|
|
28
|
-
FaTrash
|
|
29
13
|
} from "react-icons/fa";
|
|
30
14
|
import {getDefaultForType, getUserId} from "../../src/data.js";
|
|
31
15
|
import {Trans, useTranslation} from "react-i18next";
|
|
32
16
|
|
|
33
|
-
import {
|
|
17
|
+
import {getObjectHash} from "../../src/core.js";
|
|
34
18
|
import Button from "./Button.jsx";
|
|
35
19
|
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
36
20
|
import APIInfo from "./APIInfo.jsx";
|
|
@@ -49,7 +33,6 @@ import CalendarConfigModal from "./CalendarConfigModal.jsx";
|
|
|
49
33
|
import KanbanView from "./KanbanView.jsx";
|
|
50
34
|
|
|
51
35
|
|
|
52
|
-
// --- AJOUT : Composants Placeholders pour la démonstration ---
|
|
53
36
|
const CalendarView = ({ settings, model }) => (
|
|
54
37
|
<div className="p-4 border rounded-md mt-4 bg-gray-50">
|
|
55
38
|
<h3 className="font-bold">Vue Calendrier</h3>
|
|
@@ -349,7 +332,7 @@ function DataLayout() {
|
|
|
349
332
|
|
|
350
333
|
console.log('Données enregistrées:', data, selectedModel);
|
|
351
334
|
|
|
352
|
-
|
|
335
|
+
Event.Trigger(recordToEdit ? 'API_ADD_DATA' : 'API_ADD_DATA', "custom", "data", {
|
|
353
336
|
model: selectedModel.name,
|
|
354
337
|
});
|
|
355
338
|
|
package/client/src/DataTable.jsx
CHANGED
|
@@ -6,6 +6,8 @@ import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
|
6
6
|
import React, {useEffect, useMemo, useRef, useState} from "react";
|
|
7
7
|
import {getUserId} from "../../src/data.js";
|
|
8
8
|
import cronstrue from 'cronstrue/i18n';
|
|
9
|
+
import {Event} from "../../src/events.js";
|
|
10
|
+
|
|
9
11
|
import {
|
|
10
12
|
FaBook,
|
|
11
13
|
FaCopy,
|
|
@@ -41,7 +43,6 @@ import RelationValue from "./RelationValue.jsx";
|
|
|
41
43
|
import {FaGear, FaPencil, FaTriangleExclamation} from "react-icons/fa6";
|
|
42
44
|
import RestoreConfirmationModal from "./RestoreConfirmationModal.jsx";
|
|
43
45
|
import {event_trigger, isLightColor} from "../../src/core.js";
|
|
44
|
-
import {isConditionMet} from "./DataEditor.jsx";
|
|
45
46
|
import {Tooltip} from "react-tooltip";
|
|
46
47
|
import ExportDialog from "./ExportDialog.jsx";
|
|
47
48
|
import Captions from "yet-another-react-lightbox/plugins/captions";
|
|
@@ -56,6 +57,7 @@ import PackGallery from "./PackGallery.jsx";
|
|
|
56
57
|
import {HiddenableCell} from "./HiddenableCell.jsx";
|
|
57
58
|
import ConditionBuilder from "./ConditionBuilder.jsx";
|
|
58
59
|
import {pagedFilterToMongoConds} from "./filter.js";
|
|
60
|
+
import {isConditionMet} from "data-primals-engine/filter";
|
|
59
61
|
|
|
60
62
|
// Ajoutez cette constante pour la clé de sessionStorage
|
|
61
63
|
const SESSION_STORAGE_IMPORT_JOBS_KEY = 'activeImportJobs';
|
|
@@ -238,7 +240,7 @@ export function DataTable({
|
|
|
238
240
|
console.log('Données non trouvées');
|
|
239
241
|
}
|
|
240
242
|
|
|
241
|
-
|
|
243
|
+
Event.Trigger('API_DELETE_DATA', "custom", "data",{ model: item._model, id: item._id });
|
|
242
244
|
queryClient.invalidateQueries(['api/data', item._model, 'page', page, elementsPerPage, pagedFilters[item._model], pagedSort[item._model]]);
|
|
243
245
|
|
|
244
246
|
} else {
|
|
@@ -15,80 +15,6 @@ const RestoreConfirmationModal = ({ isOpen, onClose, onConfirm, showS3Config = f
|
|
|
15
15
|
const { me, fetchMe } = useAuthContext(); // fetchMe pour recharger les données utilisateur après sauvegarde
|
|
16
16
|
const { addNotification } = useNotificationContext();
|
|
17
17
|
|
|
18
|
-
// États pour les champs de configuration S3
|
|
19
|
-
const [s3Config, setS3Config] = useState({
|
|
20
|
-
bucketName: '',
|
|
21
|
-
accessKeyId: '',
|
|
22
|
-
secretAccessKey: '', // Ne sera pas affiché directement mais envoyé
|
|
23
|
-
region: ''
|
|
24
|
-
});
|
|
25
|
-
const [isSavingS3Config, setIsSavingS3Config] = useState(false);
|
|
26
|
-
const [showConfig, setConfigVisible] = useState(false);
|
|
27
|
-
|
|
28
|
-
// Charger la configuration S3 existante de l'utilisateur au montage si me.s3Config existe
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
if (me?.s3Config && showS3Config) {
|
|
31
|
-
setS3Config({
|
|
32
|
-
bucketName: me.s3Config.bucketName || '',
|
|
33
|
-
accessKeyId: me.s3Config.accessKeyId || '',
|
|
34
|
-
secretAccessKey: '', // Ne pas pré-remplir la clé secrète pour la sécurité
|
|
35
|
-
region: me.s3Config.region || '',
|
|
36
|
-
pathPrefix: me.s3Config.pathPrefix || ''
|
|
37
|
-
});
|
|
38
|
-
} else if (showS3Config) {
|
|
39
|
-
// Réinitialiser si pas de config ou si on quitte la section S3
|
|
40
|
-
setS3Config({ bucketName: '', accessKeyId: '', secretAccessKey: '', region: '', pathPrefix: '' });
|
|
41
|
-
}
|
|
42
|
-
}, [me, showS3Config, isOpen]); // Ajouter isOpen pour recharger si la modale est rouverte
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const handleS3ConfigChange = (e) => {
|
|
46
|
-
const { name, value } = e.target;
|
|
47
|
-
setS3Config(prev => ({ ...prev, [name]: value }));
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const handleSaveS3Config = async () => {
|
|
51
|
-
setIsSavingS3Config(true);
|
|
52
|
-
// Validation basique côté client
|
|
53
|
-
if (!s3Config.bucketName || !s3Config.accessKeyId || !s3Config.region) {
|
|
54
|
-
addNotification({ type: 'error', message: t('backup.s3config.validationError', 'Le nom du bucket, l\'Access Key ID et la Région sont requis.') });
|
|
55
|
-
setIsSavingS3Config(false);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const payload = { ...s3Config };
|
|
61
|
-
// N'envoyer la clé secrète que si elle a été modifiée
|
|
62
|
-
if (!payload.secretAccessKey) {
|
|
63
|
-
delete payload.secretAccessKey;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const response = await fetch('/api/user/s3-config', { // Endpoint pour sauvegarder la config S3
|
|
67
|
-
method: 'POST',
|
|
68
|
-
headers: {
|
|
69
|
-
'Content-Type': 'application/json',
|
|
70
|
-
// Les en-têtes d'authentification (_user, Authorization) sont gérés globalement par le fetch wrapper si tu en as un, sinon ajoute-les ici
|
|
71
|
-
// Exemple:
|
|
72
|
-
// '_user': me?.username,
|
|
73
|
-
// 'Authorization': `Bearer ${me?.token}`,
|
|
74
|
-
},
|
|
75
|
-
body: JSON.stringify(payload),
|
|
76
|
-
});
|
|
77
|
-
const result = await response.json();
|
|
78
|
-
if (response.ok && result.success) {
|
|
79
|
-
addNotification({ status: 'completed', title: t('backup.s3config.saveSuccess', 'Configuration S3 enregistrée avec succès.') });
|
|
80
|
-
} else {
|
|
81
|
-
addNotification({ status: 'error', title: result.error || t('backup.s3config.saveError', 'Erreur lors de l\'enregistrement de la configuration S3.') });
|
|
82
|
-
}
|
|
83
|
-
} catch (error) {
|
|
84
|
-
addNotification({ status: 'error', title: t('backup.s3config.saveError', 'Erreur lors de l\'enregistrement de la configuration S3.') });
|
|
85
|
-
console.error("Error saving S3 config:", error);
|
|
86
|
-
} finally {
|
|
87
|
-
setIsSavingS3Config(false);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
|
|
92
18
|
if (!isOpen) {
|
|
93
19
|
return null;
|
|
94
20
|
}
|
|
@@ -110,71 +36,8 @@ const RestoreConfirmationModal = ({ isOpen, onClose, onConfirm, showS3Config = f
|
|
|
110
36
|
<Trans i18nKey="btns.cancel">Annuler</Trans>
|
|
111
37
|
</Button>
|
|
112
38
|
</div>
|
|
113
|
-
<div className={"flex flex-centered actions"}>
|
|
114
|
-
{!showConfig && (<NavLink onClick={() => setConfigVisible(true)}><Trans i18nKey={"backup.s3config.title"}></Trans></NavLink>)}
|
|
115
|
-
</div>
|
|
116
39
|
</>
|
|
117
40
|
|
|
118
|
-
{showConfig && (<>
|
|
119
|
-
<h2>{t('backup.s3config.title', 'Configuration du stockage S3')}</h2>
|
|
120
|
-
<p><Trans i18nKey="backup.prez"></Trans></p>
|
|
121
|
-
<form className="s3-config-form space-y-4"> {/* Ajout de space-y pour l'espacement vertical */}
|
|
122
|
-
<TextField
|
|
123
|
-
name="bucketName"
|
|
124
|
-
className={"flex flex-1"}
|
|
125
|
-
label={t('backup.s3config.bucketName', 'Nom du Bucket S3')}
|
|
126
|
-
value={s3Config.bucketName}
|
|
127
|
-
onChange={handleS3ConfigChange}
|
|
128
|
-
placeholder="my-bucket-name"
|
|
129
|
-
required
|
|
130
|
-
/>
|
|
131
|
-
<TextField
|
|
132
|
-
name="accessKeyId"
|
|
133
|
-
className={"flex flex-1"}
|
|
134
|
-
label={t('backup.s3config.accessKeyId', 'Access Key ID AWS')}
|
|
135
|
-
value={s3Config.accessKeyId}
|
|
136
|
-
onChange={handleS3ConfigChange}
|
|
137
|
-
placeholder="AKIAIOSFODNN7EXAMPLE"
|
|
138
|
-
required
|
|
139
|
-
/>
|
|
140
|
-
<TextField
|
|
141
|
-
name="secretAccessKey"
|
|
142
|
-
className={"flex flex-1"}
|
|
143
|
-
label={t('backup.s3config.secretAccessKey', 'Secret Access Key AWS')}
|
|
144
|
-
type="password" // Important pour masquer la clé
|
|
145
|
-
value={s3Config.secretAccessKey}
|
|
146
|
-
onChange={handleS3ConfigChange}
|
|
147
|
-
placeholder={t('backup.s3config.secretPlaceholder', 'Laisser vide pour ne pas modifier')} />
|
|
148
|
-
<TextField
|
|
149
|
-
name="region"
|
|
150
|
-
className={"flex flex-1"}
|
|
151
|
-
label={t('backup.s3config.region', 'Région AWS')}
|
|
152
|
-
value={s3Config.region}
|
|
153
|
-
onChange={handleS3ConfigChange}
|
|
154
|
-
placeholder="eu-west-3"
|
|
155
|
-
required
|
|
156
|
-
/>
|
|
157
|
-
<TextField
|
|
158
|
-
name="pathPrefix"
|
|
159
|
-
label={t('backup.s3config.pathPrefix', 'Préfixe de chemin (Optionnel)')}
|
|
160
|
-
value={s3Config.pathPrefix}
|
|
161
|
-
onChange={handleS3ConfigChange}
|
|
162
|
-
placeholder="saves/my-app/"
|
|
163
|
-
help={t('backup.s3config.pathPrefixHelp', 'Ex: "mon-dossier/". Laissez vide pour la racine du bucket.')}
|
|
164
|
-
/>
|
|
165
|
-
<div
|
|
166
|
-
className="modal-actions flex justify-end space-x-2 mt-4"> {/* Flex pour aligner les boutons */}
|
|
167
|
-
<Button onClick={handleSaveS3Config} disabled={isSavingS3Config}
|
|
168
|
-
className="btn-primary"> {/* Classe btn-primary pour le bouton principal */}
|
|
169
|
-
{isSavingS3Config ? t('btns.saving', 'Enregistrement...') : t('btns.save', 'Enregistrer la configuration S3')}
|
|
170
|
-
</Button>
|
|
171
|
-
<Button onClick={onClose} className="btn-secondary">
|
|
172
|
-
<Trans i18nKey="btns.cancel">Annuler</Trans>
|
|
173
|
-
</Button>
|
|
174
|
-
</div>
|
|
175
|
-
</form>
|
|
176
|
-
</>)}
|
|
177
|
-
|
|
178
41
|
</div>
|
|
179
42
|
</Dialog>
|
|
180
43
|
);
|
package/client/src/constants.js
CHANGED
|
@@ -53,7 +53,7 @@ export const MONGO_OPERATORS = {
|
|
|
53
53
|
export const profiles = {
|
|
54
54
|
'personal': ['contact', 'location', 'imageGallery', 'budget', 'currency', 'taxonomy'], // budget,
|
|
55
55
|
'developer': ['alert','endpoint','request','webpage', 'content', 'taxonomy', 'resource', 'translation', 'contact', 'location', 'channel', 'lang', 'token', 'message', 'ticket', 'user', 'permission', 'role'],
|
|
56
|
-
'company': ['alert','request','location', '
|
|
56
|
+
'company': ['alert','request','location', 'order', 'currency', 'product', 'cart', 'cartItem', 'invoice', 'message', 'user', 'role', 'permission', 'token','translation', 'lang', 'webpage', 'content', 'taxonomy', 'contact', 'resource', 'accountingExercise', 'accountingLineItem', 'accountingEntry', 'employee', 'kpi', 'dashboard'],
|
|
57
57
|
'engineer': ['alert','endpoint','request','dashboard', 'kpi', 'user', 'role', 'token', 'permission', 'workflow', 'workflowRun', 'workflowStep', "channel", "message", 'workflowAction', 'workflowTrigger']
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -6,7 +6,7 @@ import { tutorialsConfig } from '../tutorials.js';
|
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
8
8
|
import useLocalStorage from "./useLocalStorage.js";
|
|
9
|
-
import {
|
|
9
|
+
import {Event} from "../../../src/events.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Hook pour gérer la logique des tutoriels multi-étapes.
|
|
@@ -205,10 +205,10 @@ export const useTutorials = () => {
|
|
|
205
205
|
|
|
206
206
|
console.log(me.activeTutorial + new Date().getMilliseconds());
|
|
207
207
|
const eventTypes = ['API_ADD_DATA', 'API_EDIT_DATA', 'API_DELETE_DATA'];
|
|
208
|
-
eventTypes.forEach(type =>
|
|
208
|
+
eventTypes.forEach(type => Event.Listen(type, handleDataChange, "custom", "data"));
|
|
209
209
|
|
|
210
210
|
return () => {
|
|
211
|
-
eventTypes.forEach(type =>
|
|
211
|
+
eventTypes.forEach(type => Event.RemoveCallback(type, handleDataChange, "custom", "data"));
|
|
212
212
|
};
|
|
213
213
|
}, [me?.activeTutorial]);
|
|
214
214
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "data-primals-engine is
|
|
3
|
+
"version": "1.2.3",
|
|
4
|
+
"description": "data-primals-engine is a package responsible from handling large amount of data using MongoDB in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation. It also has integrated AI assistant.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
package/src/constants.js
CHANGED
|
@@ -66,7 +66,7 @@ export const awsDefaultConfig = {
|
|
|
66
66
|
export const emailDefaultConfig = {
|
|
67
67
|
from: "Support - data@primals.net <data@primals.net>",
|
|
68
68
|
host: 'smtp.mydomain.tld',
|
|
69
|
-
port:
|
|
69
|
+
port: 587,
|
|
70
70
|
secure: false,
|
|
71
71
|
user: 'user',
|
|
72
72
|
pass: 'password'
|
|
@@ -288,7 +288,7 @@ metaModels['messaging'] = { load: ['alert','ticket', 'message', 'channel'], 'req
|
|
|
288
288
|
metaModels['eshopping'] = { load: [
|
|
289
289
|
'order', 'currency', 'product', 'productVariant', 'discount', 'cart', 'cartItem',
|
|
290
290
|
'brand', 'return', 'review', 'stock', 'returnItem', 'userSubscription',
|
|
291
|
-
'warehouse', 'shipment', '
|
|
291
|
+
'warehouse', 'shipment', 'stockAlert', 'invoice'],
|
|
292
292
|
'require': ['i18n', 'users', 'messaging'] };
|
|
293
293
|
metaModels['workflow'] = { load: ['env', 'workflow', 'workflowRun', 'workflowAction', 'workflowStep', 'workflowTrigger']};
|
|
294
294
|
metaModels['erp'] = { load: [ 'accountingExercise', 'accountingLineItem', 'accountingEntry', 'employee', 'dashboard', 'kpi'] };
|
package/src/data.js
CHANGED
|
@@ -5,21 +5,21 @@ import {mainFieldsTypes} from "./constants.js";
|
|
|
5
5
|
|
|
6
6
|
const IV_LENGTH = 16;
|
|
7
7
|
export function encryptValue(text) {
|
|
8
|
-
if (!process.env.
|
|
8
|
+
if (!process.env.ENCRYPTION_KEY) throw new Error("ENCRYPTION_KEY is not set");
|
|
9
9
|
let iv = crypto.randomBytes(IV_LENGTH);
|
|
10
|
-
let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(process.env.
|
|
10
|
+
let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(process.env.ENCRYPTION_KEY), iv);
|
|
11
11
|
let encrypted = cipher.update(text);
|
|
12
12
|
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
13
13
|
return iv.toString('hex') + ':' + encrypted.toString('hex');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function decryptValue(text) {
|
|
17
|
-
if (!process.env.
|
|
17
|
+
if (!process.env.ENCRYPTION_KEY) throw new Error("ENCRYPTION_KEY is not set");
|
|
18
18
|
if (!text || typeof text !== 'string' || !text.includes(':')) return text; // ou throw error
|
|
19
19
|
let textParts = text.split(':');
|
|
20
20
|
let iv = Buffer.from(textParts.shift(), 'hex');
|
|
21
21
|
let encryptedText = Buffer.from(textParts.join(':'), 'hex');
|
|
22
|
-
let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(process.env.
|
|
22
|
+
let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(process.env.ENCRYPTION_KEY), iv);
|
|
23
23
|
let decrypted = decipher.update(encryptedText);
|
|
24
24
|
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
25
25
|
return decrypted.toString();
|