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.
- package/README.md +9 -9
- package/client/package-lock.json +8430 -8121
- package/client/package.json +7 -4
- package/client/src/APIInfo.jsx +1 -1
- package/client/src/App.jsx +2 -1
- package/client/src/App.scss +1635 -1626
- package/client/src/AssistantChat.jsx +2 -3
- package/client/src/CalendarView.jsx +1 -0
- package/client/src/ContentView.jsx +3 -3
- package/client/src/Dashboard.jsx +5 -2
- package/client/src/DashboardChart.jsx +1 -0
- package/client/src/DashboardFlexViewItem.jsx +1 -0
- package/client/src/DashboardHtmlViewItem.jsx +1 -0
- package/client/src/DashboardView.jsx +2 -0
- package/client/src/DataEditor.jsx +0 -1
- package/client/src/DataImporter.jsx +489 -468
- package/client/src/DataLayout.jsx +25 -23
- package/client/src/DataTable.jsx +6 -5
- package/client/src/Dialog.jsx +92 -90
- package/client/src/Dialog.scss +122 -116
- package/client/src/DisplayFlexNodeRenderer.jsx +1 -0
- package/client/src/DocumentationPageLayout.scss +1 -1
- package/client/src/FlexBuilderControls.jsx +1 -0
- package/client/src/FlexBuilderPreview.jsx +1 -1
- package/client/src/FlexNode.jsx +1 -1
- package/client/src/HistoryDialog.jsx +3 -2
- package/client/src/KPIWidget.jsx +1 -1
- package/client/src/KanbanView.jsx +1 -0
- package/client/src/ModelCreator.jsx +4 -0
- package/client/src/ModelList.jsx +1 -0
- package/client/src/PackGallery.jsx +5 -4
- package/client/src/RTETrans.jsx +1 -1
- package/client/src/RelationField.jsx +2 -0
- package/client/src/RelationSelectorWidget.jsx +2 -0
- package/client/src/RelationValue.jsx +1 -0
- package/client/src/RestoreDialog.jsx +1 -0
- package/client/src/ViewSwitcher.jsx +1 -1
- package/client/src/WorkflowEditor.jsx +3 -0
- package/client/src/contexts/CommandContext.jsx +2 -1
- package/client/src/contexts/ModelContext.jsx +3 -3
- package/client/src/hooks/data.js +1 -0
- package/client/src/hooks/useValidation.js +1 -0
- package/client/src/translations.js +24 -24
- package/client/vite.config.js +31 -30
- package/doc/AI-assistance.md +87 -63
- package/doc/Concepts.md +122 -0
- package/doc/Custom-Endpoints.md +31 -0
- package/doc/Event-system.md +13 -14
- package/doc/Home.md +33 -0
- package/doc/Modules.md +83 -0
- package/doc/Packs-gallery.md +8 -23
- package/doc/Workflows.md +32 -0
- package/doc/automation-workflows.md +141 -102
- package/doc/dashboards-kpis-charts.md +47 -49
- package/doc/data-management.md +126 -120
- package/doc/data-models.md +68 -75
- package/doc/roles-permissions.md +144 -43
- package/doc/sharding-replication.md +158 -0
- package/doc/users.md +54 -30
- package/package.json +27 -17
- package/server.js +37 -37
- package/src/ai.jobs.js +135 -0
- package/src/constants.js +560 -545
- package/src/data.js +521 -518
- package/src/email.js +157 -154
- package/src/engine.js +167 -49
- package/src/gameObject.js +6 -0
- package/src/i18n.js +0 -1
- package/src/modules/assistant/assistant.js +782 -763
- package/src/modules/assistant/constants.js +23 -16
- package/src/modules/assistant/providers.js +77 -37
- package/src/modules/auth-google/index.js +53 -50
- package/src/modules/bucket.js +346 -335
- package/src/modules/data/data.backup.js +400 -376
- package/src/modules/data/data.cluster.js +559 -0
- package/src/modules/data/data.core.js +11 -8
- package/src/modules/data/data.js +311 -311
- package/src/modules/data/data.operations.js +3666 -3555
- package/src/modules/data/data.relations.js +1 -0
- package/src/modules/data/data.replication.js +125 -0
- package/src/modules/data/data.routes.js +2206 -1879
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/file.js +248 -247
- package/src/modules/mongodb.js +76 -73
- package/src/modules/user.js +18 -2
- package/src/modules/worker-script-runner.js +97 -0
- package/src/modules/workflow.js +177 -52
- package/src/packs.js +5701 -5701
- package/src/providers.js +298 -297
- package/src/sso.js +91 -25
- package/test/assistant.test.js +207 -206
- package/test/cluster.test.js +221 -0
- package/test/core.test.js +0 -2
- package/test/data.integration.test.js +1425 -1416
- package/test/import_export.integration.test.js +210 -210
- package/test/replication.test.js +163 -0
- package/test/workflow.actions.integration.test.js +487 -475
- package/test/workflow.integration.test.js +332 -329
- package/doc/core-concepts.md +0 -33
package/src/email.js
CHANGED
|
@@ -1,155 +1,158 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
* @param {
|
|
44
|
-
* @param {object
|
|
45
|
-
* @param {
|
|
46
|
-
* @param {string
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
"
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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
|
|
57
|
-
|
|
58
|
-
const clientOptions = {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
|
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