@anonympins/fingerprint 0.2.1 → 0.2.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 +719 -598
- package/fingerprint.client.js +477 -471
- package/fingerprint.js +2953 -2673
- package/library.js +83 -8
- package/mongodb-store.js +52 -52
- package/package.json +88 -79
- package/pow.solver.js +61 -13
- package/pow.worker.js +26 -26
- package/problem-manager.js +249 -1
- package/redis-store.js +42 -42
- package/sql-store.js +77 -77
package/problem-manager.js
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { Optimization } from './library.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @namespace FunctionRegistry
|
|
6
|
+
* @description Registre pour exposer de manière contrôlée les fonctions de la bibliothèque.
|
|
7
|
+
* Permet de les appeler dynamiquement depuis la configuration des problèmes.
|
|
8
|
+
* Utilise la notation par points pour accéder aux fonctions imbriquées (ex: 'tsp.calculateEnergy').
|
|
9
|
+
*/
|
|
10
|
+
const FunctionRegistry = {};
|
|
11
|
+
|
|
12
|
+
// --- Fonctions de "Scoring" (évaluation d'une solution) ---
|
|
13
|
+
// Ces fonctions sont des adaptateurs pour utiliser les utilitaires de la bibliothèque
|
|
14
|
+
// avec la structure attendue par le ProblemManager.
|
|
15
|
+
FunctionRegistry['cpc.solve'] = Optimization.Operators.solveOptimalCPC; // NOUVEAU: Enregistrement du solveur CPC
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
/**
|
|
19
|
+
* Évalue la distance totale d'un chemin pour le problème du voyageur de commerce (TSP).
|
|
20
|
+
* @param {Array<{x: number, y: number}>} path - Un tableau de points représentant le chemin.
|
|
21
|
+
* @returns {number} La distance totale du chemin.
|
|
22
|
+
*/
|
|
23
|
+
FunctionRegistry['tsp.calculateEnergy'] = (path) => {
|
|
24
|
+
// Crée un tableau d'indices [0, 1, 2, ...] pour la fonction evaluatePathDistance.
|
|
25
|
+
const indices = Array.from({ length: path.length }, (_, i) => i);
|
|
26
|
+
return Optimization.Utils.evaluatePathDistance(path, indices);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Évalue les métriques d'un portefeuille (rendement et volatilité).
|
|
31
|
+
* Pour l'instant, retourne le rendement négatif pour correspondre à l'objectif de minimisation
|
|
32
|
+
* de l'algorithme génétique de la bibliothèque.
|
|
33
|
+
* @param {Array<number>} weights - Les poids des actifs dans le portefeuille.
|
|
34
|
+
* @param {object} payload - Le payload du problème, contenant les actifs.
|
|
35
|
+
* @returns {number} Le rendement négatif du portefeuille.
|
|
36
|
+
*/
|
|
37
|
+
FunctionRegistry['portfolio.calculateMetrics'] = (weights, payload) => {
|
|
38
|
+
const { assets, maxVolatility } = payload;
|
|
39
|
+
// On utilise l'opérateur de la bibliothèque pour créer la fonction de fitness
|
|
40
|
+
// et on l'appelle immédiatement.
|
|
41
|
+
const fitnessFunction = Optimization.Operators.createPortfolioAllocator({
|
|
42
|
+
assets,
|
|
43
|
+
maxVolatility,
|
|
44
|
+
});
|
|
45
|
+
// La fonction de fitness retourne le rendement négatif, ce qui est ce que nous voulons
|
|
46
|
+
// stocker comme "énergie" ou score.
|
|
47
|
+
return fitnessFunction(weights);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// --- Fonctions de "Résolution" (algorithmes complets) ---
|
|
51
|
+
// Utiles pour les workers qui exécutent une tâche de bout en bout.
|
|
52
|
+
FunctionRegistry['tsp.solve'] = Optimization.Operators.solveTSP;
|
|
53
|
+
FunctionRegistry['portfolio.solve'] = Optimization.Operators.solvePortfolio;
|
|
54
|
+
FunctionRegistry['fraud.solve'] = Optimization.Operators.solveFraudDetection; // NOUVEAU: Enregistrement du solveur de fraude
|
|
55
|
+
FunctionRegistry['facility.solve'] = Optimization.Operators.solveFacilityLocation;
|
|
56
|
+
FunctionRegistry['security.tune'] = Optimization.Operators.solveFullSecurityTuning;
|
|
57
|
+
|
|
58
|
+
// --- Fonctions "Utilitaires" ---
|
|
59
|
+
FunctionRegistry['utils.evaluatePathDistance'] = Optimization.Utils.evaluatePathDistance;
|
|
60
|
+
|
|
61
|
+
|
|
4
62
|
/**
|
|
5
63
|
* @namespace ProblemInitializers
|
|
6
64
|
* @description Fonctions pour générer dynamiquement les données d'un problème.
|
|
@@ -68,11 +126,18 @@ class ProblemManager {
|
|
|
68
126
|
const problems = JSON.parse(data);
|
|
69
127
|
// Initialisation dynamique des problèmes
|
|
70
128
|
for (const problem of problems) { // eslint-disable-line no-unused-vars
|
|
129
|
+
// Résolution des fonctions via le registre
|
|
130
|
+
if (problem.workUnit.scoreFunction) {
|
|
131
|
+
problem.workUnit.scoreFunction = FunctionRegistry[problem.workUnit.scoreFunction] || null;
|
|
132
|
+
}
|
|
133
|
+
|
|
71
134
|
for (const key in problem.payload) {
|
|
72
135
|
const value = problem.payload[key];
|
|
73
136
|
// On cherche une instruction d'initialisation (ex: { "$init": "generate:randomPoints", ... })
|
|
74
137
|
if (typeof value === 'object' && value !== null && value.$init) {
|
|
75
138
|
const initializer = ProblemInitializers[value.$init];
|
|
139
|
+
// On cherche une instruction de fonction (ex: { "$func": "tsp.calculateEnergy" })
|
|
140
|
+
// Note: Actuellement non utilisé, mais prêt pour une future extension.
|
|
76
141
|
if (initializer) {
|
|
77
142
|
// On remplace l'objet d'instruction par les données générées.
|
|
78
143
|
problem.payload[key] = initializer(value.params || {});
|
|
@@ -140,6 +205,18 @@ class ProblemManager {
|
|
|
140
205
|
task.logProgress = problem.payload.logProgress || false;
|
|
141
206
|
task.concurrency = problem.payload.concurrency;
|
|
142
207
|
break;
|
|
208
|
+
|
|
209
|
+
case 'multi_objective_genetic_algorithm':
|
|
210
|
+
// La difficulté s'applique au nombre de générations
|
|
211
|
+
const baseGenerationsMulti = Math.max(30, problem.workUnit.baseGenerations || 0);
|
|
212
|
+
task.generations = scalingFactor
|
|
213
|
+
? Math.floor(baseGenerationsMulti * Math.pow(scalingFactor, suspicionFactor))
|
|
214
|
+
: Math.floor(baseGenerationsMulti * (0.5 + suspicionFactor));
|
|
215
|
+
task.payload = problem.payload;
|
|
216
|
+
// L'état initial est le front de Pareto actuel, que le client peut utiliser pour l'élitisme
|
|
217
|
+
task.initialFront = problem.state.paretoFront;
|
|
218
|
+
task.solverName = problem.workUnit.solverName; // Le nom du solveur à utiliser (ex: 'cpc.solve')
|
|
219
|
+
break;
|
|
143
220
|
}
|
|
144
221
|
|
|
145
222
|
return { problemId: problem.id, task };
|
|
@@ -156,19 +233,190 @@ class ProblemManager {
|
|
|
156
233
|
|
|
157
234
|
switch (problem.workUnit.type) {
|
|
158
235
|
case 'simulated_annealing_iterations':
|
|
159
|
-
|
|
236
|
+
const currentBest = parseFloat(problem.state.bestEnergy) || Infinity;
|
|
237
|
+
const isBetter = problem.workUnit.objective === 'maximize'
|
|
238
|
+
? solutionData.energy > currentBest
|
|
239
|
+
: solutionData.energy < currentBest;
|
|
240
|
+
|
|
241
|
+
if (isBetter) {
|
|
160
242
|
problem.state.bestSolution = solutionData.solution;
|
|
161
243
|
problem.state.bestEnergy = solutionData.energy;
|
|
244
|
+
problem.state.lastUpdate = new Date().toISOString();
|
|
162
245
|
console.log(`[ProblemManager] Nouvelle meilleure solution pour ${problemId}: ${solutionData.energy.toFixed(2)}`);
|
|
163
246
|
}
|
|
164
247
|
break;
|
|
165
248
|
case 'genetic_algorithm_generations':
|
|
249
|
+
// Pour l'algo génétique, on pourrait comparer le meilleur fitness de la nouvelle population
|
|
166
250
|
problem.state.population = solutionData.population;
|
|
167
251
|
console.log(`[ProblemManager] Population mise à jour pour ${problemId}.`);
|
|
168
252
|
break;
|
|
253
|
+
|
|
254
|
+
case 'multi_objective_genetic_algorithm':
|
|
255
|
+
// Pour le multi-objectifs, on fusionne le front de Pareto existant avec celui du client.
|
|
256
|
+
this._integrateParetoFront(problem, solutionData.paretoFront);
|
|
257
|
+
break;
|
|
169
258
|
}
|
|
170
259
|
this.saveProblems();
|
|
171
260
|
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* S'assure qu'un problème a une solution initiale. Si non, en génère une.
|
|
264
|
+
* @param {object} problem - L'objet problème.
|
|
265
|
+
* @private
|
|
266
|
+
*/
|
|
267
|
+
_ensureInitialSolution(problem) {
|
|
268
|
+
if (problem.state.bestSolution) {
|
|
269
|
+
return; // Une solution existe déjà
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(`[ProblemManager] Génération d'une solution initiale pour le problème ${problem.id}...`);
|
|
273
|
+
|
|
274
|
+
// On utilise la fonction de score définie dans la config
|
|
275
|
+
const scoreFunction = problem.workUnit.scoreFunction;
|
|
276
|
+
// On suppose que la source de la solution initiale est définie dans la config
|
|
277
|
+
const initialSolutionSource = problem.payload[problem.workUnit.initialSolutionSource];
|
|
278
|
+
|
|
279
|
+
if (scoreFunction && initialSolutionSource && Array.isArray(initialSolutionSource)) {
|
|
280
|
+
const initialSolution = initialSolutionSource;
|
|
281
|
+
// On calcule le score (énergie, fitness, etc.) de cette solution initiale.
|
|
282
|
+
// La fonction de scoring peut nécessiter des arguments supplémentaires du payload.
|
|
283
|
+
const score = scoreFunction(initialSolution, problem.payload);
|
|
284
|
+
|
|
285
|
+
problem.state.bestSolution = initialSolution;
|
|
286
|
+
// Le nom de la propriété du score dépend du type de problème
|
|
287
|
+
problem.state.bestEnergy = score; // Pourrait être généralisé si besoin
|
|
288
|
+
problem.state.lastUpdate = new Date().toISOString();
|
|
289
|
+
|
|
290
|
+
console.log(`[ProblemManager] Solution initiale pour ${problem.id} générée avec un score de ${score.toFixed(2)}.`);
|
|
291
|
+
this.saveProblems(); // On sauvegarde la nouvelle solution
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Intègre un nouveau front de Pareto dans l'état du problème.
|
|
297
|
+
* @param {object} problem - L'objet problème.
|
|
298
|
+
* @param {Array<object>} newFront - Le front de Pareto renvoyé par un client.
|
|
299
|
+
* @private
|
|
300
|
+
*/
|
|
301
|
+
_integrateParetoFront(problem, newFront) {
|
|
302
|
+
if (!Array.isArray(newFront) || newFront.length === 0) return;
|
|
303
|
+
|
|
304
|
+
const currentFront = problem.state.paretoFront || [];
|
|
305
|
+
const combined = [...currentFront, ...newFront];
|
|
306
|
+
|
|
307
|
+
// --- Logique de tri non-dominé pour trouver le nouveau meilleur front ---
|
|
308
|
+
const paretoDominates = (a, b) => {
|
|
309
|
+
let aIsBetterInOne = false;
|
|
310
|
+
// On suppose que les objectifs sont à minimiser
|
|
311
|
+
for (let i = 0; i < a.objectives.length; i++) {
|
|
312
|
+
if (a.objectives[i] > b.objectives[i]) return false; // A est pire sur au moins un objectif
|
|
313
|
+
if (a.objectives[i] < b.objectives[i]) aIsBetterInOne = true; // A est strictement meilleur sur au moins un
|
|
314
|
+
}
|
|
315
|
+
return aIsBetterInOne;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const nextFront = [];
|
|
319
|
+
const dominatedIndices = new Set();
|
|
320
|
+
|
|
321
|
+
for (let i = 0; i < combined.length; i++) {
|
|
322
|
+
if (dominatedIndices.has(i)) continue;
|
|
323
|
+
let isDominated = false;
|
|
324
|
+
for (let j = 0; j < combined.length; j++) {
|
|
325
|
+
if (i === j || dominatedIndices.has(j)) continue;
|
|
326
|
+
if (paretoDominates(combined[j], combined[i])) {
|
|
327
|
+
isDominated = true;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
if (paretoDominates(combined[i], combined[j])) {
|
|
331
|
+
dominatedIndices.add(j);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!isDominated) {
|
|
335
|
+
nextFront.push(combined[i]);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (nextFront.length > currentFront.length || !problem.state.paretoFront) {
|
|
340
|
+
console.log(`[ProblemManager] Nouveau front de Pareto pour ${problem.id} avec ${nextFront.length} solutions (précédemment ${currentFront.length}).`);
|
|
341
|
+
problem.state.paretoFront = nextFront;
|
|
342
|
+
problem.state.lastUpdate = new Date().toISOString();
|
|
343
|
+
this.saveProblems();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Récupère la meilleure solution actuellement connue pour un ou plusieurs problèmes.
|
|
349
|
+
* @param {string} [problemId] - L'ID optionnel du problème à consulter.
|
|
350
|
+
* Si non fourni, retourne les meilleures solutions pour tous les problèmes.
|
|
351
|
+
* @returns {object|Array<object>|null}
|
|
352
|
+
* - Si un `problemId` est fourni, retourne un objet `{ id, solution, score }` ou `null` si non trouvé.
|
|
353
|
+
* - Si aucun `problemId` n'est fourni, retourne un tableau de ces objets.
|
|
354
|
+
*/
|
|
355
|
+
getBestSolutions(problemId) {
|
|
356
|
+
const problemsToProcess = problemId
|
|
357
|
+
? this.problems.filter(p => p.id === problemId)
|
|
358
|
+
: this.problems;
|
|
359
|
+
|
|
360
|
+
// On ne génère une solution initiale que pour les problèmes mono-objectif
|
|
361
|
+
problemsToProcess
|
|
362
|
+
.filter(p => p.workUnit.type !== 'multi_objective_genetic_algorithm')
|
|
363
|
+
.forEach(p => this._ensureInitialSolution(p));
|
|
364
|
+
|
|
365
|
+
const formatSolution = (p) => {
|
|
366
|
+
// Après _ensureInitialSolution, on peut supposer que p.state existe.
|
|
367
|
+
if (!p || !p.state) return null;
|
|
368
|
+
|
|
369
|
+
// Cas spécial pour les problèmes multi-objectifs
|
|
370
|
+
if (p.workUnit.type === 'multi_objective_genetic_algorithm') {
|
|
371
|
+
return {
|
|
372
|
+
id: p.id,
|
|
373
|
+
solution: p.state.paretoFront, // La "solution" est l'ensemble du front
|
|
374
|
+
score: p.state.paretoFront?.length || 0, // Le "score" est le nombre de points sur le front
|
|
375
|
+
lastUpdate: p.state.lastUpdate,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
id: p.id,
|
|
381
|
+
solution: p.state.bestSolution,
|
|
382
|
+
score: p.state.bestEnergy,
|
|
383
|
+
lastUpdate: p.state.lastUpdate,
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
if (problemId) {
|
|
388
|
+
const problem = this.problems.find(p => p.id === problemId);
|
|
389
|
+
return problem ? formatSolution(problem) : null; // Le filtrage initial a déjà fait le travail
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Retourne un aperçu pour tous les problèmes
|
|
393
|
+
return this.problems.map(formatSolution).filter(s => s && s.solution);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Met à jour le payload d'un problème spécifique par son ID.
|
|
398
|
+
* @param {string} problemId - L'ID du problème à mettre à jour.
|
|
399
|
+
* @param {object} newPayload - Le nouvel objet payload qui remplacera l'ancien.
|
|
400
|
+
* @returns {boolean} - True si la mise à jour a réussi, false sinon.
|
|
401
|
+
*/
|
|
402
|
+
updateProblemPayload(problemId, newPayload) {
|
|
403
|
+
const problem = this.problems.find(p => p.id === problemId);
|
|
404
|
+
if (!problem) {
|
|
405
|
+
console.error(`[ProblemManager] Impossible de mettre à jour : problème avec l'ID '${problemId}' non trouvé.`);
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
console.log(`[ProblemManager] Mise à jour du payload pour le problème '${problemId}'.`);
|
|
410
|
+
problem.payload = newPayload;
|
|
411
|
+
|
|
412
|
+
// Invalider l'état actuel car le problème a changé
|
|
413
|
+
problem.state.bestSolution = null;
|
|
414
|
+
problem.state.bestEnergy = "Infinity";
|
|
415
|
+
|
|
416
|
+
this.saveProblems();
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
|
|
172
420
|
}
|
|
173
421
|
|
|
174
422
|
export { ProblemManager }; // Export the class for testing
|
package/redis-store.js
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Creates a store adapter for ioredis.
|
|
3
|
-
* This adapter handles the serialization and deserialization of complex objects,
|
|
4
|
-
* including the conversion of Set objects to arrays for storage in Redis.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a store adapter for an ioredis client.
|
|
9
|
-
* @param {import('ioredis').Redis} redisClient - An instance of the ioredis client.
|
|
10
|
-
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
11
|
-
*/
|
|
12
|
-
export function createRedisStore(redisClient) {
|
|
13
|
-
return {
|
|
14
|
-
async get(key) {
|
|
15
|
-
const value = await redisClient.get(key);
|
|
16
|
-
if (!value) return null;
|
|
17
|
-
// Use a reviver to convert arrays back to Sets for specific keys like 'ips'.
|
|
18
|
-
return JSON.parse(value, (k, v) => {
|
|
19
|
-
if (k === 'ips' && Array.isArray(v)) {
|
|
20
|
-
return new Set(v);
|
|
21
|
-
}
|
|
22
|
-
return v;
|
|
23
|
-
});
|
|
24
|
-
},
|
|
25
|
-
async set(key, value, ttl) {
|
|
26
|
-
// Use a replacer to convert Set objects into arrays before serialization.
|
|
27
|
-
const stringValue = JSON.stringify(value, (k, v) => {
|
|
28
|
-
if (v instanceof Set) {
|
|
29
|
-
return Array.from(v);
|
|
30
|
-
}
|
|
31
|
-
return v;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
if (ttl && ttl > 0) {
|
|
35
|
-
await redisClient.set(key, stringValue, 'EX', ttl);
|
|
36
|
-
} else {
|
|
37
|
-
await redisClient.set(key, stringValue);
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
async has(key) { return (await redisClient.exists(key)) === 1; },
|
|
41
|
-
async delete(key) { await redisClient.del(key); },
|
|
42
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @file Creates a store adapter for ioredis.
|
|
3
|
+
* This adapter handles the serialization and deserialization of complex objects,
|
|
4
|
+
* including the conversion of Set objects to arrays for storage in Redis.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a store adapter for an ioredis client.
|
|
9
|
+
* @param {import('ioredis').Redis} redisClient - An instance of the ioredis client.
|
|
10
|
+
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
11
|
+
*/
|
|
12
|
+
export function createRedisStore(redisClient) {
|
|
13
|
+
return {
|
|
14
|
+
async get(key) {
|
|
15
|
+
const value = await redisClient.get(key);
|
|
16
|
+
if (!value) return null;
|
|
17
|
+
// Use a reviver to convert arrays back to Sets for specific keys like 'ips'.
|
|
18
|
+
return JSON.parse(value, (k, v) => {
|
|
19
|
+
if (k === 'ips' && Array.isArray(v)) {
|
|
20
|
+
return new Set(v);
|
|
21
|
+
}
|
|
22
|
+
return v;
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
async set(key, value, ttl) {
|
|
26
|
+
// Use a replacer to convert Set objects into arrays before serialization.
|
|
27
|
+
const stringValue = JSON.stringify(value, (k, v) => {
|
|
28
|
+
if (v instanceof Set) {
|
|
29
|
+
return Array.from(v);
|
|
30
|
+
}
|
|
31
|
+
return v;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (ttl && ttl > 0) {
|
|
35
|
+
await redisClient.set(key, stringValue, 'EX', ttl);
|
|
36
|
+
} else {
|
|
37
|
+
await redisClient.set(key, stringValue);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async has(key) { return (await redisClient.exists(key)) === 1; },
|
|
41
|
+
async delete(key) { await redisClient.del(key); },
|
|
42
|
+
};
|
|
43
43
|
}
|
package/sql-store.js
CHANGED
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Creates a store adapter for Knex.js.
|
|
3
|
-
* This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
|
|
4
|
-
* It handles serialization of complex objects and TTL for automatic data expiration.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a store adapter for a Knex.js client.
|
|
9
|
-
*
|
|
10
|
-
* **Note:** You must create the table yourself before using the store.
|
|
11
|
-
* The table should have at least the following columns:
|
|
12
|
-
* - `key` (string, primary key)
|
|
13
|
-
* - `value` (text or json/jsonb)
|
|
14
|
-
* - `expiresAt` (datetime or timestamp with time zone)
|
|
15
|
-
*
|
|
16
|
-
* Example schema for PostgreSQL:
|
|
17
|
-
* ```sql
|
|
18
|
-
* CREATE TABLE your_table_name (
|
|
19
|
-
* "key" VARCHAR(255) PRIMARY KEY,
|
|
20
|
-
* "value" TEXT NOT NULL,
|
|
21
|
-
* "expiresAt" TIMESTAMPTZ
|
|
22
|
-
* );
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* @param {import('knex').Knex} knex - An instance of the Knex client.
|
|
26
|
-
* @param {string} [tableName='fingerprint_store'] - The name of the table to use.
|
|
27
|
-
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
28
|
-
*/
|
|
29
|
-
export function createSqlStore(knex, tableName = 'fingerprint_store') {
|
|
30
|
-
// Custom replacer/reviver to handle Set serialization, similar to the Redis store.
|
|
31
|
-
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
32
|
-
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
async get(key) {
|
|
36
|
-
const row = await knex(tableName).where('key', key).first();
|
|
37
|
-
if (!row) return null;
|
|
38
|
-
|
|
39
|
-
// Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
|
|
40
|
-
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
41
|
-
await this.delete(key); // Clean up expired key.
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
return JSON.parse(row.value, reviver);
|
|
47
|
-
} catch (e) {
|
|
48
|
-
// In case of malformed JSON, treat it as a miss.
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
async set(key, value, ttl) {
|
|
54
|
-
const stringValue = JSON.stringify(value, replacer);
|
|
55
|
-
const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
|
|
56
|
-
|
|
57
|
-
// Use native upsert capabilities of Knex for different SQL dialects.
|
|
58
|
-
await knex(tableName)
|
|
59
|
-
.insert({ key, value: stringValue, expiresAt })
|
|
60
|
-
.onConflict('key')
|
|
61
|
-
.merge();
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
async has(key) {
|
|
65
|
-
const row = await knex(tableName).where('key', key).first('key');
|
|
66
|
-
if (!row) return false;
|
|
67
|
-
// Also check for expiration here.
|
|
68
|
-
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
return true;
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
async delete(key) {
|
|
75
|
-
await knex(tableName).where('key', key).del();
|
|
76
|
-
},
|
|
77
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @file Creates a store adapter for Knex.js.
|
|
3
|
+
* This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
|
|
4
|
+
* It handles serialization of complex objects and TTL for automatic data expiration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a store adapter for a Knex.js client.
|
|
9
|
+
*
|
|
10
|
+
* **Note:** You must create the table yourself before using the store.
|
|
11
|
+
* The table should have at least the following columns:
|
|
12
|
+
* - `key` (string, primary key)
|
|
13
|
+
* - `value` (text or json/jsonb)
|
|
14
|
+
* - `expiresAt` (datetime or timestamp with time zone)
|
|
15
|
+
*
|
|
16
|
+
* Example schema for PostgreSQL:
|
|
17
|
+
* ```sql
|
|
18
|
+
* CREATE TABLE your_table_name (
|
|
19
|
+
* "key" VARCHAR(255) PRIMARY KEY,
|
|
20
|
+
* "value" TEXT NOT NULL,
|
|
21
|
+
* "expiresAt" TIMESTAMPTZ
|
|
22
|
+
* );
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param {import('knex').Knex} knex - An instance of the Knex client.
|
|
26
|
+
* @param {string} [tableName='fingerprint_store'] - The name of the table to use.
|
|
27
|
+
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
28
|
+
*/
|
|
29
|
+
export function createSqlStore(knex, tableName = 'fingerprint_store') {
|
|
30
|
+
// Custom replacer/reviver to handle Set serialization, similar to the Redis store.
|
|
31
|
+
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
32
|
+
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
async get(key) {
|
|
36
|
+
const row = await knex(tableName).where('key', key).first();
|
|
37
|
+
if (!row) return null;
|
|
38
|
+
|
|
39
|
+
// Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
|
|
40
|
+
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
41
|
+
await this.delete(key); // Clean up expired key.
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(row.value, reviver);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// In case of malformed JSON, treat it as a miss.
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async set(key, value, ttl) {
|
|
54
|
+
const stringValue = JSON.stringify(value, replacer);
|
|
55
|
+
const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
|
|
56
|
+
|
|
57
|
+
// Use native upsert capabilities of Knex for different SQL dialects.
|
|
58
|
+
await knex(tableName)
|
|
59
|
+
.insert({ key, value: stringValue, expiresAt })
|
|
60
|
+
.onConflict('key')
|
|
61
|
+
.merge();
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async has(key) {
|
|
65
|
+
const row = await knex(tableName).where('key', key).first('key');
|
|
66
|
+
if (!row) return false;
|
|
67
|
+
// Also check for expiration here.
|
|
68
|
+
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async delete(key) {
|
|
75
|
+
await knex(tableName).where('key', key).del();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
78
|
}
|