data-primals-engine 1.7.1 → 1.7.2

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 (36) hide show
  1. package/README.md +160 -160
  2. package/client/package-lock.json +484 -175
  3. package/client/package.json +7 -4
  4. package/client/src/AssistantChat.jsx +1 -3
  5. package/client/src/DataLayout.jsx +19 -20
  6. package/client/src/DataTable.jsx +2 -2
  7. package/client/src/DocumentationPageLayout.scss +1 -1
  8. package/client/src/ViewSwitcher.jsx +1 -1
  9. package/client/vite.config.js +31 -30
  10. package/package.json +27 -17
  11. package/src/ai.jobs.js +135 -0
  12. package/src/constants.js +561 -545
  13. package/src/data.js +2 -0
  14. package/src/engine.js +50 -42
  15. package/src/modules/assistant/assistant.js +782 -763
  16. package/src/modules/assistant/constants.js +23 -16
  17. package/src/modules/assistant/providers.js +77 -37
  18. package/src/modules/bucket.js +4 -0
  19. package/src/modules/data/data.cluster.js +191 -0
  20. package/src/modules/data/data.core.js +11 -8
  21. package/src/modules/data/data.js +311 -311
  22. package/src/modules/data/data.operations.js +186 -106
  23. package/src/modules/data/data.relations.js +1 -0
  24. package/src/modules/data/data.replication.js +83 -0
  25. package/src/modules/data/data.routes.js +2183 -1879
  26. package/src/modules/mongodb.js +76 -73
  27. package/src/modules/user.js +7 -1
  28. package/src/modules/worker-script-runner.js +97 -0
  29. package/src/modules/workflow.js +177 -52
  30. package/src/packs.js +5701 -5701
  31. package/src/providers.js +298 -297
  32. package/test/assistant.test.js +207 -206
  33. package/test/data.integration.test.js +1425 -1416
  34. package/test/import_export.integration.test.js +210 -210
  35. package/test/workflow.actions.integration.test.js +487 -475
  36. package/test/workflow.integration.test.js +332 -329
@@ -1,17 +1,24 @@
1
-
2
- export const providers = {
3
- "OpenAI" : {
4
- key:"OPENAI_API_KEY",
5
- defaultModel: 'gpt-3.5-turbo'
6
- },
7
- "Google": {
8
- key:"GOOGLE_API_KEY"
9
- },
10
- "DeepSeek": {
11
- key: "DEEPSEEK_API_KEY",
12
- defaultModel: 'deepseek-chat'
13
- },
14
- "Anthropic": {
15
- key:"ANTHROPIC_API_KEY"
16
- }
1
+
2
+ export const providers = {
3
+ "OpenAI" : {
4
+ key:"OPENAI_API_KEY",
5
+ defaultModel: 'gpt-4o-mini',
6
+ generationModel: 'gpt-4o-mini' // Excellent for JSON generation
7
+ },
8
+ "Google": {
9
+ key:"GOOGLE_API_KEY",
10
+ defaultModel: 'gemini-1.5-flash',
11
+ generationModel: 'gemini-1.5-flash'
12
+ },
13
+ "DeepSeek": {
14
+ key: "DEEPSEEK_API_KEY",
15
+ defaultModel: 'deepseek-chat',
16
+ // As per the error message, 'deepseek-v2-coder' is a good candidate for code/JSON generation
17
+ generationModel: 'deepseek-coder'
18
+ },
19
+ "Anthropic": {
20
+ key:"ANTHROPIC_API_KEY",
21
+ defaultModel: 'claude-3-haiku-20240307',
22
+ generationModel: 'claude-3-haiku-20240307' // Haiku is fast and good with JSON
23
+ }
17
24
  };
@@ -1,38 +1,78 @@
1
- import { Config } from "../../config.js";
2
- import { assistantConfig } from "../../constants.js";
3
- import { Logger } from "../../gameObject.js";
4
-
5
- const logger = new Logger("AssistantProviders");
6
-
7
- export const getAIProvider = async (aiProvider, aiModel, apiKey) => {
8
- const maxTokens = Config.Get('assistant.maxTokens', assistantConfig.maxTokens);
9
- try {
10
- switch (aiProvider) {
11
- case 'OpenAI': {
12
- const { ChatOpenAI } = await import("@langchain/openai");
13
- return new ChatOpenAI({ apiKey, model: aiModel, temperature: 0.7, maxTokens });
14
- }
15
- case 'Google': {
16
- const { ChatGoogleGenerativeAI } = await import("@langchain/google-genai");
17
- return new ChatGoogleGenerativeAI({ apiKey, model: aiModel, temperature: 0.7, maxTokens });
18
- }
19
- case 'DeepSeek': {
20
- const { ChatDeepSeek } = await import("@langchain/deepseek");
21
- return new ChatDeepSeek({ apiKey, model: aiModel, temperature: 0.7, maxTokens });
22
- }
23
- case 'Anthropic': {
24
- const { ChatAnthropic } = await import("@langchain/anthropic");
25
- return new ChatAnthropic({ apiKey, model: aiModel, temperature: 0.7, maxTokens });
26
- }
27
- default:
28
- throw new Error(`Unsupported AI provider: ${aiProvider}`);
29
- }
30
- } catch (e) {
31
- if (e.code === 'ERR_MODULE_NOT_FOUND') {
32
- logger.error(`[Assistant] The package for the '${aiProvider}' provider is not installed. Please run 'npm install @langchain/${aiProvider.toLowerCase()}' to use this provider.`);
33
- throw new Error(`The AI provider '${aiProvider}' is not installed. Please ask the administrator to install the corresponding package.`);
34
- }
35
- logger.error(`[Assistant] Error initializing AI provider '${aiProvider}': ${e.message}`);
36
- throw e; // Re-throw other errors
37
- }
1
+ import { Config } from "../../config.js";
2
+ import {assistantConfig} from "../../constants.js";
3
+ import { Logger } from "../../gameObject.js";
4
+ import { getCollectionForUser } from "../mongodb.js";
5
+ import { providers } from "./constants.js";
6
+ import process from "node:process";
7
+
8
+ const logger = new Logger("AssistantProviders");
9
+
10
+ /**
11
+ * Finds the first available AI provider based on configured API keys for a given user.
12
+ * It checks the user's specific environment variables first, then falls back to the machine's process.env.
13
+ * @param {object} user - The user object.
14
+ * @param {string|null} [preferredProvider=null] - If specified, will only check for this provider.
15
+ * @returns {Promise<{provider: string, apiKey: string}|null>} An object with the provider name and API key, or null if none are found.
16
+ */
17
+ export async function findFirstAvailableProvider(user, preferredProvider = null) {
18
+ const envCollection = await getCollectionForUser(user);
19
+ const providerNames = preferredProvider ? [preferredProvider] : Object.keys(providers);
20
+
21
+ for (const pName of providerNames) {
22
+ const providerInfo = providers[pName];
23
+ if (!providerInfo) continue;
24
+
25
+ const envKeyName = providerInfo.key;
26
+ const userEnvVar = await envCollection.findOne({ _model: 'env', name: envKeyName, _user: user.username });
27
+ const key = userEnvVar?.value || process.env[envKeyName];
28
+
29
+ if (key) {
30
+ return { provider: pName, apiKey: key };
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+
36
+ export const getAIProvider = async (aiProvider, aiModel, apiKey, isJsonMode = false) => {
37
+ const maxTokens = Config.Get('assistant.maxTokens', assistantConfig.maxTokens);
38
+ const modelOptions = {
39
+ apiKey,
40
+ model: aiModel,
41
+ temperature: 0.7,
42
+ maxTokens
43
+ };
44
+
45
+ if (isJsonMode) {
46
+ modelOptions.response_format = { type: "json_object" };
47
+ }
48
+
49
+ try {
50
+ switch (aiProvider) {
51
+ case 'OpenAI': {
52
+ const { ChatOpenAI } = await import("@langchain/openai");
53
+ return new ChatOpenAI(modelOptions);
54
+ }
55
+ case 'Google': {
56
+ const { ChatGoogleGenerativeAI } = await import("@langchain/google-genai");
57
+ return new ChatGoogleGenerativeAI(modelOptions);
58
+ }
59
+ case 'DeepSeek': {
60
+ const { ChatDeepSeek } = await import("@langchain/deepseek");
61
+ return new ChatDeepSeek(modelOptions);
62
+ }
63
+ case 'Anthropic': {
64
+ const { ChatAnthropic } = await import("@langchain/anthropic");
65
+ return new ChatAnthropic(modelOptions);
66
+ }
67
+ default:
68
+ throw new Error(`Unsupported AI provider: ${aiProvider}`);
69
+ }
70
+ } catch (e) {
71
+ if (e.code === 'ERR_MODULE_NOT_FOUND') {
72
+ logger.error(`[Assistant] The package for the '${aiProvider}' provider is not installed. Please run 'npm install @langchain/${aiProvider.toLowerCase()}' to use this provider.`);
73
+ throw new Error(`The AI provider '${aiProvider}' is not installed. Please ask the administrator to install the corresponding package.`);
74
+ }
75
+ logger.error(`[Assistant] Error initializing AI provider '${aiProvider}': ${e.message}`);
76
+ throw e; // Re-throw other errors
77
+ }
38
78
  }
@@ -266,6 +266,10 @@ export async function onInit(defaultEngine) {
266
266
  engine.get('/api/backup/restore', [throttle, middlewareAuthenticator, userInitiator], async (req, res) => {
267
267
  const { token, username } = req.query;
268
268
 
269
+ if (!((req.me?.roles || []).includes("admin"))) {
270
+ return res.status(403).json({success: false, error: 'Cannot dump data.'})
271
+ }
272
+
269
273
  if (!token || !username) {
270
274
  return res.status(400).json({ error: 'Token and username are required.' });
271
275
  }
@@ -0,0 +1,191 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { clusterPeers, getHost, port } from '../../constants.js';
3
+ import { Config } from '../../config.js';
4
+ import { Logger } from '../../gameObject.js';
5
+
6
+ const logger = new Logger('DataCluster');
7
+
8
+ // La liste de tous les nœuds inclut le nœud actuel.
9
+ // On la trie pour s'assurer que l'ordre est le même sur toutes les instances.
10
+ const selfUrl = `http://${getHost()}:${process.env.PORT || port}`;
11
+ const allNodes = Array.from(new Set([selfUrl, ...clusterPeers])).sort();
12
+ const REPLICATION_FACTOR = Config.Get('replicationFactor', 2); // Maître + 1 réplique par défaut
13
+
14
+
15
+ logger.info(`[Cluster] Initialized. Self: ${selfUrl}. All nodes: ${allNodes.join(', ')}`);
16
+
17
+ /**
18
+ * Diffuse un événement d'invalidation de cache à tous les nœuds du cluster.
19
+ * @param {string} cacheType - Le type de cache à invalider (ex: 'model').
20
+ * @param {string} key - La clé spécifique à invalider dans le cache.
21
+ */
22
+ export async function broadcastCacheInvalidation(cacheType, key) {
23
+ if (clusterPeers.length === 0) {
24
+ return; // Pas de cluster, rien à faire.
25
+ }
26
+
27
+ logger.info(`[Cluster] Broadcasting direct cache invalidation for '${cacheType}' with key '${key}' to peers.`);
28
+
29
+ // On envoie une requête à chaque autre nœud du cluster.
30
+ const broadcastPromises = clusterPeers.map(peerUrl => {
31
+ const targetUrl = `${peerUrl}/api/internal/cache-invalidate`;
32
+ // On suppose que les nœuds partagent un token/secret pour s'authentifier.
33
+ // Le token de l'utilisateur actuel est une bonne option s'il est admin ou a des droits étendus.
34
+ const authToken = (process.env.INTERNAL_CLUSTER_TOKEN) ? `Bearer ${process.env.INTERNAL_CLUSTER_TOKEN}` : null;
35
+ if (!authToken) {
36
+ logger.warn(`[Cluster] INTERNAL_CLUSTER_TOKEN is not set. Cannot broadcast cache invalidation securely.`);
37
+ return Promise.resolve(); // Ne rien faire si pas de token
38
+ }
39
+
40
+ return fetch(targetUrl, {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ 'Authorization': authToken
45
+ },
46
+ body: JSON.stringify({ cacheType, key })
47
+ }).catch(error => {
48
+ logger.error(`[Cluster] Failed to send cache invalidation to peer ${peerUrl}: ${error.message}`);
49
+ });
50
+ });
51
+
52
+ await Promise.allSettled(broadcastPromises);
53
+ }
54
+
55
+ /**
56
+ * Détermine la liste ordonnée des nœuds responsables pour un utilisateur.
57
+ * Le premier est le maître, les suivants sont les répliques.
58
+ * @param {string} username - Le nom de l'utilisateur.
59
+ * @returns {string[]} Une liste d'URLs de nœuds.
60
+ */
61
+ export function getResponsibleNodesForUser(username) {
62
+ if (allNodes.length <= 1) {
63
+ return [selfUrl];
64
+ }
65
+
66
+ const numNodes = allNodes.length;
67
+ const numReplicas = Math.min(REPLICATION_FACTOR, numNodes);
68
+ const responsibleNodes = [];
69
+
70
+ const hash = createHash('sha256').update(username).digest('hex');
71
+ const startIndex = parseInt(hash.substring(0, 8), 16) % numNodes;
72
+
73
+ for (let i = 0; i < numReplicas; i++) {
74
+ responsibleNodes.push(allNodes[(startIndex + i) % numNodes]);
75
+ }
76
+
77
+ return responsibleNodes;
78
+ }
79
+
80
+ /**
81
+ * Détermine l'URL du nœud "maître" pour un utilisateur donné.
82
+ * @param {string} username - Le nom de l'utilisateur.
83
+ * @returns {string} L'URL du nœud maître.
84
+ */
85
+ export function getMasterNodeForUser(username) {
86
+ return getResponsibleNodesForUser(username)[0];
87
+ }
88
+
89
+ /**
90
+ * Vérifie si le nœud actuel est le maître pour cet utilisateur.
91
+ * @param {string} username - Le nom de l'utilisateur.
92
+ * @returns {boolean}
93
+ */
94
+ export function isSelfMasterForUser(username) {
95
+ return getMasterNodeForUser(username) === selfUrl;
96
+ }
97
+
98
+ /**
99
+ * Relaye une requête (lecture ou écriture) vers le nœud maître approprié de manière performante (streaming).
100
+ * @param {object} req - L'objet requête Express.
101
+ * @param {object} res - L'objet réponse Express.
102
+ * @param {string} username - Le nom de l'utilisateur concerné.
103
+ * @returns {Promise<boolean>} - Retourne `true` si la requête a été relayée, `false` sinon.
104
+ */
105
+ export async function proxyRequest(req, res, username) {
106
+ const responsibleNodes = getResponsibleNodesForUser(username);
107
+ const masterNodeUrl = responsibleNodes[0];
108
+
109
+ if (masterNodeUrl === selfUrl) {
110
+ // Ce nœud est le maître, on ne relaie pas.
111
+ return false;
112
+ }
113
+
114
+ // Pour les écritures (POST, PUT, PATCH, DELETE), on cible toujours le maître.
115
+ if (req.method !== 'GET') {
116
+ logger.info(`[Cluster] Proxying WRITE ${req.method} for user ${username} to master node ${masterNodeUrl}`);
117
+ await attemptProxy(req, res, masterNodeUrl);
118
+ return true; // La requête a été traitée (relayée ou a échoué).
119
+ }
120
+
121
+ // Pour les lectures (GET), on tente le maître, puis les répliques en cas d'échec.
122
+ logger.info(`[Cluster] Proxying READ for user ${username}. Node preference: ${responsibleNodes.join(' -> ')}`);
123
+ for (const nodeUrl of responsibleNodes) {
124
+ try {
125
+ // Si le nœud est nous-mêmes, on arrête le proxying et on laisse le handler local prendre le relais.
126
+ if (nodeUrl === selfUrl) {
127
+ logger.info(`[Cluster] Failover to self. Handling request locally.`);
128
+ return false;
129
+ }
130
+ await attemptProxy(req, res, nodeUrl);
131
+ return true; // Succès, on arrête la boucle de failover.
132
+ } catch (error) {
133
+ logger.warn(`[Cluster] Proxy attempt to ${nodeUrl} failed: ${error.message}. Trying next node...`);
134
+ }
135
+ }
136
+
137
+ // Si tous les nœuds ont échoué
138
+ logger.error(`[Cluster] All responsible nodes for user ${username} are down. Cannot serve request.`);
139
+ res.status(503).json({ success: false, error: `Service Unavailable: All responsible nodes for user ${username} are currently down.` });
140
+ return true; // La requête a été traitée (a échoué).
141
+ }
142
+
143
+ async function attemptProxy(req, res, nodeUrl) {
144
+ const targetUrl = new URL(req.originalUrl, nodeUrl);
145
+ const headers = { ...req.headers };
146
+ delete headers['host'];
147
+ headers['X-Federation-Proxy'] = 'true';
148
+ if (!headers['content-type']) {
149
+ headers['content-type'] = 'application/json';
150
+ }
151
+
152
+ // Utilisation d'un AbortController pour gérer les timeouts de connexion
153
+ const controller = new AbortController();
154
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // Timeout de 5 secondes
155
+
156
+ try {
157
+ const proxyResponse = await fetch(targetUrl.toString(), {
158
+ method: req.method,
159
+ headers: headers,
160
+ body: (req.method !== 'GET' && req.method !== 'HEAD') ? JSON.stringify(req.fields) : undefined,
161
+ signal: controller.signal
162
+ });
163
+
164
+ if (!proxyResponse.ok) {
165
+ throw new Error(`Node responded with status ${proxyResponse.status}`);
166
+ }
167
+
168
+ res.status(proxyResponse.status);
169
+ proxyResponse.headers.forEach((value, name) => {
170
+ // On évite de propager des en-têtes liés à la connexion interne du cluster.
171
+ if (name.toLowerCase() !== 'transfer-encoding' && name.toLowerCase() !== 'connection') {
172
+ res.setHeader(name, value);
173
+ }
174
+ });
175
+ const { Readable } = await import('node:stream');
176
+ Readable.fromWeb(proxyResponse.body).pipe(res);
177
+ } catch (error) {
178
+ throw error; // Relancer l'erreur pour que la boucle de failover puisse l'attraper.
179
+ } finally {
180
+ clearTimeout(timeoutId);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Vérifie si une requête a déjà été relayée pour éviter les boucles.
186
+ * @param {object} req - L'objet requête Express.
187
+ * @returns {boolean}
188
+ */
189
+ export function isProxiedRequest(req) {
190
+ return req.headers['x-federation-proxy'] === 'true';
191
+ }
@@ -1,12 +1,9 @@
1
1
  import NodeCache from "node-cache";
2
- import path from "node:path";
3
2
  import {Worker} from 'worker_threads';
4
- import {fileURLToPath} from "node:url";
3
+ import { fileURLToPath, URL } from "node:url";
4
+ import path from "node:path";
5
5
  export const modelsCache = new NodeCache( { stdTTL: 100, checkperiod: 120 } );
6
6
 
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
-
10
7
  /**
11
8
  * Helper to escape characters for regex.
12
9
  * @param {string} str The string to escape.
@@ -56,8 +53,12 @@ export let importJobs = {};
56
53
  */
57
54
  export function runImportExportWorker(action, payload) {
58
55
  return new Promise((resolve, reject) => {
59
- const workerPath = path.resolve(process.cwd(), './src/workers/import-export-worker.js');
60
- const worker = new Worker(workerPath);
56
+ // Construct a full URL to the worker script. This is the most robust way
57
+ // to ensure the worker is found, then convert it to a file path.
58
+ //const workerPath = path.resolve('src/workers/import-export-worker.js');
59
+ const workerPath = import.meta.dirname;
60
+
61
+ const worker = new Worker(path.resolve(workerPath,"../../workers/import-export-worker.js"));
61
62
 
62
63
  worker.postMessage({ action, payload });
63
64
 
@@ -91,7 +92,9 @@ export function runImportExportWorker(action, payload) {
91
92
  */
92
93
  export function runCryptoWorkerTask(action, payload) {
93
94
  return new Promise((resolve, reject) => {
94
- const workerPath = path.resolve(__dirname, '../../workers/crypto-worker.js');
95
+ // Construct a full URL to the worker script, then convert it to a file path
96
+ // for maximum reliability with the Worker constructor.
97
+ const workerPath = path.resolve('src/workers/crypto-worker.js');
95
98
  const worker = new Worker(workerPath);
96
99
 
97
100
  worker.postMessage({ action, payload });