data-primals-engine 1.0.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.
package/src/email.js ADDED
@@ -0,0 +1,349 @@
1
+ import { translations } from "../../data-primals-engine/src/i18n.js";
2
+ import process from "node:process";
3
+ import nodemailer from "nodemailer";
4
+ import juice from "juice";
5
+
6
+ // Le transporteur par défaut, utilisé si aucune config spécifique n'est fournie.
7
+ const defaultTransporter = nodemailer.createTransport({
8
+ host: "mail.smtp2go.com",
9
+ port: 587,
10
+ secure: false,
11
+ auth: {
12
+ user: process.env.MAIL_USER,
13
+ pass: process.env.MAIL_PASS,
14
+ },
15
+ });
16
+
17
+ /**
18
+ * Crn * @param {object} smtpConfig - L'objet de configuration SMTP.
19
+ * @returns {import('nodemailer').Transporter} Un transporteur Nodemailer.
20
+ */
21
+ const createTransporter = (smtpConfig) => {
22
+ // Valider que les informations de base sont présentes
23
+ if (!smtpConfig.host || !smtpConfig.port || !smtpConfig.user || !smtpConfig.pass){
24
+ throw new Error("SMTP configuration incomplete ('SMTP_HOST', 'SMTP_PORT','SMTP_USER', and 'SMTP_PASS' keys need to be completed in env').");
25
+ }
26
+ return nodemailer.createTransport({
27
+ host: smtpConfig.host,
28
+ port: parseInt(smtpConfig.port, 10),
29
+ secure: smtpConfig.secure === true, // Par défaut à false si non défini
30
+ auth: {
31
+ user: smtpConfig.user,
32
+ pass: smtpConfig.pass,
33
+ },
34
+ });
35
+ };
36
+
37
+
38
+ /**
39
+ * Envoie un email en utilisant une configuration SMTP spécifique ou celle par défaut.
40
+ * @param {string|string[]} email - Le ou les destinataires.
41
+ * @param {object} data - Les données de l'email (title, content).
42
+ * @param {object|null} smtpConfig - La configuration SMTP à utiliser. Si null, utilise la configuration par défaut.
43
+ * @param {string} lang - La langue pour le template.
44
+ * @param {string|null} tpl - Le template HTML à utiliser.
45
+ */
46
+ export const sendEmail = async (email = "", data, smtpConfig = null, lang, tpl = null) => {
47
+ const contactEmail = smtpConfig ? (smtpConfig.from || "Our company <noreply@ourdomain.tld>") : "Support - data@primals.net <data@primals.net>";
48
+ const emails = Array.isArray(email) ? email : [email];
49
+ if (emails.length === 0) return;
50
+
51
+ // Choisir le transporteur à utiliser
52
+ const transporter = smtpConfig ? createTransporter(smtpConfig) : defaultTransporter;
53
+
54
+ if (tpl === null) tpl = etemplate1(data, lang);
55
+ let html = tpl;
56
+ try {
57
+ html = juice(tpl);
58
+ } catch (e) {
59
+ console.error("Erreur lors de l'inlining du CSS de l'email :", e);
60
+ }
61
+
62
+ // Utiliser Promise.all pour gérer les envois de manière asynchrone
63
+ const sendPromises = emails.map((e) => {
64
+ const mailOptions = {
65
+ from: contactEmail,
66
+ to: e,
67
+ subject: data.title,
68
+ html,
69
+ };
70
+ return transporter.sendMail(mailOptions);
71
+ });
72
+
73
+ try {
74
+ await Promise.all(sendPromises);
75
+ console.log(`Email(s) envoyé(s) avec succès à: ${emails.join(', ')}`);
76
+ } catch (error) {
77
+ console.error("Erreur lors de l'envoi d'un ou plusieurs emails :", error);
78
+ // Vous pouvez relancer l'erreur si vous voulez que l'appelant la gère
79
+ throw error;
80
+ }
81
+ };
82
+
83
+ export const etemplate1 = (data, lang = "de", font = "Roboto") => {
84
+ const { title, content } = data;
85
+ // S'assurer que les traductions existent pour la langue demandée
86
+ const trs = translations[lang]?.translation || translations['en']['translation'];
87
+ return (
88
+ `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
89
+ <html xmlns="http://www.w3.org/1999/xhtml">
90
+ <head>
91
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
92
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
93
+ <title>Newsletter web.primals.net</title>
94
+ <script type="application/ld+json">
95
+ {
96
+ "@context": "http://schema.org",
97
+ "@type": "EmailMessage",
98
+ "potentialAction": {
99
+ "@type": "ViewAction",
100
+ "url": "https://web.primals.net",
101
+ "name": "web.primals.net"
102
+ }
103
+ }
104
+ </script>
105
+ <style type="text/css">
106
+ /* Based on The MailChimp Reset INLINE: Yes. */
107
+ /* Client-specific Styles */
108
+ #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
109
+ body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0; color:black}
110
+ /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
111
+ .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
112
+ .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
113
+ /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
114
+ #backgroundTable {margin:0; padding:0; width:100% !important; line-height: 100% !important;}
115
+ /* End reset */
116
+
117
+ /* Some sensible defaults for images
118
+ Bring inline: Yes. */
119
+ img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
120
+ a img {border:none;}
121
+ .image_fix {display:block;}
122
+
123
+ /* Yahoo paragraph fix
124
+ Bring inline: Yes. */
125
+ p {margin: 0;}
126
+
127
+ /* Hotmail header color reset
128
+ Bring inline: Yes. */
129
+ h1, h2, h3, h4, h5, h6 {color: black !important;}
130
+
131
+ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color: blue !important;}
132
+
133
+ h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {
134
+ color: red !important; /* Preferably not the same color as the normal header link color. There is limited support for psuedo classes in email clients, this was added just for good measure. */
135
+ }
136
+
137
+ h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {
138
+ color: purple !important; /* Preferably not the same color as the normal header link color. There is limited support for psuedo classes in email clients, this was added just for good measure. */
139
+ }
140
+
141
+ /* Outlook 07, 10 Padding issue fix
142
+ Bring inline: No.*/
143
+ table td {border-collapse: collapse; }
144
+
145
+ /* Remove spacing around Outlook 07, 10 tables
146
+ Bring inline: Yes */
147
+
148
+ /* Styling your links has become much simpler with the new Yahoo. In fact, it falls in line with the main credo of styling in email and make sure to bring your styles inline. Your link colors will be uniform across clients when brought inline.
149
+ Bring inline: Yes. */
150
+ a {color: #1381b1;}
151
+ p { color:black; }
152
+
153
+ h2 {
154
+ padding: 20px 0;
155
+ }
156
+
157
+
158
+ /***************************************************
159
+ ****************************************************
160
+ MOBILE TARGETING
161
+ ****************************************************
162
+ ***************************************************/
163
+ @media only screen and (max-device-width: 480px) {
164
+ /* Part one of controlling phone number linking for mobile. */
165
+ a[href^="tel"], a[href^="sms"] {
166
+ text-decoration: none;
167
+ color: blue; /* or whatever your want */
168
+ pointer-events: none;
169
+ cursor: default;
170
+ }
171
+
172
+ .mobile_link a[href^="tel"], .mobile_link a[href^="sms"] {
173
+ text-decoration: default;
174
+ color: orange !important;
175
+ pointer-events: auto;
176
+ cursor: default;
177
+ }
178
+
179
+ }
180
+
181
+ /* More Specific Targeting */
182
+
183
+ @media only screen and (min-device-width: 768px) and (max-device-width: 1024px) {
184
+ /* You guessed it, ipad (tablets, smaller screens, etc) */
185
+ /* repeating for the ipad */
186
+ a[href^="tel"], a[href^="sms"] {
187
+ text-decoration: none;
188
+ color: blue; /* or whatever your want */
189
+ pointer-events: none;
190
+ cursor: default;
191
+ }
192
+
193
+ .mobile_link a[href^="tel"], .mobile_link a[href^="sms"] {
194
+ text-decoration: default;
195
+ color: orange !important;
196
+ pointer-events: auto;
197
+ cursor: default;
198
+ }
199
+ }
200
+
201
+ @media only screen and (-webkit-min-device-pixel-ratio: 2) {
202
+ /* Put your iPhone 4g styles in here */
203
+ }
204
+
205
+ /* Android targeting */
206
+ @media only screen and (-webkit-device-pixel-ratio:.75){
207
+ /* Put CSS for low density (ldpi) Android layouts in here */
208
+ }
209
+ @media only screen and (-webkit-device-pixel-ratio:1){
210
+ /* Put CSS for medium density (mdpi) Android layouts in here */
211
+ }
212
+ @media only screen and (-webkit-device-pixel-ratio:1.5){
213
+ /* Put CSS for high density (hdpi) Android layouts in here */
214
+ }
215
+ /* end Android targeting */
216
+
217
+ </style>
218
+
219
+ <!-- Targeting Windows Mobile -->
220
+ <!--[if IEMobile 7]>
221
+ <style type="text/css">
222
+
223
+ </style>
224
+ <![endif]-->
225
+
226
+ <!-- ***********************************************
227
+ ****************************************************
228
+ END MOBILE TARGETING
229
+ ****************************************************
230
+ ************************************************ -->
231
+
232
+ <!--[if gte mso 9]>
233
+ <style>
234
+ /* Target Outlook 2007 and 2010 */
235
+ </style>
236
+ <![endif]-->
237
+ </head>
238
+ <body><style>
239
+ @import url(https://fonts.googleapis.com/css?family=${font});
240
+
241
+ /* Type styles for all clients */
242
+ h1,h2,h3 {
243
+ font-family: Helvetica, Arial, serif;
244
+ text-align: center;
245
+ }
246
+ h3 {
247
+ margin: 0;
248
+ }
249
+ p{
250
+ font-family: Helvetica, Arial, serif;
251
+ }
252
+ /* Type styles for WebKit clients */
253
+ @media screen and (-webkit-min-device-pixel-ratio:0) {
254
+ h1,h2,h3 {
255
+ font-family: ${font}, Helvetica, Arial, serif !important;
256
+ }
257
+ }
258
+ </style>` +
259
+ etable(
260
+ etable(
261
+ erow(
262
+ etag("h2", {'text-align': 'left'}, title) +
263
+ etable(
264
+ erow(etag('p', {}, trs[content] || content || ''), { 'text-align': 'left' })+
265
+ erow(etag('p', {}, trs['email.defaultSignature'] ||'')),
266
+ { 'text-align': 'left' }),
267
+ { 'text-align': 'left' }
268
+ )+
269
+ erow(
270
+ etag('hr', {}, '')+
271
+ etag('a', { href: "https://data.primals.net"}, 'https://data.primals.net')+
272
+ etag('div', {}, 'Service gratuit de base de donnée en ligne')+
273
+ etag("div", {}, 'Support: '+etag('a', { href:'mailto:data@primals.net'}, 'data@primals.net')),
274
+ { 'text-align': 'left' }
275
+ )
276
+ ),
277
+ { width: "1024px", 'text-align': "left" },
278
+ )
279
+ );
280
+ };
281
+
282
+ export const etag = (name, attrs, content = "") => {
283
+ return (
284
+ "<" +
285
+ name +
286
+ " " +
287
+ Object.keys(attrs)
288
+ .map((m) => m + '="' + attrs[m] + '" ')
289
+ .join("")
290
+ .trim() +
291
+ ">" +
292
+ content +
293
+ "</" +
294
+ name +
295
+ ">"
296
+ );
297
+ };
298
+
299
+ export const ebutton = (content = "", attrs = {}, style = {}) => {
300
+ return etable(
301
+ erow(
302
+ '<a href="' +
303
+ attrs["href"] +
304
+ '" style="' +
305
+ estyle(style) +
306
+ '">' +
307
+ content +
308
+ "</a>",
309
+ ),
310
+ );
311
+ };
312
+
313
+ export const etable = (content = "", style = {}) => {
314
+ return (
315
+ '<table border="0" cellPadding="0" cellSpacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; ' +
316
+ (style.width === "100%" ? "min-width: 100%;" : "") +
317
+ " !important; " +
318
+ estyle(style) +
319
+ '" width="' +
320
+ (style.width || "100%") +
321
+ '">' +
322
+ content +
323
+ "</table>"
324
+ );
325
+ };
326
+
327
+ export const erow = (content = "", style = {}) => {
328
+ const contents = !Array.isArray(content) ? [content] : content;
329
+ return (
330
+ "<tr>" +
331
+ contents
332
+ .map(
333
+ (c) =>
334
+ '<td align="center" style="' +
335
+ estyle(style) +
336
+ '" valign="top">' +
337
+ c +
338
+ "</td>",
339
+ )
340
+ .join("") +
341
+ "</tr>"
342
+ );
343
+ };
344
+
345
+ export const estyle = (options) => {
346
+ return Object.keys(options)
347
+ .map((m) => m + ": " + options[m] + ";")
348
+ .join("");
349
+ };
package/src/engine.js ADDED
@@ -0,0 +1,168 @@
1
+ import {GameObject, Logger} from "./gameObject.js";
2
+ import {Config} from "./config.js";
3
+ import fs from 'node:fs'
4
+ import express from 'express'
5
+ import {MongoClient as InternalMongoClient} from 'mongodb'
6
+ import process from "process";
7
+ import multipart from 'connect-multiparty';
8
+ import path from 'node:path';
9
+ import bodyParser from "body-parser";
10
+ import {cookiesSecret, dbName, install, maxDataSize} from "./constants.js";
11
+ import http from "http";
12
+ import cookieParser from "cookie-parser";
13
+ import requestIp from 'request-ip';
14
+ import {createModel, getModels, installAllPacks, validateModelStructure} from "./modules/data.js";
15
+ import {defaultModels} from "./defaultModels.js";
16
+ import {DefaultUserProvider} from "./providers.js";
17
+
18
+ // Constants
19
+ const isProduction = process.env.NODE_ENV === 'production'
20
+
21
+ // Connection URL
22
+ export const dbUrl = process.env.MONGO_DB_URL || 'mongodb://127.0.0.1:27017';
23
+ export const MongoClient = new InternalMongoClient(dbUrl, { maxPoolSize: 20 });
24
+
25
+ // Database Name
26
+ export const MongoDatabase = MongoClient.db(dbName);
27
+
28
+
29
+ export const Engine = {
30
+ Create: (options) => {
31
+ const engine = GameObject.Create("Engine");
32
+ console.log("Creating engine", Config.Get('modules'));
33
+
34
+ engine.userProvider = new DefaultUserProvider(engine);
35
+
36
+ engine.setUserProvider = (providerInstance) => {
37
+ engine.userProvider = providerInstance;
38
+ engine.getComponent(Logger).info(`Custom UserProvider '${providerInstance.constructor.name}' has been set.`);
39
+ };
40
+
41
+ var app = express();
42
+
43
+ // Allows you to set port in the project properties.
44
+ app.set('port', process.env.PORT || 3000);
45
+ app.set('engine', engine);
46
+
47
+ app.use(bodyParser.urlencoded({extended: true, limit: 4096 }))
48
+ app.use(bodyParser.json({ limit: maxDataSize }))
49
+ app.use(cookieParser(isProduction ? cookiesSecret : 'secret'));
50
+
51
+ var multipartMiddleware = multipart();
52
+ app.use(multipartMiddleware);
53
+
54
+ app.use(requestIp.mw())
55
+
56
+ engine.use = (...args) => {
57
+ return app.use(...args);
58
+ }
59
+ engine.post = (...args) => {
60
+ return app.post(...args);
61
+ };
62
+ engine.get = (...args) => {
63
+ return app.get(...args);
64
+ };
65
+ engine.delete = (...args) => {
66
+ return app.delete(...args);
67
+ };
68
+ engine.patch = (...args) => {
69
+ return app.patch(...args);
70
+ };
71
+ engine.put = (...args) => {
72
+ return app.put(...args);
73
+ };
74
+ engine.getModule = (module) => {
75
+ return engine._modules.find(m => m.module === module);
76
+ };
77
+
78
+ let server;
79
+ engine.start = async (port, cb) =>{
80
+ // Use connect method to connect to the server
81
+ await MongoClient.connect();
82
+
83
+ // Start http server
84
+ server = http.createServer(app);
85
+
86
+ // Server Timeout Settings
87
+ server.timeout = 120000;
88
+ server.headersTimeout = 20000;
89
+ server.requestTimeout = 30000;
90
+ server.keepAliveTimeout = 5000;
91
+
92
+ server.listen(port);
93
+
94
+ const importModule = async (module) => {
95
+ const moduleA = await import(module);
96
+ if (moduleA.onInit){
97
+ await moduleA.onInit(engine);
98
+ return {...moduleA, module};
99
+ }else {
100
+ const mod = moduleA.default();
101
+ await mod?.onInit(engine);
102
+ return { ...mod, module};
103
+ }
104
+ };
105
+
106
+ await Promise.all(Config.Get('modules').map(async module => {
107
+ try {
108
+ if( fs.existsSync(module)){
109
+ return await importModule(module);
110
+ }else {
111
+ return await importModule('./modules/' + module + ".js");
112
+ }
113
+ } catch (e){
114
+ console.log('ERROR at loading module '+ module + ' in /modules dir.'+ e);
115
+ }
116
+ })).then(async e => {
117
+ engine._modules = e;
118
+ if (cb)
119
+ return await cb();
120
+ return Promise.resolve();
121
+ });
122
+
123
+ await setupInitialModels();
124
+
125
+ process.on('uncaughtException', function (exception) {
126
+ console.error(exception);
127
+ fs.appendFile('bugs.txt', JSON.stringify({ code: exception.code, message: exception.message, stack: exception.stack }), function (err) {
128
+ if (err){
129
+ throw err;
130
+ }
131
+ });
132
+ process.exit(0);
133
+ });
134
+ }
135
+ engine.addComponent(Logger);
136
+
137
+ engine.stop = async () => {
138
+ await server.close();
139
+ };
140
+
141
+ const logger = engine.getComponent(Logger);
142
+
143
+ async function setupInitialModels() {
144
+ logger.info("Validation des structures de modèles et insertion");
145
+ const ms = Object.values(Config.Get('defaultModels', []));
146
+
147
+ let dbModels = await getModels();
148
+
149
+ for(let i = 0; i < ms.length; ++i){
150
+ const model = ms[i];
151
+ validateModelStructure(model);
152
+ // Création des modèles
153
+ if( !dbModels.find(m =>m.name === model.name) )
154
+ {
155
+ model.locked = true;
156
+ const r = await createModel(model);
157
+ dbModels.push({...model, _id: r.insertedId });
158
+ }
159
+ else
160
+ logger.info('Model loaded (' + model.name + ')', {});
161
+ }
162
+ logger.info("All models loaded.");
163
+ }
164
+
165
+ return engine;
166
+ }
167
+ }
168
+
package/src/events.js ADDED
@@ -0,0 +1,81 @@
1
+ import {createModel, getModels, validateModelStructure} from "./modules/data.js";
2
+
3
+ const events = {};
4
+
5
+ const eventLayerSystems = {
6
+ "priority": ["high", "medium", "low"],
7
+ "log": ["info", "debug", "warn", "error", "critical"],
8
+ "system": ["calls"]
9
+ };
10
+
11
+
12
+
13
+ export const Event = {
14
+ Trigger: (name, system = "priority", layer = "medium", ...args) => { // Ajout des arguments system et layer
15
+ if (!events[system] || !events[system][name] || (layer && !events[system][name][layer])) {
16
+ //console.warn(`No trigger found for ${name} in system ${system} layer ${layer}`);
17
+ return;
18
+ }
19
+
20
+ const systemsToProcess = system ? [system] : Object.keys(events); // Si system est spécifié, on cible ce système uniquement, sinon tous les systèmes
21
+
22
+ for (const currentSystem of systemsToProcess) {
23
+ if (events[currentSystem] && events[currentSystem][name]) {
24
+ const layersToProcess = layer ? [layer] : eventLayerSystems[currentSystem] || Object.keys(events[currentSystem][name]); // Si layer est spécifié, on cible cette couche, sinon toutes les couches ou celles définies dans eventLayerSystems
25
+
26
+ if (layersToProcess) {
27
+ for (const currentLayer of layersToProcess) {
28
+ if (events[currentSystem][name][currentLayer]) {
29
+ for (const callback of events[currentSystem][name][currentLayer]) {
30
+ try {
31
+ callback(...args);
32
+ } catch (error) {
33
+ console.error(`Error in callback for event ${name} in system ${currentSystem} layer ${currentLayer}:`, error);
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ },
42
+ Listen: (name = "", callback = () => {}, system = "priority", layer = "medium") => {
43
+ const validSystems = Object.keys(eventLayerSystems); // Récupération des clés pour une vérification plus performante
44
+ if (!validSystems.includes(system)) {
45
+ throw new Error(`System '${system}' does not exist. Valid systems are: ${validSystems.join(', ')}`); // Message d'erreur plus informatif
46
+ }
47
+
48
+ const validLayers = eventLayerSystems[system]; // Récupération des couches valides pour le système
49
+ if (!validLayers.includes(layer)) {
50
+ throw new Error(`Layer '${layer}' does not exist in system '${system}'. Valid layers are: ${validLayers.join(', ')}`); // Message d'erreur plus informatif
51
+ }
52
+
53
+ events[system] = events[system] || {}; // Simplification de la création des objets
54
+ events[system][name] = events[system][name] || {};
55
+ events[system][name][layer] = events[system][name][layer] || [];
56
+ events[system][name][layer].push(callback);
57
+ },
58
+ RemoveCallback: (name, callback, layer="medium", system='priority') => {
59
+ if (!events[system] || !events[system][name] || !events[system][name][layer]) {
60
+ return; // Si l'événement, le système ou la couche n'existent pas, on ne fait rien
61
+ }
62
+
63
+ const index = events[system][name][layer].indexOf(callback);
64
+ if (index > -1) {
65
+ events[system][name][layer].splice(index, 1);
66
+ }
67
+ },
68
+ // Ajout d'une méthode pour supprimer tous les callbacks d'un événement (optionnel)
69
+ RemoveAllCallbacks: (name, system = 'priority') => {
70
+ if (events[system] && events[system][name]) {
71
+ delete events[system][name]; // Supprime l'objet contenant les couches et les callbacks
72
+ }
73
+ },
74
+
75
+ // Ajout d'une méthode pour supprimer tous les callbacks d'un système (optionnel)
76
+ RemoveSystemCallbacks: (system) => {
77
+ if (events[system]) {
78
+ delete events[system];
79
+ }
80
+ },
81
+ }