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/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // data-primals-engine/src/index.js
2
+
3
+ // --- Core Engine & Utilities ---
4
+ export { Engine } from './engine.js';
5
+ export { GameObject, Logger, BenchmarkTool } from './gameObject.js';
6
+ export { Config } from './config.js';
7
+ export { event_on, event_trigger, event_off } from './core.js';
8
+
9
+ export { UserProvider } from './providers.js';
10
+
11
+ // --- Database & Data Modules ---
12
+ export { datasCollection, filesCollection, modelsCollection, packsCollection } from './modules/mongodb.js';
13
+ export { searchData, insertData, editData, loadFromDump, dumpUserData, validateRestoreRequest, patchData, deleteData, createModel, getModel, getModels } from './modules/data.js';
14
+
15
+
@@ -0,0 +1,131 @@
1
+ import {isIsoDate} from "../core.js";
2
+
3
+ const TEST_REGEX = /^\$|\./;
4
+ const TEST_REGEX_WITHOUT_DOT = /^\$/;
5
+ const REPLACE_REGEX = /^\$|\./g;
6
+
7
+ function isPlainObject(obj) {
8
+ return typeof obj === 'object' && obj !== null;
9
+ }
10
+
11
+ function getTestRegex(allowDots) {
12
+ return allowDots ? TEST_REGEX_WITHOUT_DOT : TEST_REGEX;
13
+ }
14
+
15
+ function withEach(target, cb) {
16
+ (function act(obj) {
17
+ if (Array.isArray(obj)) {
18
+ obj.forEach(act);
19
+ } else if (isPlainObject(obj)) {
20
+ Object.keys(obj).forEach(function (key) {
21
+ const val = obj[key];
22
+ const resp = cb(obj, val, key);
23
+ if (resp.shouldRecurse) {
24
+ act(obj[resp.key || key]);
25
+ }
26
+ });
27
+ }
28
+ })(target);
29
+ }
30
+
31
+ function has(target, allowDots) {
32
+ const regex = getTestRegex(allowDots);
33
+
34
+ let hasProhibited = false;
35
+ withEach(target, function (obj, val, key) {
36
+ if (regex.test(key)) {
37
+ hasProhibited = true;
38
+ return { shouldRecurse: false };
39
+ } else {
40
+ return { shouldRecurse: true };
41
+ }
42
+ });
43
+
44
+ return hasProhibited;
45
+ }
46
+
47
+ function _sanitize(target, options) {
48
+ const regex = getTestRegex(options.allowDots);
49
+
50
+ let isSanitized = false;
51
+ let replaceWith = null;
52
+ const dryRun = Boolean(options.dryRun);
53
+ if (!regex.test(options.replaceWith) && options.replaceWith !== '.') {
54
+ replaceWith = options.replaceWith;
55
+ }
56
+
57
+ const wl = options.whitelist || [];
58
+ RegExp.prototype.toJSON = RegExp.prototype.toString;
59
+
60
+ withEach(target, function (obj, val, key) {
61
+ let shouldRecurse = true;
62
+
63
+ if( (key === 'regex' && obj.input) || key === '$regex' ){
64
+ obj[key] = new RegExp(val, "ui");
65
+ console.log('regex san', obj[key]);
66
+ }
67
+ else if (!wl.includes(key) && regex.test(key)) {
68
+ isSanitized = true;
69
+ // if dryRun is enabled, do not modify the target
70
+ if (dryRun) {
71
+ return {
72
+ shouldRecurse: shouldRecurse,
73
+ key: key,
74
+ };
75
+ }
76
+ delete obj[key];
77
+ if (replaceWith) {
78
+ key = key.replace(REPLACE_REGEX, replaceWith);
79
+ // Avoid to set __proto__ and constructor.prototype
80
+ // https://portswigger.net/daily-swig/prototype-pollution-the-dangerous-and-underrated-vulnerability-impacting-javascript-applications
81
+ // https://snyk.io/vuln/SNYK-JS-LODASH-73638
82
+ if (
83
+ key !== '__proto__' &&
84
+ key !== 'constructor' &&
85
+ key !== 'prototype'
86
+ ) {
87
+ obj[key] = val;
88
+ }
89
+ } else {
90
+ shouldRecurse = false;
91
+ }
92
+ }
93
+
94
+ return {
95
+ shouldRecurse: shouldRecurse,
96
+ key: key,
97
+ };
98
+ });
99
+
100
+ return {
101
+ isSanitized,
102
+ target,
103
+ };
104
+ }
105
+
106
+ function sanitize(target, options = {}) {
107
+ return _sanitize(target, options).target;
108
+ }
109
+
110
+ /**
111
+ * @param {{replaceWith?: string, onSanitize?: function, dryRun?: boolean}} options
112
+ * @returns {function}
113
+ */
114
+ export function middleware(options = {}) {
115
+ const hasOnSanitize = typeof options.onSanitize === 'function';
116
+ return function (req, res, next) {
117
+ ['body', 'params', 'headers', 'query'].forEach(function (key) {
118
+ if (req[key]) {
119
+ const { target, isSanitized } = _sanitize(req[key], options);
120
+ req[key] = target;
121
+ if (isSanitized && hasOnSanitize) {
122
+ options.onSanitize({
123
+ req,
124
+ key,
125
+ });
126
+ }
127
+ }
128
+ });
129
+ next();
130
+ };
131
+ }
@@ -0,0 +1,33 @@
1
+ export const throttleMiddleware = (bps) => function(req, res, next) {
2
+ if (bps > 0) {
3
+ var total = 0;
4
+ var resume = req.socket.resume;
5
+
6
+ // make sure nothing else can resume
7
+ req.socket.resume = function() {};
8
+
9
+ var pulse = setInterval(function() {
10
+ total = total - bps / 100;
11
+ if (total < bps) {
12
+ resume.call(req.socket);
13
+ }
14
+ }, 10);
15
+
16
+ req.on('data', function(chunk) {
17
+ total += chunk.length;
18
+ if (total >= bps) {
19
+ req.socket.pause();
20
+ }
21
+ });
22
+
23
+ req.on('end', function() {
24
+ clearInterval(pulse);
25
+ // restore resume because socket could be reused
26
+ req.socket.resume = resume;
27
+ // future requests need the socket to be flowing
28
+ req.socket.resume();
29
+ });
30
+ }
31
+
32
+ next();
33
+ };
@@ -0,0 +1,47 @@
1
+
2
+ /**
3
+ * Middleware to set a specific request timeout.
4
+ * If the timeout is reached, it sends a 408 Request Timeout response.
5
+ * @param {number} timeoutMs - The timeout in milliseconds.
6
+ * @returns {function(req, res, next): void}
7
+ */
8
+ // Fichier: server/src/middlewares/timeout.js (suggestion)
9
+
10
+ /**
11
+ * Middleware to set a specific request timeout.
12
+ * If the timeout is reached, it sends a 408 Request Timeout response.
13
+ * @param {number} timeoutMs - The timeout in milliseconds.
14
+ * @returns {function(req, res, next): void}
15
+ */
16
+ export const setTimeoutMiddleware = (timeoutMs) => {
17
+ return (req, res, next) => {
18
+ // Set the timeout for this specific request's socket
19
+ req.setTimeout(timeoutMs);
20
+
21
+ const timeoutHandler = () => {
22
+ // Check if headers have already been sent
23
+ if (!res.headersSent) {
24
+ res.status(408).json({
25
+ error: true,
26
+ message: `Request Timeout: The server did not receive a complete request message within the time that it was prepared to wait (${timeoutMs}ms).`
27
+ });
28
+ }
29
+ // req.abort() n'est généralement pas nécessaire ici.
30
+ // La réponse envoyée ci-dessus signale la fin de la requête.
31
+ // Le serveur gérera la fermeture du socket.
32
+ };
33
+
34
+ const cleanupListeners = () => {
35
+ req.removeListener('timeout', timeoutHandler);
36
+ };
37
+
38
+ // Assign the timeout handler
39
+ req.on('timeout', timeoutHandler);
40
+
41
+ // Clean up the listener when the response finishes or the connection closes
42
+ res.on('finish', cleanupListeners);
43
+ res.on('close', cleanupListeners);
44
+
45
+ next();
46
+ };
47
+ };
package/src/migrate.js ADDED
@@ -0,0 +1,216 @@
1
+ // C:/Dev/hackersonline-engine/server/src/migrate.js
2
+
3
+ import process from "node:process";
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ // FIX: Import 'pathToFileURL' instead of 'fileURLToPath'
7
+ import { pathToFileURL } from 'node:url';
8
+ import { Engine, MongoDatabase } from "./engine.js";
9
+ import { Logger } from "./gameObject.js";
10
+ import { Config } from "./config.js";
11
+ import chalk from "chalk"; // Pour une sortie plus lisible. Assurez-vous que 'chalk' est dans vos dépendances.
12
+
13
+ // Configuration de base
14
+ Config.Set("modules", ["mongodb"]);
15
+ const MIGRATIONS_DIR = path.resolve(process.cwd(), 'server', 'src', 'migrations');
16
+ const MIGRATIONS_COLLECTION = 'migrations_log';
17
+
18
+ const engine = Engine.Create();
19
+ const port = process.env.MIGRATE_PORT || 7640;
20
+
21
+ engine.start(port, async () => {
22
+ const logger = engine.getComponent(Logger);
23
+ logger.info("Migration Runner started on port " + port);
24
+
25
+ const command = process.argv[2]; // 'up', 'down', 'create', 'status', 'to', 'revert'
26
+ const target = process.argv[3]; // Le nom du fichier de migration cible
27
+
28
+ try {
29
+ const db = MongoDatabase;
30
+ const { executedNames, allMigrationFiles } = await getMigrationStatus(db);
31
+
32
+ switch (command) {
33
+ case 'create':
34
+ if (!target) throw new Error("Please provide a name for the migration.");
35
+ await createMigrationFile(target, logger);
36
+ break;
37
+ case 'status':
38
+ displayStatus(executedNames, allMigrationFiles, logger);
39
+ break;
40
+ case 'up':
41
+ await runMigrations('up', null, logger, db, executedNames, allMigrationFiles);
42
+ break;
43
+ case 'down':
44
+ // Annule la dernière migration
45
+ await runMigrations('down', 'last', logger, db, executedNames, allMigrationFiles);
46
+ break;
47
+ case 'to':
48
+ if (!target) throw new Error("Please specify a target migration file for the 'to' command.");
49
+ await runMigrations('up', target, logger, db, executedNames, allMigrationFiles);
50
+ break;
51
+ case 'revert':
52
+ if (!target) throw new Error("Please specify a target migration file for the 'revert' command. Use '0' to revert all.");
53
+ await runMigrations('down', target, logger, db, executedNames, allMigrationFiles);
54
+ break;
55
+ default:
56
+ throw new Error(`Unknown command: ${command}. Use 'up', 'down', 'create', 'status', 'to', 'revert'.`);
57
+ }
58
+ } catch (error) {
59
+ logger.error(chalk.red("Migration process failed:"), error.message);
60
+ if (error.stack) {
61
+ logger.error(error.stack);
62
+ }
63
+ process.exit(1); // Sortir avec un code d'erreur
64
+ } finally {
65
+ logger.info("Migration process finished. Shutting down.");
66
+ process.exit(0);
67
+ }
68
+ });
69
+
70
+ async function getMigrationStatus(db) {
71
+ const migrationLogCollection = db.collection(MIGRATIONS_COLLECTION);
72
+ const executedMigrations = await migrationLogCollection.find().sort({ name: 1 }).toArray();
73
+ const executedNames = new Set(executedMigrations.map(m => m.name));
74
+
75
+ let allMigrationFiles = [];
76
+ try {
77
+ allMigrationFiles = (await fs.readdir(MIGRATIONS_DIR))
78
+ .filter(file => file.endsWith('.js'))
79
+ .sort();
80
+ } catch (e) {
81
+ if (e.code !== 'ENOENT') throw e;
82
+ // Le dossier n'existe pas, c'est ok s'il n'y a aucune migration.
83
+ }
84
+
85
+ return { executedNames, allMigrationFiles };
86
+ }
87
+
88
+ function displayStatus(executedNames, allMigrationFiles, logger) {
89
+ logger.info(chalk.bold('Migration Status:'));
90
+ if (allMigrationFiles.length === 0) {
91
+ logger.info('No migration files found.');
92
+ return;
93
+ }
94
+ allMigrationFiles.forEach(file => {
95
+ if (executedNames.has(file)) {
96
+ logger.info(`${chalk.green('[Applied]')} ${file}`);
97
+ } else {
98
+ logger.info(`${chalk.yellow('[Pending]')} ${file}`);
99
+ }
100
+ });
101
+ }
102
+
103
+ async function runMigrations(direction, target, logger, db, executedNames, allMigrationFiles) {
104
+ const migrationLogCollection = db.collection(MIGRATIONS_COLLECTION);
105
+
106
+ if (direction === 'up') {
107
+ let pendingMigrations = allMigrationFiles.filter(file => !executedNames.has(file));
108
+
109
+ if (target) {
110
+ const targetIndex = pendingMigrations.findIndex(file => file.startsWith(target) || file === target);
111
+ if (targetIndex === -1) {
112
+ logger.info(`Target migration '${target}' not found or already applied.`);
113
+ return;
114
+ }
115
+ pendingMigrations = pendingMigrations.slice(0, targetIndex + 1);
116
+ }
117
+
118
+ if (pendingMigrations.length === 0) {
119
+ logger.info(chalk.green("Database is already up to date."));
120
+ return;
121
+ }
122
+
123
+ logger.info(`Applying ${pendingMigrations.length} new migration(s)...`);
124
+ for (const fileName of pendingMigrations) {
125
+ logger.info(`-> ${chalk.cyan('Running UP')}: ${fileName}`);
126
+ const migrationPath = path.resolve(MIGRATIONS_DIR, fileName);
127
+ // FIX: Convert the absolute path to a file URL for dynamic import
128
+ const migration = await import(pathToFileURL(migrationPath).href);
129
+ await migration.up(db);
130
+ await migrationLogCollection.insertOne({ name: fileName, executedAt: new Date() });
131
+ logger.info(` ${chalk.green('OK')} - ${fileName} applied and logged.`);
132
+ }
133
+ }
134
+
135
+ if (direction === 'down') {
136
+ const executedFiles = allMigrationFiles
137
+ .filter(file => executedNames.has(file))
138
+ .sort((a, b) => b.localeCompare(a)); // Trier en ordre inverse pour la descente
139
+
140
+ if (executedFiles.length === 0) {
141
+ logger.info(chalk.yellow("No migrations to roll back."));
142
+ return;
143
+ }
144
+
145
+ let migrationsToRevert;
146
+
147
+ if (target === 'last') {
148
+ migrationsToRevert = [executedFiles[0]]; // Juste la dernière
149
+ } else if (target === '0') {
150
+ migrationsToRevert = executedFiles; // Toutes les migrations
151
+ } else {
152
+ const targetIndex = executedFiles.findIndex(file => file.startsWith(target) || file === target);
153
+ if (targetIndex === -1) {
154
+ logger.info(`Target migration '${target}' not found among applied migrations.`);
155
+ return;
156
+ }
157
+ migrationsToRevert = executedFiles.slice(0, targetIndex);
158
+ }
159
+
160
+ if (!migrationsToRevert || migrationsToRevert.length === 0) {
161
+ logger.info(chalk.green("Already at the target state. No migrations to revert."));
162
+ return;
163
+ }
164
+
165
+ logger.info(`Reverting ${migrationsToRevert.length} migration(s)...`);
166
+ for (const fileName of migrationsToRevert) {
167
+ logger.info(`-> ${chalk.cyan('Running DOWN')}: ${fileName}`);
168
+ const migrationPath = path.resolve(MIGRATIONS_DIR, fileName);
169
+ // FIX: Convert the absolute path to a file URL for dynamic import
170
+ const migration = await import(pathToFileURL(migrationPath).href);
171
+ await migration.down(db);
172
+ await migrationLogCollection.deleteOne({ name: fileName });
173
+ logger.info(` ${chalk.green('OK')} - ${fileName} rolled back and removed from log.`);
174
+ }
175
+ }
176
+ }
177
+
178
+ // La fonction createMigrationFile reste la même
179
+ async function createMigrationFile(name, logger) {
180
+ const timestamp = new Date().toISOString().replace(/[-:.]/g, '').slice(0, 14);
181
+ const sanitizedName = name.replace(/[^a-z0-9_]/gi, '-').toLowerCase();
182
+ const fileName = `${timestamp}-${sanitizedName}.js`;
183
+ const filePath = path.join(MIGRATIONS_DIR, fileName);
184
+
185
+ const template = `
186
+ /**
187
+ * Migration: ${name}
188
+ * Created at: ${new Date().toISOString()}
189
+ */
190
+ import { Db } from 'mongodb';
191
+
192
+ /**
193
+ * La fonction 'up' est exécutée pour appliquer la migration.
194
+ * @param {Db} db - L'instance de la base de données MongoDB.
195
+ */
196
+ export const up = async (db) => {
197
+ // Votre logique de migration ici. Exemple :
198
+ // await db.collection('users').updateMany({}, { $set: { newField: 'defaultValue' } });
199
+ console.log("Migration UP: ${fileName}");
200
+ };
201
+
202
+ /**
203
+ * La fonction 'down' est exécutée pour annuler la migration.
204
+ * @param {Db} db - L'instance de la base de données MongoDB.
205
+ */
206
+ export const down = async (db) => {
207
+ // Votre logique pour annuler la migration ici. Exemple :
208
+ // await db.collection('users').updateMany({}, { $unset: { newField: '' } });
209
+ console.log("Migration DOWN: ${fileName}");
210
+ };
211
+ `;
212
+
213
+ await fs.mkdir(MIGRATIONS_DIR, { recursive: true });
214
+ await fs.writeFile(filePath, template.trim());
215
+ logger.info(`Created migration file: ${chalk.magenta(filePath)}`);
216
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Migration: setting content field to 'fr' in 'content' model
3
+ * Created at: 2024-05-21T10:00:00.000Z
4
+ */
5
+
6
+ export const up = async (db) => {
7
+ const datasCollection = db.collection("datas");
8
+ const content = await datasCollection.find({ _model:"content"}).toArray();
9
+ await Promise.all(content.map(async (doc) => {
10
+ if (typeof(doc.html)==='string') {
11
+ const c = { "fr" : doc.html };
12
+ await datasCollection.updateOne({_id: doc._id}, {$set: {html: c}});
13
+ }
14
+ }));
15
+ console.log("Migration UP: setting content field to 'fr' in 'content' model");
16
+ };
17
+
18
+ export const down = async (db) => {
19
+ console.log("Migration DOWN: noting to do");
20
+ };