data-primals-engine 1.7.1 → 1.7.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.
Files changed (99) hide show
  1. package/README.md +9 -9
  2. package/client/package-lock.json +8430 -8121
  3. package/client/package.json +7 -4
  4. package/client/src/APIInfo.jsx +1 -1
  5. package/client/src/App.jsx +2 -1
  6. package/client/src/App.scss +1635 -1626
  7. package/client/src/AssistantChat.jsx +2 -3
  8. package/client/src/CalendarView.jsx +1 -0
  9. package/client/src/ContentView.jsx +3 -3
  10. package/client/src/Dashboard.jsx +5 -2
  11. package/client/src/DashboardChart.jsx +1 -0
  12. package/client/src/DashboardFlexViewItem.jsx +1 -0
  13. package/client/src/DashboardHtmlViewItem.jsx +1 -0
  14. package/client/src/DashboardView.jsx +2 -0
  15. package/client/src/DataEditor.jsx +0 -1
  16. package/client/src/DataImporter.jsx +489 -468
  17. package/client/src/DataLayout.jsx +25 -23
  18. package/client/src/DataTable.jsx +6 -5
  19. package/client/src/Dialog.jsx +92 -90
  20. package/client/src/Dialog.scss +122 -116
  21. package/client/src/DisplayFlexNodeRenderer.jsx +1 -0
  22. package/client/src/DocumentationPageLayout.scss +1 -1
  23. package/client/src/FlexBuilderControls.jsx +1 -0
  24. package/client/src/FlexBuilderPreview.jsx +1 -1
  25. package/client/src/FlexNode.jsx +1 -1
  26. package/client/src/HistoryDialog.jsx +3 -2
  27. package/client/src/KPIWidget.jsx +1 -1
  28. package/client/src/KanbanView.jsx +1 -0
  29. package/client/src/ModelCreator.jsx +4 -0
  30. package/client/src/ModelList.jsx +1 -0
  31. package/client/src/PackGallery.jsx +5 -4
  32. package/client/src/RTETrans.jsx +1 -1
  33. package/client/src/RelationField.jsx +2 -0
  34. package/client/src/RelationSelectorWidget.jsx +2 -0
  35. package/client/src/RelationValue.jsx +1 -0
  36. package/client/src/RestoreDialog.jsx +1 -0
  37. package/client/src/ViewSwitcher.jsx +1 -1
  38. package/client/src/WorkflowEditor.jsx +3 -0
  39. package/client/src/contexts/CommandContext.jsx +2 -1
  40. package/client/src/contexts/ModelContext.jsx +3 -3
  41. package/client/src/hooks/data.js +1 -0
  42. package/client/src/hooks/useValidation.js +1 -0
  43. package/client/src/translations.js +24 -24
  44. package/client/vite.config.js +31 -30
  45. package/doc/AI-assistance.md +87 -63
  46. package/doc/Concepts.md +122 -0
  47. package/doc/Custom-Endpoints.md +31 -0
  48. package/doc/Event-system.md +13 -14
  49. package/doc/Home.md +33 -0
  50. package/doc/Modules.md +83 -0
  51. package/doc/Packs-gallery.md +8 -23
  52. package/doc/Workflows.md +32 -0
  53. package/doc/automation-workflows.md +141 -102
  54. package/doc/dashboards-kpis-charts.md +47 -49
  55. package/doc/data-management.md +126 -120
  56. package/doc/data-models.md +68 -75
  57. package/doc/roles-permissions.md +144 -43
  58. package/doc/sharding-replication.md +158 -0
  59. package/doc/users.md +54 -30
  60. package/package.json +27 -17
  61. package/server.js +37 -37
  62. package/src/ai.jobs.js +135 -0
  63. package/src/constants.js +560 -545
  64. package/src/data.js +521 -518
  65. package/src/email.js +157 -154
  66. package/src/engine.js +167 -49
  67. package/src/gameObject.js +6 -0
  68. package/src/i18n.js +0 -1
  69. package/src/modules/assistant/assistant.js +782 -763
  70. package/src/modules/assistant/constants.js +23 -16
  71. package/src/modules/assistant/providers.js +77 -37
  72. package/src/modules/auth-google/index.js +53 -50
  73. package/src/modules/bucket.js +346 -335
  74. package/src/modules/data/data.backup.js +400 -376
  75. package/src/modules/data/data.cluster.js +559 -0
  76. package/src/modules/data/data.core.js +11 -8
  77. package/src/modules/data/data.js +311 -311
  78. package/src/modules/data/data.operations.js +3666 -3555
  79. package/src/modules/data/data.relations.js +1 -0
  80. package/src/modules/data/data.replication.js +125 -0
  81. package/src/modules/data/data.routes.js +2206 -1879
  82. package/src/modules/data/data.scheduling.js +2 -1
  83. package/src/modules/file.js +248 -247
  84. package/src/modules/mongodb.js +76 -73
  85. package/src/modules/user.js +18 -2
  86. package/src/modules/worker-script-runner.js +97 -0
  87. package/src/modules/workflow.js +177 -52
  88. package/src/packs.js +5701 -5701
  89. package/src/providers.js +298 -297
  90. package/src/sso.js +91 -25
  91. package/test/assistant.test.js +207 -206
  92. package/test/cluster.test.js +221 -0
  93. package/test/core.test.js +0 -2
  94. package/test/data.integration.test.js +1425 -1416
  95. package/test/import_export.integration.test.js +210 -210
  96. package/test/replication.test.js +163 -0
  97. package/test/workflow.actions.integration.test.js +487 -475
  98. package/test/workflow.integration.test.js +332 -329
  99. package/doc/core-concepts.md +0 -33
package/src/email.js CHANGED
@@ -1,155 +1,158 @@
1
- import process from "node:process";
2
- import nodemailer from "nodemailer";
3
- import juice from "juice";
4
- import {Event} from "./events.js";
5
- import {emailDefaultConfig} from "./constants.js";
6
- import {Config} from "./config.js";
7
-
8
- // Le transporteur par défaut, utilisé si aucune config spécifique n'est fournie.
9
- const defaultTransporter = nodemailer.createTransport({
10
- host: process.env.SMTP_HOST,
11
- port: process.env.SMTP_PORT || 587,
12
- secure: false,
13
- auth: {
14
- user: process.env.SMTP_USER,
15
- pass: process.env.SMTP_PASS
16
- }
17
- });
18
-
19
- /**
20
- * Crn * @param {object} smtpConfig - L'objet de configuration SMTP.
21
- * @returns {import('nodemailer').Transporter} Un transporteur Nodemailer.
22
- */
23
- const createTransporter = (smtpConfig) => {
24
- // Valider que les informations de base sont présentes
25
- if (!smtpConfig.host || !smtpConfig.port || !smtpConfig.user || !smtpConfig.pass){
26
- throw new Error("SMTP configuration incomplete ('SMTP_HOST', 'SMTP_PORT','SMTP_USER', and 'SMTP_PASS' keys need to be completed in env').");
27
- }
28
- return nodemailer.createTransport({
29
- host: smtpConfig.host,
30
- port: parseInt(smtpConfig.port, 10),
31
- secure: smtpConfig.secure === true, // Par défaut à false si non défini
32
- auth: {
33
- user: smtpConfig.user,
34
- pass: smtpConfig.pass
35
- }
36
- });
37
- };
38
-
39
-
40
- /**
41
- * Envoie un email en utilisant une configuration SMTP spécifique ou celle par défaut.
42
- * @param {string|string[]} email - Le ou les destinataires.
43
- * @param {object} data - Les données de l'email (title, content).
44
- * @param {object|null} smtpConfig - La configuration SMTP à utiliser. Si null, utilise la configuration par défaut.
45
- * @param {string} lang - La langue pour le template.
46
- * @param {string|null} tpl - Le template HTML à utiliser.
47
- */
48
- export const sendEmail = async (email = "", data, smtpConfig = null, lang, tpl = null) => {
49
-
50
- const cfg = Config.Get('emailDefaultConfig', emailDefaultConfig);
51
- const contactEmail = smtpConfig ? (smtpConfig.from || cfg.from) :"Our company <noreply@ourdomain.tld>";
52
- const emails = Array.isArray(email) ? email : [email];
53
- if (emails.length === 0) return false;
54
-
55
- // Choisir le transporteur à utiliser
56
- const transporter = smtpConfig ? createTransporter(smtpConfig||cfg) : defaultTransporter;
57
-
58
- if (tpl === null) tpl = await Event.Trigger("OnEmailTemplate", "event", "system", data, lang);
59
- let html = tpl;
60
- try {
61
- html = juice(tpl);
62
- } catch (e) {
63
- console.error("Erreur lors de l'inlining du CSS de l'email :", e);
64
- }
65
-
66
- // Utiliser Promise.all pour gérer les envois de manière asynchrone
67
- const sendPromises = emails.map((e) => {
68
- const mailOptions = {
69
- from: contactEmail,
70
- to: e,
71
- subject: data.title,
72
- html
73
- };
74
- return transporter.sendMail(mailOptions);
75
- });
76
-
77
- try {
78
- await Promise.all(sendPromises);
79
- console.log(`Email(s) sent successfully to : ${emails.join(', ')}`);
80
- return true;
81
- } catch (error) {
82
- console.error("Error when sending to one ore more emails :", error);
83
- // Vous pouvez relancer l'erreur si vous voulez que l'appelant la gère
84
- throw error;
85
- }
86
- };
87
-
88
- export const etag = (name, attrs, content = "") => {
89
- return (
90
- "<" +
91
- name +
92
- " " +
93
- Object.keys(attrs)
94
- .map((m) => m + '="' + attrs[m] + '" ')
95
- .join("")
96
- .trim() +
97
- ">" +
98
- content +
99
- "</" +
100
- name +
101
- ">"
102
- );
103
- };
104
-
105
- export const ebutton = (content = "", attrs = {}, style = {}) => {
106
- return etable(
107
- erow(
108
- '<a href="' +
109
- attrs["href"] +
110
- '" style="' +
111
- estyle(style) +
112
- '">' +
113
- content +
114
- "</a>"
115
- )
116
- );
117
- };
118
-
119
- export const etable = (content = "", style = {}) => {
120
- return (
121
- '<table border="0" cellPadding="0" cellSpacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; ' +
122
- (style.width === "100%" ? "min-width: 100%;" : "") +
123
- " !important; " +
124
- estyle(style) +
125
- '" width="' +
126
- (style.width || "100%") +
127
- '">' +
128
- content +
129
- "</table>"
130
- );
131
- };
132
-
133
- export const erow = (content = "", style = {}) => {
134
- const contents = !Array.isArray(content) ? [content] : content;
135
- return (
136
- "<tr>" +
137
- contents
138
- .map(
139
- (c) =>
140
- '<td align="center" style="' +
141
- estyle(style) +
142
- '" valign="top">' +
143
- c +
144
- "</td>"
145
- )
146
- .join("") +
147
- "</tr>"
148
- );
149
- };
150
-
151
- export const estyle = (options) => {
152
- return Object.keys(options)
153
- .map((m) => m + ": " + options[m] + ";")
154
- .join("");
1
+ import nodemailer from "nodemailer";
2
+ import juice from "juice";
3
+ import {Event} from "./events.js";
4
+ import {emailDefaultConfig} from "./constants.js";
5
+ import {Config} from "./config.js";
6
+ import i18n from "./i18n.js";
7
+ import {websiteTranslations} from "../client/src/translations.js";
8
+ import i18next from "i18next";
9
+
10
+ /**
11
+ * Crn * @param {object} smtpConfig - L'objet de configuration SMTP.
12
+ * @returns {import('nodemailer').Transporter} Un transporteur Nodemailer.
13
+ */
14
+ const createTransporter = (smtpConfig) => {
15
+ // Valider que les informations de base sont présentes
16
+ if (!smtpConfig.host || !smtpConfig.port || !smtpConfig.user || !smtpConfig.pass){
17
+ throw new Error("SMTP configuration incomplete ('SMTP_HOST', 'SMTP_PORT','SMTP_USER', and 'SMTP_PASS' keys need to be completed in env').");
18
+ }
19
+ return nodemailer.createTransport({
20
+ host: smtpConfig.host,
21
+ port: parseInt(smtpConfig.port, 10),
22
+ secure: smtpConfig.secure === true, // Par défaut à false si non défini
23
+ auth: {
24
+ user: smtpConfig.user,
25
+ pass: smtpConfig.pass
26
+ }
27
+ });
28
+ };
29
+
30
+
31
+
32
+ export const changeLanguage = async (lang) => {
33
+ // --- NOUVELLE LOGIQUE DE GESTION DE LANGUE ---
34
+ await i18n.changeLanguage(lang, (err) => {
35
+ if (err) return console.error('something went wrong loading', err);
36
+ const trs = {...websiteTranslations[lang]?.['translation']};
37
+ i18next.addResourceBundle(lang, 'translation', trs, true);
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Envoie un email en utilisant une configuration SMTP spécifique ou celle par défaut.
43
+ * @param {string|string[]} email - Le ou les destinataires.
44
+ * @param {object} data - Les données de l'email (title, content).
45
+ * @param {object|null} smtpConfig - La configuration SMTP à utiliser. Si null, utilise la configuration par défaut.
46
+ * @param {string} lang - La langue pour le template.
47
+ * @param {string|null} tpl - Le template HTML à utiliser.
48
+ */
49
+ export const sendEmail = async (email = "", data, smtpConfig = null, lang, tpl = null) => {
50
+
51
+ const cfg = Config.Get('emailDefaultConfig', emailDefaultConfig);
52
+ const contactEmail = smtpConfig ? (smtpConfig.from || cfg.from) :"Our company <noreply@ourdomain.tld>";
53
+ const emails = Array.isArray(email) ? email : [email];
54
+ if (emails.length === 0) return false;
55
+
56
+ // Choisir le transporteur à utiliser
57
+ const transporter = createTransporter(smtpConfig || cfg); // Always create transporter dynamically
58
+
59
+ if (tpl === null) tpl = await Event.Trigger("OnEmailTemplate", "event", "system", data, lang);
60
+ let html = tpl;
61
+ try {
62
+ html = juice(tpl);
63
+ } catch (e) {
64
+ console.error("Erreur lors de l'inlining du CSS de l'email :", e);
65
+ }
66
+
67
+ // Utiliser Promise.all pour gérer les envois de manière asynchrone
68
+ const sendPromises = emails.map((e) => {
69
+ const mailOptions = {
70
+ from: contactEmail,
71
+ to: e,
72
+ subject: data.title,
73
+ html
74
+ };
75
+ return transporter.sendMail(mailOptions);
76
+ });
77
+
78
+ try {
79
+ await Promise.all(sendPromises);
80
+ console.log(`Email(s) sent successfully to : ${emails.join(', ')}`);
81
+ return true;
82
+ } catch (error) {
83
+ console.error("Error when sending to one ore more emails :", error);
84
+ // Vous pouvez relancer l'erreur si vous voulez que l'appelant la gère
85
+ throw error;
86
+ } finally {
87
+
88
+ }
89
+ };
90
+
91
+ export const etag = (name, attrs, content = "") => {
92
+ return (
93
+ "<" +
94
+ name +
95
+ " " +
96
+ Object.keys(attrs)
97
+ .map((m) => m + '="' + attrs[m] + '" ')
98
+ .join("")
99
+ .trim() +
100
+ ">" +
101
+ content +
102
+ "</" +
103
+ name +
104
+ ">"
105
+ );
106
+ };
107
+
108
+ export const ebutton = (content = "", attrs = {}, style = {}) => {
109
+ return etable(
110
+ erow(
111
+ '<a href="' +
112
+ attrs["href"] +
113
+ '" style="' +
114
+ estyle(style) +
115
+ '">' +
116
+ content +
117
+ "</a>"
118
+ )
119
+ );
120
+ };
121
+
122
+ export const etable = (content = "", style = {}) => {
123
+ return (
124
+ '<table border="0" cellPadding="0" cellSpacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; ' +
125
+ (style.width === "100%" ? "min-width: 100%;" : "") +
126
+ " !important; " +
127
+ estyle(style) +
128
+ '" width="' +
129
+ (style.width || "100%") +
130
+ '">' +
131
+ content +
132
+ "</table>"
133
+ );
134
+ };
135
+
136
+ export const erow = (content = "", style = {}) => {
137
+ const contents = !Array.isArray(content) ? [content] : content;
138
+ return (
139
+ "<tr>" +
140
+ contents
141
+ .map(
142
+ (c) =>
143
+ '<td align="center" style="' +
144
+ estyle(style) +
145
+ '" valign="top">' +
146
+ c +
147
+ "</td>"
148
+ )
149
+ .join("") +
150
+ "</tr>"
151
+ );
152
+ };
153
+
154
+ export const estyle = (options) => {
155
+ return Object.keys(options)
156
+ .map((m) => m + ": " + options[m] + ";")
157
+ .join("");
155
158
  };
package/src/engine.js CHANGED
@@ -1,9 +1,18 @@
1
+ import path from "node:path";
2
+ import {fileURLToPath} from 'node:url';
3
+ import process from "process";
4
+ // Charger les variables d'environnement depuis le fichier .env
5
+ import dotenv from "dotenv";
6
+
7
+ // Charger le .env depuis la racine du projet appelant, et non depuis la lib.
8
+ dotenv.config({ path: path.resolve(process.cwd(), '.env') });
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
1
11
  import {GameObject, Logger} from "./gameObject.js";
2
12
  import {Config} from "./config.js";
3
13
  import fs from 'node:fs'
4
14
  import express from 'express'
5
15
  import {MongoClient as InternalMongoClient} from 'mongodb'
6
- import process from "process";
7
16
  import {
8
17
  cookiesSecret,
9
18
  databasePoolSize,
@@ -19,19 +28,13 @@ import formidableMiddleware from 'express-formidable';
19
28
  import sirv from "sirv";
20
29
  import * as tls from "node:tls";
21
30
  import {Event} from "./events.js";
22
- import path from "node:path";
23
- import { fileURLToPath, pathToFileURL } from 'node:url';
31
+ import { pathToFileURL } from 'node:url';
24
32
  import {validateModelStructure} from "./modules/data/data.validation.js";
25
33
  import { setSafeRegex } from "./filter.js";
26
34
  import safeRegexCallback from "safe-regex";
27
35
  import {createModel, deleteModels, getModels, installAllPacks} from "./modules/data/data.operations.js";
28
36
  // Constants
29
37
 
30
- // On définit __dirname pour obtenir le chemin absolu du répertoire courant,
31
- // ce qui est la méthode standard en ES Modules.
32
- const __filename = fileURLToPath(import.meta.url);
33
- const __dirname = path.dirname(__filename);
34
-
35
38
  let dbName = Config.Get('dbName', dbNameBase);
36
39
  let caFile, certFile, keyFile;
37
40
  try {
@@ -53,49 +56,54 @@ const secureContext = tls.createSecureContext({
53
56
 
54
57
  export const dbUrl = process.env.CI ? 'mongodb://mongodb:27017' : (process.env.MONGO_DB_URL || 'mongodb://127.0.0.1:27017');
55
58
 
56
- const isTlsActive = !(!process.env.TLS || ["0", "false"].includes(process.env.TLS.toLowerCase()));
57
-
58
- const clientOptions = {
59
- maxPoolSize: databasePoolSize
60
- };
61
-
62
- // We add TLS options if enabled
63
- if (isTlsActive) {
64
- clientOptions.tls = true;
65
-
66
- // is mTLS ? (client certificate required instead of password)
67
- if (process.env.CERT) {
68
- clientOptions.secureContext = tls.createSecureContext({
69
- ca: fs.readFileSync(process.env.CA_CERT),
70
- cert: fs.readFileSync(process.env.CERT),
71
- key: fs.readFileSync(process.env.CERT_KEY)
72
- });
73
- }else {
74
- // Path to the authority certificate
75
- if (process.env.CA_CERT) {
76
- clientOptions.tlsCAFile = process.env.CA_CERT;
59
+ export const InitMongo = () => {
60
+ const isTlsActive = !(!process.env.TLS || ["0", "false"].includes(process.env.TLS.toLowerCase()));
61
+ const clientOptions = {
62
+ maxPoolSize: databasePoolSize,
63
+ authSource: 'admin'
64
+ };
65
+ if (isTlsActive) {
66
+ clientOptions.tls = true;
67
+ console.log("TLS ACTIVE");
68
+ // is mTLS ? (client certificate required instead of password)
69
+ if (process.env.CERT) {
70
+ clientOptions.secureContext = tls.createSecureContext({
71
+ ca: fs.readFileSync(process.env.CA_CERT),
72
+ cert: fs.readFileSync(process.env.CERT),
73
+ key: fs.readFileSync(process.env.CERT_KEY)
74
+ });
75
+ }else {
76
+ // Path to the authority certificate
77
+ if (process.env.CA_CERT) {
78
+ clientOptions.tlsCAFile = process.env.CA_CERT;
79
+ }
80
+ // Path to the certificate key
81
+ if (process.env.CERT_KEY) {
82
+ clientOptions.tlsCertificateKeyFile = process.env.CERT_KEY;
83
+ }
77
84
  }
78
- // Path to the certificate key
79
- if (process.env.CERT_KEY) {
80
- clientOptions.tlsCertificateKeyFile = process.env.CERT_KEY;
85
+ if (tlsAllowInvalidCertificates) {
86
+ clientOptions.tlsAllowInvalidCertificates = true;
87
+ console.warn("🚨 [SECURITY WARNING] tlsAllowInvalidCertificates is ON. Server certificate will not be validated.");
81
88
  }
89
+ if (tlsAllowInvalidHostnames) {
90
+ clientOptions.tlsAllowInvalidHostnames = true;
91
+ console.warn("🚨 [SECURITY WARNING] tlsAllowInvalidHostnames is ON. Server hostname will not be validated.");
92
+ }
93
+ }else{
94
+ console.log("🚨[SEC] TLS INACTIVE", dbUrl, clientOptions);
82
95
  }
83
- if (tlsAllowInvalidCertificates) {
84
- clientOptions.tlsAllowInvalidCertificates = true;
85
- console.warn("🚨 [SECURITY WARNING] tlsAllowInvalidCertificates is ON. Server certificate will not be validated.");
86
- }
87
- if (tlsAllowInvalidHostnames) {
88
- clientOptions.tlsAllowInvalidHostnames = true;
89
- console.warn("🚨 [SECURITY WARNING] tlsAllowInvalidHostnames is ON. Server hostname will not be validated.");
90
- }
96
+ return new InternalMongoClient(dbUrl, clientOptions);
91
97
  }
92
98
 
93
- export const MongoClient = new InternalMongoClient(dbUrl, clientOptions);
94
-
99
+ export let MongoClient = null;
95
100
 
96
101
  // Database Name
97
102
  export const MongoDatabase = () => {
98
103
  let dbName = Config.Get('dbName', dbNameBase);
104
+ if( !MongoClient)
105
+ MongoClient = InitMongo();
106
+ console.log('SELECTING ' + dbName + " database.");
99
107
  return MongoClient.db(dbName);
100
108
  }
101
109
 
@@ -164,6 +172,111 @@ export const Engine = {
164
172
  return engine._modules.find(m => m.module === module);
165
173
  };
166
174
 
175
+ /**
176
+ * Envoie une requête à un pair spécifique du cluster.
177
+ * Cette fonction gère la recherche du pair, la construction de l'URL,
178
+ * et l'ajout du header 'X-Target-Peer-Id' nécessaire pour le reverse proxy.
179
+ * @param {string} peerId - L'ID du pair cible (ex: "vox-main-1").
180
+ * @param {string} path - Le chemin de l'API à appeler sur le pair (ex: "/api/internal/replicate").
181
+ * @param {object} payload - Le corps de la requête (sera converti en JSON).
182
+ * @param {object} [options={}] - Options supplémentaires pour la requête fetch (method, headers, etc.).
183
+ * @returns {Promise<Response>} La promesse retournée par fetch.
184
+ */
185
+ engine.sendToPeer = async (peerId, path, payload, options = {}) => {
186
+ const targetPeer = engine.peers.find(p => p.id === peerId);
187
+
188
+ if (!targetPeer) {
189
+ const errorMessage = `[sendToPeer] Cannot send request: Peer '${peerId}' is not in the list of online peers.`;
190
+ logger.error(errorMessage);
191
+ return Promise.reject(new Error(errorMessage));
192
+ }
193
+
194
+ // Utilise le protocole défini dans les données du pair, ou https par défaut.
195
+ const protocol = targetPeer.protocol || 'https';
196
+ const apiPath = path.startsWith('/') ? path : `/${path}`;
197
+ const url = `${protocol}://${targetPeer.public_domain}${apiPath}`;
198
+
199
+ const fetchOptions = {
200
+ method: 'POST', // POST par défaut
201
+ ...options,
202
+ headers: {
203
+ 'Content-Type': 'application/json',
204
+ 'X-Target-Peer-Id': peerId, // Le header crucial pour le routage
205
+ ...(options.headers || {})
206
+ },
207
+ body: JSON.stringify(payload)
208
+ };
209
+
210
+ try {
211
+ logger.info(`[sendToPeer] Sending request to peer '${peerId}' at ${url}`);
212
+ return await fetch(url, fetchOptions);
213
+ } catch (error) {
214
+ logger.error(`[sendToPeer] Network error while sending to peer '${peerId}':`, error.message);
215
+ throw error; // Relancer l'erreur pour que l'appelant puisse la gérer
216
+ }
217
+ };
218
+
219
+ engine.peers = []; // Initialise la liste des pairs
220
+
221
+ const discoverPeers = async () => {
222
+ const endpoint = process.env.PEERS_ENDPOINT;
223
+ if (!endpoint) {
224
+ logger.info("PEERS_ENDPOINT not set. Skipping peer discovery.");
225
+ return;
226
+ }
227
+
228
+ try {
229
+ logger.info(`Discovering peers from ${endpoint}...`);
230
+ // Ajout d'un AbortController pour gérer le timeout
231
+ const controller = new AbortController();
232
+ const timeoutId = setTimeout(() => controller.abort(), 30000); // Timeout de 30 secondes
233
+
234
+ const response = await fetch(endpoint, { signal: controller.signal });
235
+ clearTimeout(timeoutId); // Annuler le timeout si la requête réussit
236
+
237
+ if (!response.ok) {
238
+ throw new Error(`Failed to fetch peers: ${response.status} ${response.statusText}`);
239
+ }
240
+ const data = await response.json(); // Parser en JSON
241
+
242
+ if (!data || !Array.isArray(data.peers)) { // Valider la structure
243
+ throw new Error("Invalid peer discovery response: 'peers' array not found.");
244
+ }
245
+
246
+ const allOnlinePeers = data.peers.filter(p => (p.status === 'online' || p.public_domain.startsWith(process.env.PEER_DOMAIN)));
247
+
248
+ // --- NOUVELLE LOGIQUE DE FILTRAGE PAR PRÉFIXE ---
249
+ // On se base sur le nom de domaine public de l'instance pour plus de robustesse.
250
+ if (process.env.PEER_DOMAIN) {
251
+ engine.selfUrl = process.env.PEER_DOMAIN;
252
+ const selfHostname = process.env.PEER_DOMAIN; // ex: data-api-shard-1.primals.net
253
+ logger.info(`[Cluster] Self hostname identified as '${selfHostname}'.`);
254
+ // --- CORRECTION DE LA LOGIQUE DE PRÉFIXE ---
255
+ // Extrait le préfixe du nom de domaine (ex: "data-api-shard" de "data-api-shard-1.primals.net" ou "data" de "data.primals.net")
256
+ // La regex capture tout jusqu'au premier point.
257
+ const selfPrefixMatch = selfHostname.match(/^([^.]+)/);
258
+ if (selfPrefixMatch) {
259
+ const selfPrefix = selfPrefixMatch[1].replace(/-[0-9]+$/, ''); // Supprime le suffixe numérique s'il existe
260
+ logger.info(`[Cluster] Detected prefix: '${selfPrefix}'. Filtering peers...`);
261
+
262
+ // Ne garder que les pairs qui ont le même préfixe
263
+ engine.peers = allOnlinePeers.filter(p => {
264
+ return (p.public_domain || '').startsWith(selfPrefix);
265
+ });
266
+ } else {
267
+ engine.peers = allOnlinePeers; // Pas de préfixe, on garde tout
268
+ }
269
+ } else {
270
+ engine.peers = allOnlinePeers; // Pas de selfUrl, on garde tout pour éviter une erreur
271
+ }
272
+
273
+ logger.info(`[Cluster] Final peer list: [${engine.peers.map(p => p.id).join(', ')}]`);
274
+ } catch (error) {
275
+ logger.error(`Could not discover peers from endpoint ${endpoint}:`, error.message);
276
+ engine.peers = []; // En cas d'erreur, on s'assure que la liste est vide.
277
+ }
278
+ };
279
+
167
280
  const importAndPrepareModule = async (moduleEntryPoint, moduleName) => {
168
281
  const moduleA = await import(moduleEntryPoint);
169
282
 
@@ -265,27 +378,30 @@ export const Engine = {
265
378
  // Start http server
266
379
  server = http.createServer(app);
267
380
 
381
+ await discoverPeers();
382
+
268
383
  // Server Timeout Settings
269
384
  server.timeout = 120000;
270
385
  server.headersTimeout = 20000;
271
386
  server.requestTimeout = 30000;
272
387
  server.keepAliveTimeout = 5000;
273
-
274
388
  server.listen(port);
275
389
 
276
390
  await setupInitialModels();
277
391
  await installAllPacks();
278
392
 
279
- if (cb)
280
- await cb();
281
-
282
393
  engine.get('/api/health', (req, res) => {
283
394
  res.status(200).json({
284
395
  status: 'ok',
285
396
  timestamp: new Date().toISOString()
286
397
  });
287
398
  });
288
-
399
+
400
+ // --- CORRECTION DE L'ORDRE DES MIDDLEWARES ---
401
+ // Les routes API doivent être enregistrées AVANT le serveur de fichiers statiques.
402
+ await Event.Trigger("OnServerStart", "event", "system", engine);
403
+
404
+ // Le serveur de fichiers statiques doit être le DERNIER middleware "catch-all".
289
405
  if( fs.existsSync('client/dist') ){
290
406
  app.use(sirv('client/dist', {
291
407
  single: true,
@@ -293,6 +409,10 @@ export const Engine = {
293
409
  }));
294
410
  }
295
411
 
412
+ if (cb) {
413
+ await cb();
414
+ }
415
+
296
416
  process.on('uncaughtException', function (exception) {
297
417
  console.error(exception);
298
418
  fs.appendFile('issues.txt', JSON.stringify({ code: exception.code, message: exception.message, stack: exception.stack }), function (err) {
@@ -302,8 +422,6 @@ export const Engine = {
302
422
  });
303
423
  process.exit(1);
304
424
  });
305
-
306
- await Event.Trigger("OnServerStart", "event", "system", engine);
307
425
  }
308
426
 
309
427
  engine.stop = async () => {
package/src/gameObject.js CHANGED
@@ -48,6 +48,12 @@ export class Behaviour {
48
48
  // Logique de nettoyage du comportement
49
49
  }
50
50
  }
51
+ export class DataHandler extends Behaviour {
52
+ constructor(gameObject) {
53
+ super(gameObject);
54
+ }
55
+ }
56
+
51
57
  // MovableBehavior.js
52
58
  export class MovableBehaviour extends Behaviour {
53
59
  constructor(gameObject, speed = 5) {
package/src/i18n.js CHANGED
@@ -328,7 +328,6 @@ export const translations = {
328
328
  "field_workflowRun_startedAt": "Démarré le",
329
329
  "field_workflowRun_completedAt": "Terminé le",
330
330
 
331
-
332
331
  "model_budget": "Budget",
333
332
  "model_request": "Requêtes",
334
333
  "model_imageGallery": "Galerie d'images",