data-primals-engine 1.6.3 → 1.7.0

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 (55) hide show
  1. package/README.md +52 -900
  2. package/client/index.js +3 -0
  3. package/client/package-lock.json +7563 -8811
  4. package/client/package.json +11 -1
  5. package/client/src/App.scss +29 -0
  6. package/client/src/AssistantChat.jsx +369 -362
  7. package/client/src/DataEditor.jsx +383 -383
  8. package/client/src/DataLayout.jsx +54 -20
  9. package/client/src/ModelList.jsx +280 -280
  10. package/client/src/ViewSwitcher.scss +0 -31
  11. package/client/src/_variables.scss +3 -0
  12. package/client/src/constants.js +81 -100
  13. package/client/src/contexts/CommandContext.jsx +274 -259
  14. package/client/vite.config.js +30 -30
  15. package/doc/AI-assistance.md +93 -0
  16. package/doc/Advanced-workflows.md +90 -0
  17. package/doc/Event-system.md +79 -0
  18. package/doc/Packs-gallery.md +73 -0
  19. package/doc/automation-workflows.md +102 -0
  20. package/doc/core-concepts.md +33 -0
  21. package/doc/custom-api-endpoints.md +40 -0
  22. package/doc/dashboards-kpis-charts.md +49 -0
  23. package/doc/data-management.md +120 -0
  24. package/doc/data-models.md +75 -0
  25. package/doc/index.md +14 -0
  26. package/doc/roles-permissions.md +43 -0
  27. package/doc/users.md +30 -0
  28. package/package.json +20 -10
  29. package/src/client.js +6 -4
  30. package/src/constants.js +1 -1
  31. package/src/core.js +31 -12
  32. package/src/defaultModels.js +1 -1
  33. package/src/engine.js +342 -335
  34. package/src/filter.js +72 -173
  35. package/src/migrate.js +1 -1
  36. package/src/modules/assistant/assistant.js +30 -20
  37. package/src/modules/data/data.backup.js +4 -4
  38. package/src/modules/data/data.js +8 -7
  39. package/src/modules/data/data.operations.js +186 -133
  40. package/src/modules/data/data.relations.js +3 -2
  41. package/src/modules/data/data.scheduling.js +1 -1
  42. package/src/modules/mongodb.js +17 -8
  43. package/src/modules/swagger.js +25 -5
  44. package/src/modules/user.js +108 -79
  45. package/src/modules/workflow.js +138 -3
  46. package/src/packs.js +5697 -5697
  47. package/src/profiles.js +19 -0
  48. package/swagger-en.yml +3391 -1550
  49. package/swagger-fr.yml +3385 -2896
  50. package/test/assistant.test.js +2 -2
  51. package/test/core.test.js +341 -0
  52. package/test/data.backup.integration.test.js +3 -1
  53. package/test/data.history.integration.test.js +0 -1
  54. package/test/data.integration.test.js +137 -2
  55. package/test/user.test.js +33 -29
package/README.md CHANGED
@@ -5,10 +5,12 @@
5
5
  ![](https://img.shields.io/github/v/release/anonympins/data-primals-engine)
6
6
  ![](https://img.shields.io/github/license/anonympins/data-primals-engine)
7
7
 
8
- **data-primals-engine** is a powerful and flexible **Node.js** backend framework designed to accelerate development of complex data-driven applications. Built on **Express.js** and **MongoDB**, it offers dynamic data modeling, automation workflows, advanced user management, and more.
8
+ **data-primals-engine** is a powerful and flexible **Node.js** backend framework designed to accelerate the development of complex data-driven applications. Built on **Express.js** and **MongoDB**, it provides a solid foundation so you can focus on what makes your application unique.
9
9
 
10
10
  > Whether you're building a CRM, e-commerce site, CMS, or SaaS platform, **data-primals-engine** provides the strong foundations so you can focus on what makes your application unique.
11
11
 
12
+ ---
13
+
12
14
  <p align="center">
13
15
  <a href="https://data.primals.net/prez1.jpg" target="_blank"><img alt="Light" src="https://data.primals.net/prez1.jpg" width="35%"></a>
14
16
  <a href="https://data.primals.net/prez6.jpg" target="_blank"><img alt="Light" src="https://data.primals.net/prez6.jpg" width="35%"></a>
@@ -51,20 +53,24 @@ The engine has been rigorously tested to ensure stability and scalability. Load
51
53
  - **Excellent Responsiveness**: Median response times as low as 15-30ms.
52
54
  - **Linear Scaling**: Predictable performance as user load increases.
53
55
 
54
- For detailed reports, see the [Performance Testing Documentation](./perf/README.md).
56
+ For detailed reports, see the Performance Testing Documentation.
55
57
 
56
58
  ---
57
59
 
58
60
  ## ⚙️ Requirements
59
61
 
60
62
  - Node.js ≥ 20
61
- - MongoDB (local or remote), see [installation guide](https://www.mongodb.com/docs/manual/installation/)
63
+ - MongoDB (local or remote), see installation guide
62
64
  - NPM or Yarn
63
65
 
64
66
  ---
65
67
 
66
68
  ## ⚡ Quick Start
67
69
 
70
+ ### 1. Prerequisites
71
+ - **Node.js** ≥ 20
72
+ - **MongoDB** (local or remote)
73
+
68
74
  ### check
69
75
  ```bash
70
76
  # Verify required versions
@@ -72,7 +78,7 @@ node -v # Must show ≥ v20
72
78
  mongod --version # Must be installed
73
79
  ```
74
80
 
75
- ### install
81
+ ### 2. Installation
76
82
 
77
83
  ```bash
78
84
  npm i data-primals-engine
@@ -84,926 +90,72 @@ cd data-primals-engine
84
90
  npm install
85
91
  ```
86
92
 
87
- ### configure
88
- Possibly create a `.env` file:
89
- ```env
90
- MONGO_DB_URL=mongodb://127.0.0.1:27017
91
- ```
92
- | Variable | Description | Example |
93
- |:----------------------|:------------------------------------------------------------------------|:-----------------------------------------|
94
- | MONGO_DB_URL | Connection URL for your MongoDB database. | mongodb://user:pass@host:27017/db |
95
- | PORT | Port on which the Express server will listen. | 7633 |
96
- | JWT_SECRET | Secret key for signing JWT authentication tokens. | a_long_random_secret_string |
97
- | OPENAI_API_KEY | Your optional OpenAI API key for AI features. | sk-xxxxxxxxxxxxxxxxxxxx |
98
- | GOOGLE_API_KEY | Your optional Google (Gemini) API key for AI features. | AIzaSyxxxxxxxxxxxxxxxxxxxx |
99
- | DEEPSEEK_API_KEY | Your optional DeepSeek API key for AI features. | sk-xxxxxxxxxxxxxxxxxxxx |
100
- | ANTHROPIC_API_KEY | Your optional Anthropic API key for AI features. | sk-ant-xxxxxxxxxxxxxxxxxxx |
101
- | AWS_ACCESS_KEY_ID | AWS access key for S3 storage (files, backups). Keep empty to disable | AKIAIOSFODNN7EXAMPLE |
102
- | AWS_SECRET_ACCESS_KEY | AWS secret access key. | wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY | |
103
- | AWS_REGION | Region for your S3 bucket. | eu-west-3 | |
104
- | AWS_S3_BUCKET_NAME | Name of the S3 bucket to use. | my-backup-bucket | |
105
- | SMTP_HOST | SMTP server host for sending emails. | smtp.example.com |
106
- | SMTP_PORT | SMTP server port. | 587 |
107
- | SMTP_USER | Username for SMTP authentication. | user@example.com |
108
- | SMTP_PASS | Password for SMTP authentication. | password |
109
- | TLS | Encrypted connection (TLS) mode. Disabled by default | 0/1 false/true |
110
- | CA_CERT | Path to CA cert file. | certs/ca.crt |
111
- | CERT | Path to cert file. | certs/cert.pem |
112
- | CERT_KEY | Path to the key file for your certificate. | certs/key.pem |
113
-
114
- ### Programmatic Configuration
115
-
116
- In addition to environment variables, you can programmatically override most of the application's internal constants at runtime. This is useful for fine-tuning performance, limits, or behavior without restarting the server.
117
-
118
- The engine exposes a `Config` object for this purpose. You can modify any constant defined in `src/constants.js`.
119
-
120
- To override a constant, use the `Config.Set()` method **before** initializing the engine:
121
-
122
- ```javascript
123
- import express from "express";
124
- import { Engine, Config } from 'data-primals-engine';
125
-
126
- // Override default constants before engine initialization
127
- Config.Set("maxRequestData", 1000); // Increase max data per request
128
- Config.Set("maxFileSize", 50 * 1024 * 1024); // Allow larger file uploads (50MB)
129
-
130
- const app = express();
131
- const engine = await Engine.Create({ app });
132
- ```
133
-
134
- ### Start the server
135
- ```bash
136
- # Development mode
137
- npm run devserver
138
-
139
- # Production mode
140
- npm run server
141
- ```
142
-
143
- By default, the app runs on port **7633** : http://localhost:7633
144
-
145
-
146
- ---
147
-
148
- ## 🧠 Concepts
149
-
150
- ### Authentication & Authorization
151
-
152
- The engine includes a pluggable system for user management. For development and testing, it uses a `DefaultUserProvider` which creates a volatile **`demo`** user. This is perfect for getting started quickly without setting up a user database.
153
-
154
- For production environments, you should use SSO providers as seen in the page below, or extend the base `UserProvider` class to connect to your actual user database (e.g., another MongoDB collection, a SQL database, or an external authentication service). This allows you to implement your own logic for finding and creating users.
155
-
156
- ### Permission System
157
-
158
- The engine features a powerful dual-mode permission system, providing both granular, database-driven control and a lightweight, simplified approach.
159
-
160
- #### 1. The Complete System (for Local Users)
161
-
162
- This is the full-featured Role-Based Access Control (RBAC) system designed for application users. It relies on three core models:
163
-
164
- - **`permission`**: Defines a single action (e.g., `API_EDIT_DATA_product`) and can include a JSON-based **filter** to restrict its scope (e.g., only edit products where `status` is `'draft'`).
165
- - **`role`**: A group of permissions (e.g., an "Editor" role that includes `API_EDIT_DATA_product` and `API_SEARCH_DATA_product`).
166
- - **`userPermission`**: Manages exceptions for individual users, allowing you to grant or revoke specific permissions, or even override a permission's filter for a single user.
167
-
168
- When `hasPermission()` is called for a "local user" (a standard user stored in the database), the engine performs a detailed aggregation to compute the final set of active permissions and their corresponding filters.
169
-
170
- #### 2. The Simplified System (for System/Non-Local Users)
171
-
172
- For internal processes, system users, or temporary "demo" users, the full database-driven permission system can be overkill. The engine offers an "extremely simplified" mode for these cases.
173
-
174
- **How it works:**
175
-
176
- If a user object is not a "local user" (i.e., it does not have `_model: 'user'`), the permission check becomes a simple string comparison. The `hasPermission()` function will just check if the permission name you are testing exists directly in the user's `roles` array.
177
-
178
- **Use Case:** This is perfect for system-level scripts or internal services that need specific, hard-coded permissions without the overhead of database lookups.
179
-
180
- **Example:**
181
-
182
- Consider a system user defined directly in your code:
183
-
184
- ```javascript
185
- const systemUser = {
186
- username: 'internal-service',
187
- roles: ['API_SEARCH_DATA_product', 'API_ADD_DATA_order']
188
- };
189
-
190
- await hasPermission('API_SEARCH_DATA_product', systemUser); // Returns true
191
- await hasPermission('API_DELETE_DATA_product', systemUser); // Returns false
192
- ```
193
-
194
- This dual approach gives you the flexibility to use a robust, granular system for your end-users while maintaining a simple, performant mechanism for internal and system-level tasks.
195
-
196
- ### Model generation
197
- Models are the way to handle structured data. They organize data and they can be declared in JSON.
198
-
199
- ```json
200
- {
201
- "name": "product",
202
- "description": "E-commerce product schema",
203
- "fields": [
204
- { "name": "name", "type": "string", "required": true },
205
- { "name": "price", "type": "number", "required": true },
206
- { "name": "stock", "type": "number", "default": 0 },
207
- { "name": "category", "type": "relation", "relation": "taxonomy",
208
- "relationFilter": { "$eq": ["$type", "category"] }
209
- },
210
- { "name": "tags", "type": "relation", "relation": "taxonomy", "multiple": true,
211
- "relationFilter": { "$eq": ["$type", "keyword"] }
212
- }
213
- ]
214
- }
215
- ```
216
-
217
- ### Custom fields types
218
-
219
- | Type | Description | Properties/Notes |
220
- |:------------|:------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|
221
- | string | Character string. | minLength, maxLength |
222
- | string_t | International character string ID. | same as string, translated in { key, value } |
223
- | number | Numeric value (integer or float). | min, max |
224
- | boolean | Boolean value (true/false). | – |
225
- | date | Stores a ISO date. | – |
226
- | datetime | Stores an ISO date and time. | – |
227
- | richtext | Rich text field (HTML) for WYSIWYG editors. | |
228
- | richtext_t | International rich text field (HTML) for WYSIWYG editors. | i18n |
229
- | email | String validated as an email address. | – |
230
- | password | String that will be automatically hashed. | – |
231
- | enum | Allows selecting a value from a predefined list. | items: ["value1", "value2"] |
232
- | relation | Creates a link to a document in another model. | relation: "target_model_name", multiple: true/false |
233
- | file | For uploading a file (stored on S3 if configured). | allowedTypes:['image/jpeg', 'image/png', 'image/bmp'], maxSize: 1024*1000 |
234
- | image | Specialized file type for images, with preview. | – |
235
- | array | Stores a list of values. | itemsType: 'enum' // any type except relations |
236
- | object | Stores a nested JSON object. – | |
237
- | code | Stores language="*" as string, stores language="json" as arbitrary JSON structure. | language="json" conditionBuilder=true |
238
- | color | Stores an hexadecimal value of an RGB color | '#FF0000' |
239
- | model | Stores a model by name | – |
240
- | modelField | Stores a model field path | – |
241
-
242
- ### Other model features
243
- - Handles up to 1500 direct relations per document by default (can be customized). For larger datasets, use intermediate collections or maxRelationsPerData constant.
244
- - Anonymizable fields (encrypted for API users)
245
- - **Dynamic Index Management**: The engine features a sophisticated system for managing database indexes directly from the model editor UI. This eliminates the need for manual database administration or migration scripts for indexing, significantly speeding up development and reducing errors. Indexes are created, updated, and removed automatically when you save a model's structure. All created indexes are **partial indexes**, meaning they only include documents for the specific model and user, which optimizes storage and performance.
246
-
247
- #### Available Index Types
248
-
249
- You can enable indexing on any field by checking the `index` box in the model editor and choosing one of the following types:
250
-
251
- | Index Type | `indexType` | Use Case & Behavior -|
252
- |-----------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
253
- | **Regular** | `regular` | **Purpose**: The standard index for most use cases. It dramatically speeds up queries, sorting, and filtering on a specific field. <br/> **Behavior**: Creates a standard ascending B-tree index on a single field. This is the default and most common index type. -|
254
- | **Text Search** | `text` | **Purpose**: Enables powerful, language-aware full-text search across one or more fields. It's ideal for implementing search bars that need to query descriptions, titles, and other text-heavy content. <br/> **Behavior**: Creates a single, special **compound `text` index** for the entire model. All fields marked with `indexType: 'text'` are included in this single index. This allows you to use MongoDB's `$text` operator in your search queries. -|
255
- | **Geospatial** | `2dsphere` | **Purpose**: Essential for location-based queries. It allows you to efficiently find documents within a certain radius, within a polygon, or sorted by distance. <br/> **Behavior**: Creates a `2dsphere` index on a single field. This index type should be used exclusively on fields of type `geolocation` which store data in GeoJSON format (e.g., `{ "type": "Point", "coordinates": [ -73.97, 40.77 ] }`). |
256
-
257
- ### Model constraints
258
- ```javascript
259
- {
260
- "name": "modelName",
261
- "fields": [
262
- { "name": "fieldName1", .... },
263
- { "name": "fieldName2", .... }
264
- ],
265
- "constraints": [
266
- // uniqueness
267
- { "name": "uniqueConstraint", type: "unique", keys: ["fieldName1", "fieldName2"] }
268
- ]
269
- }
270
- ```
271
-
272
- ## 🏗️ Use Case Examples
273
-
274
- ### 🛒 E-Commerce Backoffice
275
- > “Sell in 15 min, not 15 days”
276
- >
277
- **Pain**: You're struggling to sync your catalog, inventory, orders, refunds…
278
-
279
- **Promise**: Install the "E-commerce Starter Kit" to get all the models you need for products, orders, and payments, plus a pre-configured workflow to process new orders.
280
-
281
- **Proof**: After installing the pack, a single API call creates the order, which can then automatically trigger the "Order Fulfillment" workflow to update its status and create a shipment record.
282
-
283
- ```bash
284
- curl -X POST "http://localhost:7633/api/data?_user=demo" \
285
- -H "Content-Type: application/json" \
286
- -d '{
287
- "model": "order",
288
- "data": { "orderId": "ORDER-12345", "totalAmount": 99.98, "status": "pending" }
289
- }'
290
- ```
291
-
292
- ### 🎫 Support Ticket System
293
- > "Transform Gmail into Zendesk in 10 minutes"
294
-
295
- **Pain**: Tickets flood a shared inbox → delayed responses, broken SLAs.
296
-
297
- **Promise**: Create a `ticket` model (with statuses, priority, etc.). Then, build a workflow that triggers on new high-priority tickets. This workflow can use the built-in **`HttpRequest`** action to send a message to a Discord or Slack webhook URL, and the **`SendEmail`** action to alert a manager.
298
-
299
- **Proof**: A single API call to create a ticket instantly triggers the notification workflow. The following `curl` command demonstrates how to create a new high-priority ticket, which would immediately fire the pre-configured alerts.
300
-
301
- ```bash
302
- curl -X POST "http://localhost:7633/api/data?_user=demo" \
303
- -H "Content-Type: application/json" \
304
- -d '{
305
- "model": "ticket",
306
- "data": {
307
- "subject": "Critical: Payment Gateway Down",
308
- "priority": "high",
309
- "description": "No payments have been processed in the last hour."
310
- }
311
- }'
312
- ```
313
-
314
- ### 🤖 AI Chatbot
315
- > "Adds an assistant that answers instead of support."
316
-
317
- **Pain**: 70% of questions are "Where is my order?" → overloaded team.
318
-
319
- **Promise**: One line in the workflow `OnDataAdded/ticket`: `GenerateAIContent(prompt="Summarize the order {triggerData.orderId} and provide the tracking link").`
320
-
321
- **Proof**: The ticket is created, the AI ​​responds in 3 seconds, the status changes to "automatically resolved" → saves 30 hours/month of support.
322
-
323
- ### 📧 Email Campaign Management
324
- > “Send 10,000 emails without crashing your VPS”
325
-
326
- **Pain**: for loop + SMTP = blacklist + 100% CPU usage.
327
-
328
- **Promise**: Install the "Marketing & Campaigning" pack, configure your SMTP credentials in the 'env' model, create a campaign: the workflow sends emails in efficient batches of 10.
329
-
330
- **Proof**: Simply edit a campaign and set its status to "scheduled". A background workflow is immediately triggered, processing your entire mailing list in small, manageable batches. This offloads your server and allows you to continue working while the campaign runs securely in the background.
331
-
332
- ---
333
-
334
- ## 📁 Project Structure
335
- ```
336
- data-primals-engine/
337
- ├── src/
338
- │ ├── middlewares/
339
- │ ├── migrations/
340
- │ ├── modules/
341
- │ ├── workers/
342
- │ ├── engine.js // The Express engine that serves the API
343
- │ ├── constants.js // The inner-application constants definitions
344
- │ ├── packs.js // The packs that will be loaded and available with installPack() method
345
- │ ├── defaultModels.js // The default models available at startup.
346
- │ ├── ...
347
- └── server.js
348
- ```
349
-
350
- ---
351
-
352
- ## 🔌 API Examples (using `curl`)
353
-
354
- ### 📁 Model Management
355
-
356
- #### Create a model
357
- > Defines a new data model (schema) in the system. The request body must contain the model's name and an array of field definitions.
358
- ```bash
359
- curl -X POST http://localhost:7633/api/model?_user=demo \
360
- -H "Authorization: Bearer demotoken" \
361
- -H "Content-Type: application/json" \
362
- -d '{
363
- "name": "newModel",
364
- "description": "A test model.",
365
- "fields": [
366
- {"name": "title", "type": "string", "required": true},
367
- {"name": "price", "type": "number"}
368
- ]
369
- }'
370
- ```
371
-
372
- #### Update a model
373
- > Modifies an existing model's structure using its unique ID. This allows you to add, remove, or change fields.
374
- ```bash
375
- curl -X PUT http://localhost:7633/api/model/60d0fe4f5311236168a109ca?_user=demo \
376
- -H "Authorization: Bearer demotoken" \
377
- -H "Content-Type: application/json" \
378
- -d '{
379
- "name": "updatedModel",
380
- "description": "Updated description.",
381
- "fields": [
382
- {"name": "title", "type": "string", "required": true},
383
- {"name": "status", "type": "enum", "items": ["active", "inactive"]}
384
- ]
385
- }'
386
- ```
387
-
388
- #### Delete a model
389
- > Permanently deletes a model definition using its name. This action is irreversible.
390
- ```bash
391
- curl -X DELETE "http://localhost:7633/api/model?_user=demo&name=newModel" \
392
- -H "Authorization: Bearer demotoken"
393
- ```
394
-
395
- ### 🗂️ Data Management
396
-
397
- #### Create a document
398
- > Creates a single new document in the specified model. The `data` object must conform to the model's schema.
399
- ```bash
400
- curl -X POST http://localhost:7633/api/data?_user=demo \
401
- -H "Authorization: Bearer demotoken" \
402
- -H "Content-Type: application/json" \
403
- -d '{
404
- "model": "product",
405
- "data": {
406
- "name": "Widget Extraordinaire",
407
- "price": 99.99,
408
- "stock": 150,
409
- "published": true
410
- }
411
- }'
412
- ```
413
-
414
-
415
- #### Search documents
416
- > Searches for documents using a MongoDB-style filter. This endpoint is ideal for complex queries, pagination, and sorting.
417
-
418
- ```bash
419
- curl -X POST http://localhost:7633/api/data/search?_user=demo \
420
- -H "Authorization: Bearer demotoken" \
421
- -H "Content-Type: application/json" \
422
- -d '{
423
- "model": "product",
424
- "filter": { "$gt" : ["$price", 50] }
425
- }'
426
- ```
427
-
428
- #### Update a document by ID
429
- > Updates a single document by its unique ID. The request body contains the fields to be modified.
430
- ```bash
431
- curl -X PUT http://localhost:7633/api/data/64a31c123ef59d4c8d55aa99?_user=demo \
432
- -H "Authorization: Bearer demotoken" \
433
- -H "Content-Type: application/json" \
434
- -d '{
435
- "model": "product",
436
- "data": { "price": 109.99 }
437
- }'
438
- ```
439
-
440
-
441
- #### Bulk update
442
- > Updates multiple documents matching a filter. This is efficient for applying changes to a batch of records, such as restocking all out-of-stock products.
443
- ```bash
444
- curl -X PUT http://localhost:7633/api/data?_user=demo \
445
- -H "Authorization: Bearer demotoken" \
446
- -H "Content-Type: application/json" \
447
- -d '{
448
- "model": "product",
449
- "data": { "stock": 999 }
450
- }'
451
- ```
452
-
453
- #### Delete documents
454
- > Deletes one or more documents. You can provide an array of `ids` to delete specific documents.
455
- ```bash
456
- curl -X DELETE http://localhost:7633/api/data?_user=demo \
457
- -H "Authorization: Bearer demotoken" \
458
- -H "Content-Type: application/json" \
459
- -d '{
460
- "ids": ["64a31c123ef59d4c8d55aa99"]
461
- }'
462
- ```
463
-
464
- ## Other operations
465
-
466
- Make sure you use the code below to initialize the user :
467
- ```javascript
468
- import express from "express";
469
- import {Engine, insertData, searchData } from 'data-primals-engine';
470
-
471
- // Ensure the engine is initialized
472
-
473
- const app = express();
474
- const engine = await Engine.Create({ app });
475
- const currentUser = await engine.userProvider.findUserByUsername('demo');
476
- if (!currentUser) {
477
- throw new Error("Could not retrieve the user. Please check credentials or user provider.");
478
- }
479
- console.log(`Successfully authenticated as ${currentUser.username}`);
480
- ```
481
-
482
- ### insertData(modelName, data, files, user)
483
-
484
- > Inserts one or more documents, intelligently handling nested relationships.
485
- _env is used to define the target environment, production ready by default.
486
- >
487
- ```javascript
488
- // Uses the `currentUser` object defined above
489
- const newProduct = { name: 'Super Widget', price: 99.99, status: 'available', _env: 'development' };
490
- const result = await insertData('product', newProduct, {}, currentUser);
491
-
492
- if (result.success) {
493
- console.log(`Successfully inserted document with ID: ${result.insertedIds[0]}`);
494
- }
495
- ```
496
-
497
- ### editData(modelName, filter, data, files, user)
498
-
499
- > Updates existing data matching the filter.
93
+ ### 3. Configuration
500
94
 
501
- Example:
502
-
503
- ```javascript
504
- await editData(
505
- "userProfile",
506
- { _id: "507f1f77bcf86cd799439011" },
507
- { bio: "Updated bio text" },
508
- null, // No files
509
- currentUser
510
- );
511
- ```
512
-
513
- ```javascript
514
- await editData(
515
- "resource",
516
- { source: "507f1f77bcf86cd799439011" },
517
- { "file[]": {
518
- "path": "C:/Users/.../test-upload-on-insert.txt",
519
- "name": 'test-upload-on-insert.txt',
520
- "type": 'text/plain',
521
- "size": 1524,
522
- "lastModifiedDate": new Date(),
523
- }}, // No files
524
- currentUser
525
- );
526
- ```
527
-
528
- ### patchData(modelName, filter, data, files, user)
529
-
530
- > Partially updates data (only modifies specified fields).
531
-
532
- Example:
533
-
534
- ```javascript
535
-
536
- // Edit user settings (dark theme mode)
537
- await patchData(
538
- "settings",
539
- { userId: "507f1f77bcf86cd799439011" },
540
- { theme: "dark" },
541
- null,
542
- currentUser
543
- );
544
-
545
- // Transfers data to the 'development' environement
546
- await patchData(
547
- "product",
548
- { _id: "507f1f77bcf86cd799439012" },
549
- { _env: "development" },
550
- null,
551
- currentUser
552
- );
553
- ```
554
-
555
- ### deleteData(modelName, filter, user)
556
-
557
- >Deletes data with cascading relation cleanup.
558
-
559
- Examples:
560
-
561
- ```javascript
562
- // Delete by IDs
563
- await deleteData("comments", ["61d1f1a9e3f1a9e3f1a9e3f1"], user);
564
-
565
- // Delete by filter
566
- await deleteData("logs", { createdAt: { $lt: "2023-01-01" } }, user);
567
- ```
568
-
569
- ### searchData(query, user)
570
-
571
- Powerful search with relation expansion and filtering.
572
-
573
- Query Options:
574
-
575
- - **model**: Model name to search
576
- - **env**: Filter data by environment (development, staging, production, ...)
577
- - **filter**: MongoDB-style filter
578
- - **depth**: Relation expansion depth (default: 1)
579
- - **limit**: Number of results
580
- - **page**: Current page for pagination (default: 1)
581
- - **sort**: Sorting criteria
582
-
583
- Example:
584
- ```javascript
585
- const results = await searchData({
586
- model: "blogPost",
587
- filter: { status: "published" },
588
- depth: 2, // Expand author and comments
589
- limit: 10,
590
- sort: "createdAt:DESC"
591
- }, user);
592
- ```
593
-
594
- ## Import/Export
595
- ### importData(options, files, user)
596
- > Imports data from Excel / JSON / CSV files.
597
-
598
- Supported Formats:
599
-
600
- - Excel with headers or field mapping
601
- - CSV with headers or field mapping
602
- - JSON arrays
603
- - JSON with model-keyed objects
604
-
605
- Example:
606
-
607
- ```javascript
608
- const result = await importData(
609
- {
610
- model: "products",
611
- hasHeaders: true
612
- },
613
- {
614
- file: req.files.myFileField // from multipart body
615
- },
616
- currentUser
617
- );
618
- ```
619
-
620
- ### exportData(options, user)
621
- > Exports data to a structured format.
622
-
623
- Example:
624
-
625
- ```javascript
626
- await exportData(
627
- {
628
- models: ["products", "categories"],
629
- depth: 1, // Include relations
630
- lang: "fr" // Localized data
631
- },
632
- currentUser
633
- );
634
- // Returns: { success: true, data: { products: [...], categories: [...] } }
635
- ```
636
-
637
- ## Backup & Restore
638
- ### dumpUserData(user)
639
- > Creates an encrypted backup of user data.
640
-
641
- Features:
642
-
643
- - Automatic encryption
644
- - S3 or local storage
645
- - Retention policies by plan (daily/weekly/monthly)
646
-
647
- Example:
648
-
649
- ```javascript
650
- await dumpUserData(currentUser);
651
- // Backup saved to S3 or ./backups/
652
- ```
653
-
654
- ### loadFromDump(user, options)
655
-
656
- > Restores user data from backup.
657
-
658
- Options:
659
- - modelsOnly: Restore only model definitions
660
-
661
- Example:
662
-
663
- ```javascript
664
- await loadFromDump(currentUser, { modelsOnly: false });
665
- // Full restore including data
666
- ```
667
-
668
- ## Pack Management
669
-
670
- ### installPack(packId, user, lang)
671
-
672
- > Installs a data pack.
673
-
674
- Example:
675
-
676
- ```javascript
677
- const result = await installPack([
678
- {
679
- "name": "My custom pack",
680
- "description": "Markdown **description** of the pack",
681
- "tags": ["customPack", "tag1", "tag2"],
682
- "models": [
683
- "env", // default model
684
- {
685
- "name": "post",
686
- "description": "Defines a post",
687
- "fields": [
688
- {"name": "subject", "type": "string", "required": true},
689
- ]
690
- }, // or custom
691
- ],
692
- "data": {
693
- "all": { // all languages installed data
694
- "post": [
695
- {"subject": "My pack first data"}
696
- ]
697
- },
698
- "en": { // English specific installed data
699
- "post": [
700
- {"subject": "My english first post"}
701
- ]
702
- }
703
- }
704
- }
705
- ], user, "en");
706
- // Returns installation summary
707
- ```
708
-
709
- ```javascript
710
- const result = await installPack("61d1f1a9e3f1a9e3f1a9e3f1", user, "en");
711
- // Returns installation summary
712
- ```
713
-
714
- > You can also open the pack gallery to see the JSON structure of each pack, before installing them.
715
-
716
-
717
- ---
718
- ### Data Auditing & History
719
- data-primals-engine includes a built-in, automatic auditing system that creates a complete history for every record in your database.
720
-
721
- >This feature is essential for traceability, debugging, and maintaining a clear audit trail of all data manipulations.
722
-
723
- #### How It Works
724
- The history feature is non-intrusive and fully automatic.
725
-
726
- Whenever you use the standard data management functions (insertData, patchData, deleteData, etc.), the engine performs two actions:
727
- 1. It executes the requested operation (create, update, or delete) on the target document.
728
- 2. It saves a complete "snapshot" of the document's state into a dedicated history collection.
729
-
730
- >Each history entry contains the original data along with crucial metadata:
731
- - `_op`: The type of operation (i for insert, u for update, d for delete).
732
- - `_v`: The version number of the record.
733
- - `_user`: The username of the user who performed the action.
734
- - `_updatedAt`: The timestamp of the operation.
735
- - `_rid`: The ID of the original record, linking all history entries together.
736
-
737
- ### Accessing the History
738
- You can retrieve the full history for any record via a simple API endpoint:
739
- ```x-sh
740
- curl -X GET http://localhost:7633/api/data/history/:modelName/:recordId
741
- ```
742
-
743
- ---
744
-
745
- ## Workflows: Automate Your Business Logic
746
-
747
- > Workflows are the automation engine of your application.
748
-
749
- They allow you to define complex business processes that run in response to specific events, without writing custom code.
750
-
751
- This is perfect for tasks like **sending welcome emails**, managing **order fulfillment**, or triggering data synchronization.
752
-
753
- A workflow is composed of two main parts: **Triggers** and **Actions**.
754
-
755
- > A 'workflowTrigger' is the event that initiates a workflow run.
756
- - **DataAdded**: Fires when a new document is created (e.g., a new user signs up).
757
- - **DataEdited**: Fires when a document is updated (e.g., an order status changes to "shipped").
758
- - **DataDeleted**: Fires when a document is removed.
759
- - **Scheduled**: Runs at a specific time or interval using a Cron expression (e.g., 0 0 * * * for a nightly data cleanup job).
760
- - **Manual**: Triggered on-demand via an API call, allowing you to integrate workflows into any part of your application.
761
-
762
- > A 'workflowAction' is the individual steps a workflow executes. You can chain them together to create sophisticated logic.
763
- - **CreateData**: Create a new document in any model.
764
- - **UpdateData**: Modify one or more existing documents that match a filter.
765
- - **SendEmail**: Send a transactional email using dynamic data from the trigger.
766
- - **HttpRequest**: Make an HTTP request (GET, POST, etc.) to an external service or API.
767
- - **ExecuteScript**: Run a custom JavaScript snippet for complex logic, data transformation, or conditional branching.
768
- - **GenerateAIContent**: Use an integrated AI provider (like OpenAI or Gemini) to generate text, summarize content, or make decisions.
769
- - **Wait**: Pause the workflow for a specific duration before continuing to the next step
770
-
771
- See the details of the workflow models for more details.
772
-
773
- ## ⚡ Dynamic API Endpoints
774
- Beyond standard CRUD operations, data-primals-engine allows you to create your own custom API endpoints directly from the UI. This feature acts like a built-in serverless function environment, enabling you to write custom business logic, integrate with third-party services, or create complex data aggregations on the fly.
775
-
776
- Your code is executed in a secure, isolated sandbox, with access to the core data functions and the incoming request context.
777
-
778
- ### How It Works
779
- Define an Endpoint: You create a new document in the endpoint model.
780
- Write Your Logic: In the code field, you write the JavaScript that will be executed.
781
- Activate: The engine automatically listens for requests on /api/actions/:path that match your endpoint's definition.
782
-
783
- ### The endpoint Model
784
- To create a custom endpoint, you need to define a document with the following structure:
785
- ```json
786
- {
787
- "name": "GetContactPostCount",
788
- "path": "postCount/:name",
789
- "method": "GET",
790
- "code": "const posts = await db.find('content', { author: { $find: { $eq: ['$lastName', request.params.name]}}}); return { postCount: posts.length };",
791
- "isActive": true,
792
- "isPublic": true
793
- }
794
- ```
795
- | Field | Type | Description |
796
- |:---------|:--------|:---------------------------------------------------------------------|
797
- | name | string | A descriptive name for your endpoint (e.g., "Calculate User Stats"). |
798
- | path | string | The URL path. It can include parameters like :id. |
799
- | method | enum | The HTTP method: GET, POST, PUT, PATCH, or DELETE. |
800
- | code | code | The JavaScript code to execute when the endpoint is called. |
801
- | isActive | boolean | A flag to enable or disable the endpoint without deleting it. |
802
- | isPublic | boolean | A flag to enable public access (private by default). |
803
- ---
804
-
805
- ### The Execution Context
806
- Your JavaScript code runs in an async context and has access to several global objects that are securely injected into the sandbox:
807
- Your current user is used to make the calls.
808
-
809
- #### The context Object
810
- > This object contains all the information about the incoming HTTP request.
811
- - context.request.**body**: The parsed request body (for POST, PUT, PATCH).
812
- - context.request.**query**: The URL query parameters as an object.
813
- - context.request.**params**: The URL path parameters (e.g., username from /user-summary/:username).
814
- - context.request.**headers**: The incoming request headers.
815
-
816
- #### The db Object
817
- > A secure API to interact with the database. All methods are async and must be awaited. They automatically operate within the authenticated user's permissions.
818
- - await db.**create**(modelName, dataObject): Inserts a new document.
819
- - await db.**find**(modelName, filter): Finds multiple documents. Returns an array.
820
- - await db.**findOne**(modelName, filter): Finds a single document. Returns an object or null.
821
- - await db.**update**(modelName, filter, updateObject): Partially updates documents matching the filter (similar to a PATCH).
822
- - await db.**delete**(modelName, filter): Deletes documents matching the filter.
823
-
824
- #### The logger Object
825
- > A safe way to log messages from your script. These logs will be collected and can be returned in the API response if an error occurs, which is very useful for debugging.
826
- - logger.**info**(...args)
827
- - logger.**warn**(...args)
828
- - logger.**error**(...args)
829
-
830
- #### The env Object
831
- > Provides access to the user-defined variables stored in the env model, not the server's process.env.
832
- - await env.**get**(variableName): Retrieves a single environment variable's value.
833
- - await env.**getAll**(): Retrieves all user environment variables as an object.
834
-
835
- #### Example: Creating a User Summary Endpoint
836
- Let's create an endpoint that fetches a user's profile and counts how many posts they have published.
837
- 1. Create the Endpoint Document
838
- Create a new document in the endpoint model with the following data:
839
- ```json
840
- {
841
- "name": "Get User Summary",
842
- "path": "user-summary/:username",
843
- "method": "GET",
844
- "isActive": true,
845
- "code": "const { username } = context.request.params;\n\n if (!username) {\n logger.error('Username parameter is required.');\n return { success: false, error: 'Username is required.' };\n }\n\n logger.info(`Fetching summary for user: ${username}`);\n\n // Fetch the user profile using the sandboxed db API\n const userProfile = await db.findOne('userProfile', { username: username });\n\n if (!userProfile) {\n return { success: false, error: 'User not found' };\n }\n\n // Count the user's published posts\n const posts = await db.find('post', { \n authorId: userProfile._id.toString(), \n status: 'published' \n });\n\n return {\n profile: userProfile,\n publishedPosts: posts.length\n };\n});",
846
- }
847
- ```
848
- 2. Call the New Endpoint
849
- You can now call this custom endpoint like any other API route:
850
-
851
- Expected response :
852
- ```json
853
- {
854
- "profile": {
855
- "_id": "60d0fe4f5311236168a109ca",
856
- "username": "demo",
857
- "bio": "A demo user profile.",
858
- "...": "..."
859
- },
860
- "publishedPosts": 15
861
- }
95
+ Create a `.env` file in the project root to configure the connection to your database :
862
96
 
97
+ ```env
98
+ MONGO_DB_URL=mongodb://127.0.0.1:27017/my_database
99
+ JWT_SECRET=a_long_and_random_secret
863
100
  ```
864
101
 
865
- ### Pluggable SSO Authentication (Google, SAML, Azure AD)
866
-
867
- `data-primals-engine` features a powerful and modular Single Sign-On (SSO) system built on Passport.js. This allows you to easily integrate with various enterprise and social identity providers. The core engine remains lightweight, and you only enable the providers you need.
868
-
869
- #### How It Works
102
+ To discover all available environment variables (AWS, SMTP, AI, etc.), consult the **configuration documentation**.
870
103
 
871
- The engine provides a central `Sso` component that manages authentication logic. Each provider (Google, SAML, etc.) is an independent module that "plugs into" this central component.
104
+ ### 4. Launch
872
105
 
873
- To enable an SSO provider, follow these three steps:
874
-
875
- #### Example: Enabling Google Sign-In
876
-
877
- **1. Install the Provider's Library**
878
-
879
- The required Passport strategy is a `peerDependency`. You must install it in your project:
880
106
  ```bash
881
- npm install passport-google-oauth20
882
- ```
883
-
884
- **2. Enable the Module**
885
-
886
- To enable the module in your next engine initilization, just add it to the `modules` array:
887
- ```javascript
888
- Config.Set("modules", ["auth-google", "..."]);
889
- ```
890
-
891
- **3. Set Environment Variables**
107
+ # Development mode with automatic reloading
108
+ npm run devserver
892
109
 
893
- Create or update your `.env` file with the credentials obtained from the Google Cloud Console:
894
- ```env
895
- GOOGLE_CLIENT_ID=your-google-client-id
896
- GOOGLE_CLIENT_SECRET=your-google-client-secret
110
+ # Production mode
111
+ npm run server
897
112
  ```
898
113
 
899
- The login flow will now be available at `/api/auth/google`.
900
-
901
- #### Other Providers
902
-
903
- The process is identical for other supported providers:
904
-
905
- - **SAML**:
906
- 1. `npm install passport-saml-encrypted`
907
- 2. Enable the `auth-saml` module.
908
- 3. Set `SAML_ENTRY_POINT`, `SAML_ISSUER`, `SAML_CERT`, and `SAML_DECRYPTION_KEY` environment variables.
909
-
910
- - **Microsoft Azure AD**:
911
- 1. `npm install passport-azure-ad`
912
- 2. Enable the `auth-microsoft` module.
913
- 3. Set `MICROSOFT_CLIENT_ID`, `MICROSOFT_CLIENT_SECRET`, and `MICROSOFT_TENANT_ID` environment variables.
914
-
915
- This modular approach makes it simple to extend the engine with any Passport-compatible strategy to meet your authentication needs.
916
-
917
- See implementations in the `modules` directory.
114
+ By default, the application is available at `http://localhost:7633`.
918
115
 
919
116
  ---
920
- ## Extensibility
921
117
 
922
- ### Events (Triggers) Table
923
- > You can use the events below to access the engine and manipulate API responses.
924
- > It is useful for custom modules or middlewares for your application.
925
118
 
926
- Just use
119
+ ## 🧭 Explore the Platform
120
+ Discover the core features to get started building and managing your data.
121
+ Link to the official documentation: https://data.primals.net/en/documentation/
927
122
 
928
- ```javascript
929
- Event.Listen("OnDataAdded", (data) => {
930
- my_callback()
931
- }, "event", "user");
932
- ```
123
+ - 🧠 [Core Concepts](https://github.com/anonympins/data-primals-engine/wiki/Concepts): Explore the fundamentals of data modeling
124
+ - 🔌 [Custom API Endpoints](https://github.com/anonympins/data-primals-engine/wiki/Custom-Endpoints): Create dynamic HTTP routes directly from the backend
125
+ - 🏗️ [Data Models](https://data.primals.net/en/documentation/create-models): Structure your information.
126
+ - 🗃️ [Data Management](https://data.primals.net/en/documentation/manage-data): Create, read, update, and delete your entries.
127
+ - 📊 [Views (Table, Kanban)](Views): Visualize your data in different ways. *(Page to be created)*
128
+ - 📈 [Dashboards, KPIs, and Charts](https://data.primals.net/en/documentation/dashboards): Track your key metrics.
129
+ - 👥 [Users](https://data.primals.net/en/documentation/users): Manage access to the platform.
130
+ - 🔐 [Roles and Permissions](https://data.primals.net/en/documentation/roles-permissions): Define who can see and do what.
131
+ - ⚙️ [Automation with Workflows](https://data.primals.net/en/documentation/automation-workflows): Create automated processes.
933
132
 
934
- or the system version
935
- ```javascript
936
- Event.Listen("OnDataAdded", (engine, data) => {
937
- my_callback()
938
- }, "event", "system");
939
- ```
940
-
941
- | Event | Description | Scope | Triggered by | Arguments (Payload) |
942
- |:-----------------|:------------------------------------------------------------------------|:-------|:-------------------------|:---------------------------------------------------------------------|
943
- | OnServerStart | Triggered once the HTTP server is started and listening. | System | engine.start() | engine |
944
- | OnServerStop | Triggered right after the HTTP server is stopped. | System | engine.stop() | engine |
945
- | OnModelsLoaded | Triggered after the initial models are loaded and validated at startup. | System | setupInitialModels() | engine, dbModels |
946
- | OnModelsDeleted | Triggered after all models are deleted via the reset function. | System | engine.resetModels() | engine |
947
- | OnUserDataDumped | Triggered after a user's data has been backed up (dumped). | System | jobDumpUserData() | engine |
948
- | OnDataRestored | Triggered after a user's data has been restored from a backup. | System | loadFromDump() | (none) |
949
- | OnPackInstalled | Triggered after a data pack has been successfully installed. | System | installPack() | pack |
950
- | OnModelEdited | Triggered after a model definition has been modified. | System | editModel() | newModel |
951
- | OnDataAdded | Triggered after new data has been inserted. | System | insertData() | engine, insertedDocs |
952
- | OnDataEdited | Triggered after data has been edited. | System | editData() / patchData() | engine, {modelName, before, after} |
953
- | OnDataDeleted | Triggered just after data is actually deleted. | System | deleteData() | engine, {model, filter} |
954
- | OnDataSearched | Triggered after a data search. | System | searchData() | engine, {data, count} |
955
- | OnDataExported | Triggered after a data export. | User | exportData() | exportResults, modelsToExport |
956
- | OnDataInsert | Triggered just before data insertion. It will use the overrided data. | System | internal | data |
957
- | OnDataValidate | Triggered to override validation check. | System | internal | value, field, data |
958
- | OnDataFilter | Triggered to override data filtering operation. | System | internal | filteredValue, field, data |
959
- | OnEmailTemplate | Triggered to override custom email templates | System | internal | templateData, lang |
960
- | OnSystemPrompt | Triggered to override assistant system prompt | User | handleChatRequest | user |
961
- | OnChatAction | Triggered when an action is created by the AI | User | handleChatRequest | action, params, parsedResponse, command, llmOptions, user, reqParams |
962
-
963
- ### Triggering events
964
-
965
- If you want to provide your own hooks, you can call :
966
- ```javascript
967
- const result = await Event.Trigger("OnMyCustomEvent", "event", "user", ...args);
968
- ```
969
- Results are merged together if multiple events are triggered.
970
- - strings are concatenated
971
- - numbers are added
972
- - booleans are ANDed
973
- - arrays are concatenated
974
- - objects are merged using spread operator
975
-
976
- ---
977
-
978
- ## ⚛️ Frontend Integration (React)
133
+ ## 🔌 Integrate with your tools
134
+ Connect the platform to your external applications and services via our API.
979
135
 
980
- This engine is designed to work seamlessly with its dedicated client library, **`data-primals-engine/client`**. This library provides a complete set of React hooks and UI components to build a rich, data-centric user interface on top of the backend.
136
+ - 📡 [API Requests](https://data.primals.net/en/documentation/api): The basics for interacting with the REST API.
137
+ - 🚀 [Advanced Requests](https://data.primals.net/en/documentation/advanced-requests): Filters, sorts, projections, and more.
138
+ - 📦 [Bulk Operations](https://data.primals.net/en/documentation/bulk-operations): Perform operations on large volumes of data.
981
139
 
982
- While this README focuses on the backend engine and its API, you can find detailed instructions on how to integrate the client part in your React application here:
140
+ ## 🧩 Extend Capabilities
141
+ Go further by developing your own features and business logic.
983
142
 
984
- ➡️ **[View the Frontend Integration Guide](https://github.com/anonympins/data-primals-engine/tree/develop/client)**
143
+ - [Event System](Event-system): React to events in real time. *(Page to create)*
144
+ - 🧠 [Advanced Workflows](Advanced-workflows): Create complex logic with custom scripts. *(Page to create)*
145
+ - 📦 [Modules](Modules): Package and share your developments. *(Page to create)*
146
+ - ❤️ [Contribute to the project (CONTRIBUTING.md)](https://github.com/anonympins/data-primals-engine/blob/main/CONTRIBUTING.md): Join the developer community.
985
147
 
148
+ ## 💻 For Developers
149
+ Essential tools for a seamless development experience.
986
150
 
987
- ---
988
-
989
- ## 🤝 Contributing
990
-
991
- 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)
151
+ - 📖 [Explore the API with Swagger](https://data.primals.net/api-docs/): Test API access points directly from your browser.
152
+ - 📮 [Postman Collection](https://data.primals.net/doc/API.postman_collection.json): Import our collection to start querying in just a few clicks.
992
153
 
993
- 1. Fork the repo
994
- 2. Create your feature branch: `git checkout -b feature/your-feature`
995
- 3. Launch ```npm run dev``` and make your changes with hot-reload on local port
996
- 3. Commit changes: `git commit -m "Add new feature"`
997
- 4. Push to your branch: `git push origin feature/your-feature`
998
- 5. Open a pull request
154
+ ## 🤝 Contribution
999
155
 
1000
- Star the repo if you find it useful!
156
+ Contributions are welcome! Check out the open issues to get started.
1001
157
 
1002
158
  ---
1003
159
 
1004
160
  ## 📄 License
1005
- Distributed under the **MIT License**. See `LICENSE` file.
1006
-
1007
- ---
1008
-
1009
- ## [🔼](#data-primals-engine) Back to Top
161
+ Distributed under the **MIT License**. See the `LICENSE` file.