@cocreate/lazy-loader 1.22.1 → 1.23.1

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/server.js CHANGED
@@ -1,592 +1,726 @@
1
- const fs = require('fs').promises;
2
- const path = require('path');
3
- const { URL } = require('url');
4
- const vm = require('vm');
1
+ const fs = require("fs").promises;
2
+ const path = require("path");
3
+ const { URL } = require("url");
4
+ const vm = require("vm");
5
5
  const Config = require("@cocreate/config");
6
- const { getValueFromObject } = require('@cocreate/utils');
7
-
6
+ const { getValueFromObject } = require("@cocreate/utils");
8
7
 
9
8
  class CoCreateLazyLoader {
10
- constructor(server, crud, files) {
11
- this.server = server
12
- this.wsManager = crud.wsManager
13
- this.crud = crud
14
- this.files = files
15
- this.exclusion = { ...require.cache };
16
- this.modules = {};
17
- this.init()
18
- }
19
-
20
- async init() {
21
- const scriptsDirectory = './scripts';
22
-
23
- try {
24
- await fs.mkdir(scriptsDirectory, { recursive: true });
25
- } catch (error) {
26
- console.error('Error creating scripts directory:', error);
27
- throw error; // Halt execution if directory creation fails
28
- }
29
-
30
- this.modules = await Config('modules', false, false)
31
- if (!this.modules)
32
- return
33
- else
34
- this.modules = this.modules.modules
35
-
36
- for (let name of Object.keys(this.modules)) {
37
- this.wsManager.on(this.modules[name].event, async (data) => {
38
- this.executeScriptWithTimeout(name, data)
39
- });
40
- }
41
-
42
- this.server.https.on('request', (req, res) => this.request(req, res))
43
- this.server.http.on('request', (req, res) => this.request(req, res))
44
-
45
- }
46
-
47
- async request(req, res) {
48
- try {
49
- // TODO: track usage
50
- const valideUrl = new URL(`http://${req.headers.host}${req.url}`);
51
- const hostname = valideUrl.hostname;
52
- let organization
53
-
54
- try {
55
- organization = await this.crud.getOrganization({ host: hostname });
56
- } catch {
57
- return this.files.send(req, res, this.crud, organization, valideUrl)
58
- }
59
-
60
- if (valideUrl.pathname.startsWith('/webhooks/')) {
61
- let name = req.url.split('/')[2]; // Assuming URL structure is /webhooks/name/...
62
- if (this.modules[name]) {
63
- this.executeScriptWithTimeout(name, { req, res, host: hostname, organization, valideUrl, organization_id: organization._id })
64
- } else {
65
- // Handle unknown module or missing webhook method
66
- res.writeHead(404, { 'Content-Type': 'application/json' });
67
- res.end(JSON.stringify({ error: 'Not found' }));
68
- }
69
-
70
- } else {
71
- this.files.send(req, res, this.crud, organization, valideUrl)
72
- }
73
-
74
- } catch (error) {
75
- res.writeHead(400, { 'Content-Type': 'text/plain' });
76
- res.end('Invalid host format');
77
- }
78
- }
79
-
80
- async executeScriptWithTimeout(name, data) {
81
- try {
82
- if (this.modules[name].initialize || this.modules[name].initialize === '') {
83
- if (data.req)
84
- data = await this.webhooks(this.modules[name], data, name)
85
- else
86
- data = await this.api(this.modules[name], data)
87
- } else {
88
- if (!this.modules[name].content) {
89
- if (this.modules[name].path)
90
- this.modules[name].content = await require(this.modules[name].path)
91
- else {
92
- try {
93
- const scriptPath = path.join(scriptsDirectory, `${name}.js`);
94
- await fs.access(scriptPath);
95
- this.modules[name].content = await fs.readFile(scriptPath, 'utf8');
96
- } catch {
97
- this.modules[name].content = await fetchScriptFromDatabaseAndSave(name, this.modules[name], data);
98
- }
99
- }
100
- }
101
-
102
- if (this.modules[name].content) {
103
- data.apis = await this.getApiKey(data, name)
104
- data.crud = this.crud
105
- data = await this.modules[name].content.send(data)
106
- delete data.apis
107
- delete data.crud
108
- } else
109
- return
110
- }
111
-
112
- if (data.socket)
113
- this.wsManager.send(data)
114
-
115
- if (this.modules[name].unload === false || this.modules[name].unload === 'false')
116
- return
117
- else if (this.modules[name].unload === true || this.modules[name].unload === 'true')
118
- console.log('config should unload after completeion ')
119
- else if (this.modules[name].unload = parseInt(this.modules[name].unload, 10)) {
120
- // Check if the script is already loaded
121
- if (this.modules[name].timeout) {
122
- clearTimeout(this.modules[name].timeout);
123
- } else if (!this.modules[name].path) {
124
- // Execute the script
125
- this.modules[name].context = new vm.createContext({});
126
- const script = new vm.Script(this.modules[name].context);
127
- script.runInContext(context);
128
- }
129
-
130
- // Reset or set the timeout
131
- const timeout = setTimeout(() => {
132
- // delete this.modules[name]
133
- delete this.modules[name].timeout
134
- delete this.modules[name].context
135
- delete this.modules[name].content
136
- console.log(`Module ${name} removed due to inactivity.`);
137
- clearModuleCache(name);
138
-
139
- }, this.modules[name].unload);
140
-
141
- this.modules[name].timeout = timeout
142
- }
143
- } catch (error) {
144
- data.error = error.message
145
- if (data.req) {
146
- data.res.writeHead(400, { 'Content-Type': 'text/plain' });
147
- data.res.end(`Lazyload Error: ${error.message}`);
148
- } if (data.socket)
149
- this.wsManager.send(data)
150
- }
151
- }
152
-
153
- /**
154
- * TODO: Implement Enhanced API Configuration Handling
155
- *
156
- * Description:
157
- * - Implement functionality to dynamically handle API configurations, supporting both complete and base URL endpoints with automatic method-based path appending.
158
- * - Enable dynamic generation of query parameters from a designated object (`stripe` in the examples) when `query` is true.
159
- *
160
- * Requirements:
161
- * 1. Dynamic Endpoint Handling:
162
- * - Check if the endpoint configuration is a complete URL or a base URL.
163
- * - If the `method` derived path is not already included in the endpoint, append it dynamically.
164
- * Example:
165
- * `{ "method": "stripe.accounts.retrieve", "endpoint": "https://api.stripe.com", "query": true, "stripe": { "acct": "acct_123", "name": "John Doe" } }`
166
- * `{ "method": "stripe.accounts.retrieve", "endpoint": "https://api.stripe.com/accounts/retrieve", "query": true, "stripe": { "acct": "acct_123", "name": "John Doe" } }`
167
- * - Develop logic to parse the `method` and check against the endpoint. If necessary, append the appropriate API method segment.
168
- *
169
- * 2. Query Parameter Handling:
170
- * - Dynamically construct and append query parameters from the `stripe` object if `query` is true. Ensure proper URL-encoding of keys and values.
171
- *
172
- * 3. Security:
173
- * - Use the `method` for permission checks, ensuring that each API request complies with security protocols.
174
- *
175
- * 4. Testing:
176
- * - Test both scenarios where the endpoint may or may not include the method path to ensure the dynamic construction works correctly.
177
- * - Ensure that all query parameters are correctly formatted and appended.
178
- *
179
- * Notes:
180
- * - Consider utility functions for parsing and modifying URLs, as well as for encoding parameters.
181
- * - Maintain clear and detailed documentation for each part of the implementation to assist future development and troubleshooting.
182
- */
183
-
184
- async api(config, data) {
185
- try {
186
- const methodPath = data.method.split('.')
187
- const name = methodPath.shift()
188
-
189
- const apis = await this.getApiKey(data, name)
190
-
191
- const key = apis.key;
192
- if (!key)
193
- throw new Error(`Missing ${name} key in organization apis object`);
194
-
195
- // ToDo: if data.endpoint service not required as endpoint will be used
196
- let instance;
197
-
198
- // Try using require() first, for CommonJS modules
199
- try {
200
- instance = require(config.path); // Attempt to require the module
201
- } catch (err) {
202
- if (err.code === 'ERR_REQUIRE_ESM') {
203
- // If it's an ESM module, fallback to dynamic import()
204
- instance = await import(config.path);
205
- } else {
206
- throw err; // Re-throw other errors
207
- }
208
- }
209
-
210
- if (config.initialize) {
211
- if (Array.isArray(config.initialize)) {
212
- const initializations = []
213
- for (let i = 0; i < config.initialize.length; i++) {
214
- const initialize = config.initialize[i].split('.');
215
- initializations.push(instance)
216
- // Traverse the nested structure to reach the correct constructor
217
- for (let j = 0; j < initialize.length; j++) {
218
- if (initializations[i][initialize[j]]) {
219
- initializations[i] = initializations[i][initialize[j]];
220
- } else {
221
- throw new Error(`Service path ${config.initialize[i]} is incorrect at ${initialize[j]}`);
222
- }
223
- }
224
- }
225
- instance = new initializations[1](new initializations[0](key));
226
- } else {
227
- const initialize = config.initialize.split('.');
228
- // Traverse the nested structure to reach the correct constructor
229
- for (let i = 0; i < initialize.length; i++) {
230
- if (instance[initialize[i]]) {
231
- instance = instance[initialize[i]];
232
- } else {
233
- throw new Error(`Service path ${config.initialize} is incorrect at ${initialize[i]}`);
234
- }
235
- }
236
- }
237
- instance = new instance(key);
238
- } else
239
- instance = new instance(key);
240
-
241
- let params = [], mainParam = false
242
- for (let i = 0; true; i++) {
243
- if (`$param[${i}]` in data[name]) {
244
- params.push(data[name][`$param[${i}]`])
245
- delete data[name][`$param[${i}]`]
246
- } else if (!mainParam) {
247
- params.push(data[name])
248
- mainParam = true
249
- } else {
250
- break;
251
- }
252
- }
253
-
254
- // TODO: should run processOperators before in order to perform complex opertions and get data
255
- // data[name] = await processOperators(data, null, data[name]);
256
-
257
- data[name] = await executeMethod(data.method, methodPath, instance, params)
258
-
259
- // TODO: should run processOperators after in order to perform complex opertions and get data
260
- // data[name] = await processOperators(data, data[name]);
261
-
262
- return data
263
- } catch (error) {
264
- data.error = error.message
265
- return data
266
- }
267
- }
268
-
269
- async webhooks(config, data, name) {
270
- try {
271
- const apis = await this.getApiKey(data, name)
272
-
273
- const key = apis.key;
274
- if (!key)
275
- throw new Error(`Missing ${name} key in organization apis object`);
276
-
277
- let webhookName = data.req.url.split('/');
278
- webhookName = webhookName[webhookName.length - 1]
279
-
280
- const webhook = apis.webhooks[webhookName];
281
- if (!webhook)
282
- throw new Error(`Webhook ${name} ${webhookName} is not defined`);
283
-
284
- // eventDataKey is used to access the event data
285
- let eventDataKey = webhook.eventDataKey || apis.eventDataKey
286
- if (!eventDataKey)
287
- throw new Error(`Webhook ${name} eventKey is not defined`);
288
-
289
- // eventNameKey is used to access the event the event name
290
- let eventNameKey = webhook.eventNameKey || apis.eventNameKey
291
- if (!eventNameKey)
292
- throw new Error(`Webhook ${name} eventNameKey is not defined`);
293
-
294
- if (!webhook.events)
295
- throw new Error(`Webhook ${name} events are not defined`);
296
-
297
- data.rawBody = '';
298
- await new Promise((resolve, reject) => {
299
- data.req.on('data', chunk => {
300
- data.rawBody += chunk.toString();
301
- });
302
- data.req.on('end', () => {
303
- resolve();
304
- });
305
- data.req.on('error', (err) => {
306
- reject(err);
307
- });
308
- });
309
-
310
- let parameters, method
311
-
312
-
313
- if (webhook.authenticate && webhook.authenticate.method) {
314
- method = webhook.authenticate.method
315
- } else if (apis.authenticate && apis.authenticate.method) {
316
- method = apis.authenticate.method
317
- } else
318
- throw new Error(`Webhook ${name} authenticate method is not defined`);
319
-
320
- if (webhook.authenticate && webhook.authenticate.parameters) {
321
- parameters = webhook.authenticate.parameters
322
- } else if (apis.authenticate && apis.authenticate.parameters) {
323
- parameters = apis.authenticate.parameters
324
- } else
325
- throw new Error(`Webhook ${name} authenticate parameters is not defined`);
326
-
327
- // TODO: webhook secert could be a key pair
328
-
329
- let event
330
- if (!method) {
331
- if (!parameters[0] !== parameters[1])
332
- throw new Error(`Webhook secret failed for ${name}. Unauthorized access attempt.`);
333
-
334
- event = JSON.parse(data.rawBody)
335
- } else {
336
- const service = require(config.path);
337
- let instance
338
- if (config.initialize)
339
- instance = new service[config.initialize](key);
340
- else
341
- instance = new service(key);
342
-
343
- const methodPath = method.split('.')
344
-
345
- await this.processOperators(data, '', parameters);
346
-
347
- event = await executeMethod(method, methodPath, instance, parameters)
348
- }
349
-
350
- let eventName = getValueFromObject(event, eventNameKey)
351
- if (!eventName)
352
- throw new Error(`Webhook ${name} eventNameKey: ${eventNameKey} could not be found in the event.`);
353
-
354
- let eventData = getValueFromObject(event, eventDataKey)
355
- if (!eventData)
356
- throw new Error(`Webhook ${name} eventDataKey: ${eventDataKey} could not be found in the event.`);
357
-
358
- let execute = webhook.events[eventName];
359
- if (execute) {
360
- execute = await this.processOperators(data, event, execute);
361
- }
362
-
363
- data.res.writeHead(200, { 'Content-Type': 'application/json' });
364
- data.res.end(JSON.stringify({ message: 'Webhook received and processed' }));
365
- return data
366
- } catch (error) {
367
- data.error = error.message
368
- data.res.writeHead(400, { 'Content-Type': 'text/plain' });
369
- data.res.end(error.message);
370
- return data
371
- }
372
- }
373
-
374
- async processOperators(data, event, execute) {
375
- if (Array.isArray(execute)) {
376
- for (let index = 0; index < execute.length; index++) {
377
- execute[index] = await this.processOperators(data, event, execute[index]);
378
- }
379
- } else if (typeof execute === 'object' && execute !== null) {
380
- for (let key of Object.keys(execute)) {
381
- if (key.startsWith('$') && !['$storage', '$database', '$array', '$filter'].includes(key)) {
382
- execute[key] = await this.processOperator(data, event, key, execute[key]);
383
- } else if (typeof execute[key] === 'string' && execute[key].startsWith('$') && !['$storage', '$database', '$array', '$filter'].includes(execute[key])) {
384
- execute[key] = await this.processOperator(data, event, execute[key]);
385
- } else if (Array.isArray(execute[key])) {
386
- execute[key] = await this.processOperators(data, event, execute[key]);
387
- } else if (typeof execute[key] === 'object' && execute[key] !== null) {
388
- execute[key] = await this.processOperators(data, event, execute[key]);
389
- }
390
- }
391
- } else if (typeof execute === 'string' && execute.startsWith('$') && !['$storage', '$database', '$array', '$filter'].includes(execute)) {
392
- execute = await this.processOperator(data, event, execute);
393
- }
394
-
395
- return execute;
396
- }
397
-
398
- async processOperator(data, event, operator, context) {
399
- let result
400
- if (operator.startsWith('$data.')) {
401
- result = getValueFromObject(data, operator.substring(6))
402
- return getValueFromObject(data, operator.substring(6))
403
- } else if (operator.startsWith('$req')) {
404
- return getValueFromObject(data, operator.substring(1))
405
- } else if (operator.startsWith('$header')) {
406
- return getValueFromObject(data.req, operator.substring(1))
407
- } else if (operator.startsWith('$rawBody')) {
408
- return getValueFromObject(data, operator.substring(1))
409
- } else if (operator.startsWith('$crud')) {
410
- let results = context
411
- let isObject = false
412
- if (!Array.isArray(results)) {
413
- isObject = true
414
- results = [results]
415
- }
416
-
417
- for (let i = 0; i < results.length; i++) {
418
- results[i] = await this.processOperators(data, event, results[i]);
419
- results[i] = await this.crud.send(results[i])
420
- if (operator.startsWith('$crud.'))
421
- results[i] = getValueFromObject(operator, operator.substring(6))
422
- results[i] = await this.processOperators(data, event, results[i])
423
- }
424
-
425
- if (isObject)
426
- results = results[0]
427
-
428
- return results;
429
- } else if (operator.startsWith('$socket')) {
430
- context = await this.processOperators(data, event, context);
431
- result = await this.socket.send(context)
432
- if (operator.startsWith('$socket.'))
433
- result = getValueFromObject(operator, operator.substring(6))
434
- return await this.processOperators(data, event, result);
435
- } else if (operator.startsWith('$api')) {
436
- context = await this.processOperators(data, event, context);
437
- let name = context.method.split('.')[0]
438
- result = this.executeScriptWithTimeout(name, context)
439
- if (operator.startsWith('$api.'))
440
- result = getValueFromObject(event, operator.substring(5))
441
- return await this.processOperators(data, event, result);
442
- } else if (operator.startsWith('$event')) {
443
- if (operator.startsWith('$event.'))
444
- result = getValueFromObject(event, operator.substring(7))
445
- return await this.processOperators(data, event, result);
446
- }
447
-
448
- return operator;
449
- }
450
-
451
- async getApiKey(data, name) {
452
- let organization = await this.crud.getOrganization(data);
453
- if (organization.error)
454
- throw new Error(organization.error);
455
- if (!organization.apis)
456
- throw new Error('Missing apis object in organization object');
457
- if (!organization.apis[name])
458
- throw new Error(`Missing ${name} in organization apis object`);
459
- return organization.apis[name]
460
- }
461
-
9
+ constructor(server, crud, files) {
10
+ this.server = server;
11
+ this.wsManager = crud.wsManager;
12
+ this.crud = crud;
13
+ this.files = files;
14
+ this.exclusion = { ...require.cache };
15
+ this.modules = {};
16
+ this.init();
17
+ }
18
+
19
+ async init() {
20
+ const scriptsDirectory = "./scripts";
21
+
22
+ try {
23
+ await fs.mkdir(scriptsDirectory, { recursive: true });
24
+ } catch (error) {
25
+ console.error("Error creating scripts directory:", error);
26
+ throw error; // Halt execution if directory creation fails
27
+ }
28
+
29
+ this.modules = await Config("modules", false, false);
30
+ if (!this.modules) return;
31
+ else this.modules = this.modules.modules;
32
+
33
+ for (let name of Object.keys(this.modules)) {
34
+ this.wsManager.on(this.modules[name].event, async (data) => {
35
+ this.executeScriptWithTimeout(name, data);
36
+ });
37
+ }
38
+
39
+ this.server.https.on("request", (req, res) => this.request(req, res));
40
+ this.server.http.on("request", (req, res) => this.request(req, res));
41
+ }
42
+
43
+ async request(req, res) {
44
+ try {
45
+ // TODO: track usage
46
+ const valideUrl = new URL(`http://${req.headers.host}${req.url}`);
47
+ const hostname = valideUrl.hostname;
48
+ let organization;
49
+
50
+ try {
51
+ organization = await this.crud.getOrganization({
52
+ host: hostname
53
+ });
54
+ } catch {
55
+ return this.files.send(
56
+ req,
57
+ res,
58
+ this.crud,
59
+ organization,
60
+ valideUrl
61
+ );
62
+ }
63
+
64
+ if (valideUrl.pathname.startsWith("/webhooks/")) {
65
+ let name = req.url.split("/")[2]; // Assuming URL structure is /webhooks/name/...
66
+ if (this.modules[name]) {
67
+ this.executeScriptWithTimeout(name, {
68
+ req,
69
+ res,
70
+ host: hostname,
71
+ organization,
72
+ valideUrl,
73
+ organization_id: organization._id
74
+ });
75
+ } else {
76
+ // Handle unknown module or missing webhook method
77
+ res.writeHead(404, { "Content-Type": "application/json" });
78
+ res.end(JSON.stringify({ error: "Not found" }));
79
+ }
80
+ } else {
81
+ this.files.send(req, res, this.crud, organization, valideUrl);
82
+ }
83
+ } catch (error) {
84
+ res.writeHead(400, { "Content-Type": "text/plain" });
85
+ res.end("Invalid host format");
86
+ }
87
+ }
88
+
89
+ async executeScriptWithTimeout(name, data) {
90
+ try {
91
+ if (
92
+ this.modules[name].initialize ||
93
+ this.modules[name].initialize === ""
94
+ ) {
95
+ if (data.req)
96
+ data = await this.webhooks(this.modules[name], data, name);
97
+ else data = await this.api(this.modules[name], data);
98
+ } else {
99
+ if (!this.modules[name].content) {
100
+ if (this.modules[name].path)
101
+ this.modules[name].content = await require(this.modules[
102
+ name
103
+ ].path);
104
+ else {
105
+ try {
106
+ const scriptPath = path.join(
107
+ scriptsDirectory,
108
+ `${name}.js`
109
+ );
110
+ await fs.access(scriptPath);
111
+ this.modules[name].content = await fs.readFile(
112
+ scriptPath,
113
+ "utf8"
114
+ );
115
+ } catch {
116
+ this.modules[name].content =
117
+ await fetchScriptFromDatabaseAndSave(
118
+ name,
119
+ this.modules[name],
120
+ data
121
+ );
122
+ }
123
+ }
124
+ }
125
+
126
+ if (this.modules[name].content) {
127
+ data.apis = await this.getApiKey(data, name);
128
+ data.crud = this.crud;
129
+ data = await this.modules[name].content.send(data);
130
+ delete data.apis;
131
+ delete data.crud;
132
+ } else return;
133
+ }
134
+
135
+ if (data.socket) this.wsManager.send(data);
136
+
137
+ if (
138
+ this.modules[name].unload === false ||
139
+ this.modules[name].unload === "false"
140
+ )
141
+ return;
142
+ else if (
143
+ this.modules[name].unload === true ||
144
+ this.modules[name].unload === "true"
145
+ )
146
+ console.log("config should unload after completeion ");
147
+ else if (
148
+ (this.modules[name].unload = parseInt(
149
+ this.modules[name].unload,
150
+ 10
151
+ ))
152
+ ) {
153
+ // Check if the script is already loaded
154
+ if (this.modules[name].timeout) {
155
+ clearTimeout(this.modules[name].timeout);
156
+ } else if (!this.modules[name].path) {
157
+ // Execute the script
158
+ this.modules[name].context = new vm.createContext({});
159
+ const script = new vm.Script(this.modules[name].context);
160
+ script.runInContext(context);
161
+ }
162
+
163
+ // Reset or set the timeout
164
+ const timeout = setTimeout(() => {
165
+ // delete this.modules[name]
166
+ delete this.modules[name].timeout;
167
+ delete this.modules[name].context;
168
+ delete this.modules[name].content;
169
+ console.log(`Module ${name} removed due to inactivity.`);
170
+ clearModuleCache(name);
171
+ }, this.modules[name].unload);
172
+
173
+ this.modules[name].timeout = timeout;
174
+ }
175
+ } catch (error) {
176
+ data.error = error.message;
177
+ if (data.req) {
178
+ data.res.writeHead(400, { "Content-Type": "text/plain" });
179
+ data.res.end(`Lazyload Error: ${error.message}`);
180
+ }
181
+ if (data.socket) this.wsManager.send(data);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * TODO: Implement Enhanced API Configuration Handling
187
+ *
188
+ * Description:
189
+ * - Implement functionality to dynamically handle API configurations, supporting both complete and base URL endpoints with automatic method-based path appending.
190
+ * - Enable dynamic generation of query parameters from a designated object (`stripe` in the examples) when `query` is true.
191
+ *
192
+ * Requirements:
193
+ * 1. Dynamic Endpoint Handling:
194
+ * - Check if the endpoint configuration is a complete URL or a base URL.
195
+ * - If the `method` derived path is not already included in the endpoint, append it dynamically.
196
+ * Example:
197
+ * `{ "method": "stripe.accounts.retrieve", "endpoint": "https://api.stripe.com", "query": true, "stripe": { "acct": "acct_123", "name": "John Doe" } }`
198
+ * `{ "method": "stripe.accounts.retrieve", "endpoint": "https://api.stripe.com/accounts/retrieve", "query": true, "stripe": { "acct": "acct_123", "name": "John Doe" } }`
199
+ * - Develop logic to parse the `method` and check against the endpoint. If necessary, append the appropriate API method segment.
200
+ *
201
+ * 2. Query Parameter Handling:
202
+ * - Dynamically construct and append query parameters from the `stripe` object if `query` is true. Ensure proper URL-encoding of keys and values.
203
+ *
204
+ * 3. Security:
205
+ * - Use the `method` for permission checks, ensuring that each API request complies with security protocols.
206
+ *
207
+ * 4. Testing:
208
+ * - Test both scenarios where the endpoint may or may not include the method path to ensure the dynamic construction works correctly.
209
+ * - Ensure that all query parameters are correctly formatted and appended.
210
+ *
211
+ * Notes:
212
+ * - Consider utility functions for parsing and modifying URLs, as well as for encoding parameters.
213
+ * - Maintain clear and detailed documentation for each part of the implementation to assist future development and troubleshooting.
214
+ */
215
+
216
+ async api(config, data) {
217
+ try {
218
+ const methodPath = data.method.split(".");
219
+ const name = methodPath.shift();
220
+
221
+ const apis = await this.getApiKey(data, name);
222
+
223
+ const key = apis.key;
224
+ if (!key)
225
+ throw new Error(
226
+ `Missing ${name} key in organization apis object`
227
+ );
228
+
229
+ // ToDo: if data.endpoint service not required as endpoint will be used
230
+ let instance;
231
+
232
+ // Try using require() first, for CommonJS modules
233
+ try {
234
+ instance = require(config.path); // Attempt to require the module
235
+ } catch (err) {
236
+ if (err.code === "ERR_REQUIRE_ESM") {
237
+ // If it's an ESM module, fallback to dynamic import()
238
+ instance = await import(config.path);
239
+ } else {
240
+ throw err; // Re-throw other errors
241
+ }
242
+ }
243
+
244
+ if (config.initialize) {
245
+ if (Array.isArray(config.initialize)) {
246
+ const initializations = [];
247
+ for (let i = 0; i < config.initialize.length; i++) {
248
+ const initialize = config.initialize[i].split(".");
249
+ initializations.push(instance);
250
+ // Traverse the nested structure to reach the correct constructor
251
+ for (let j = 0; j < initialize.length; j++) {
252
+ if (initializations[i][initialize[j]]) {
253
+ initializations[i] =
254
+ initializations[i][initialize[j]];
255
+ } else {
256
+ throw new Error(
257
+ `Service path ${config.initialize[i]} is incorrect at ${initialize[j]}`
258
+ );
259
+ }
260
+ }
261
+ }
262
+ instance = new initializations[1](
263
+ new initializations[0](key)
264
+ );
265
+ } else {
266
+ const initialize = config.initialize.split(".");
267
+ // Traverse the nested structure to reach the correct constructor
268
+ for (let i = 0; i < initialize.length; i++) {
269
+ if (instance[initialize[i]]) {
270
+ instance = instance[initialize[i]];
271
+ } else {
272
+ throw new Error(
273
+ `Service path ${config.initialize} is incorrect at ${initialize[i]}`
274
+ );
275
+ }
276
+ }
277
+ }
278
+ // instance = new instance(key);
279
+ }
280
+ // else
281
+ instance = new instance(key);
282
+
283
+ let params = [],
284
+ mainParam = false;
285
+ for (let i = 0; true; i++) {
286
+ if (`$param[${i}]` in data[name]) {
287
+ params.push(data[name][`$param[${i}]`]);
288
+ delete data[name][`$param[${i}]`];
289
+ } else if (!mainParam) {
290
+ params.push(data[name]);
291
+ mainParam = true;
292
+ } else {
293
+ break;
294
+ }
295
+ }
296
+
297
+ // TODO: should run processOperators before in order to perform complex opertions and get data, will need to loop back on permission in order to authenticate and autorize
298
+ // data[name] = await processOperators(data, null, data[name]);
299
+
300
+ // data[name] = await processOperators(data, null, data[name]);
301
+
302
+ // execute = await this.processOperators(data, event, execute);
303
+
304
+ data[name] = await executeMethod(
305
+ data.method,
306
+ methodPath,
307
+ instance,
308
+ params
309
+ );
310
+
311
+ // TODO: should run processOperators after in order to perform complex opertions and get data
312
+ // data[name] = await processOperators(data, data[name]);
313
+
314
+ return data;
315
+ } catch (error) {
316
+ data.error = error.message;
317
+ return data;
318
+ }
319
+ }
320
+
321
+ async webhooks(config, data, name) {
322
+ try {
323
+ const apis = await this.getApiKey(data, name);
324
+
325
+ const key = apis.key;
326
+ if (!key)
327
+ throw new Error(
328
+ `Missing ${name} key in organization apis object`
329
+ );
330
+
331
+ let webhookName = data.req.url.split("/");
332
+ webhookName = webhookName[webhookName.length - 1];
333
+
334
+ const webhook = apis.webhooks[webhookName];
335
+ if (!webhook)
336
+ throw new Error(
337
+ `Webhook ${name} ${webhookName} is not defined`
338
+ );
339
+
340
+ // eventDataKey is used to access the event data
341
+ let eventDataKey = webhook.eventDataKey || apis.eventDataKey;
342
+ if (!eventDataKey)
343
+ throw new Error(`Webhook ${name} eventKey is not defined`);
344
+
345
+ // eventNameKey is used to access the event the event name
346
+ let eventNameKey = webhook.eventNameKey || apis.eventNameKey;
347
+ if (!eventNameKey)
348
+ throw new Error(`Webhook ${name} eventNameKey is not defined`);
349
+
350
+ if (!webhook.events)
351
+ throw new Error(`Webhook ${name} events are not defined`);
352
+
353
+ data.rawBody = "";
354
+ await new Promise((resolve, reject) => {
355
+ data.req.on("data", (chunk) => {
356
+ data.rawBody += chunk.toString();
357
+ });
358
+ data.req.on("end", () => {
359
+ resolve();
360
+ });
361
+ data.req.on("error", (err) => {
362
+ reject(err);
363
+ });
364
+ });
365
+
366
+ let parameters, method;
367
+
368
+ if (webhook.authenticate && webhook.authenticate.method) {
369
+ method = webhook.authenticate.method;
370
+ } else if (apis.authenticate && apis.authenticate.method) {
371
+ method = apis.authenticate.method;
372
+ } else
373
+ throw new Error(
374
+ `Webhook ${name} authenticate method is not defined`
375
+ );
376
+
377
+ if (webhook.authenticate && webhook.authenticate.parameters) {
378
+ parameters = webhook.authenticate.parameters;
379
+ } else if (apis.authenticate && apis.authenticate.parameters) {
380
+ parameters = apis.authenticate.parameters;
381
+ } else
382
+ throw new Error(
383
+ `Webhook ${name} authenticate parameters is not defined`
384
+ );
385
+
386
+ // TODO: webhook secert could be a key pair
387
+
388
+ let event;
389
+ if (!method) {
390
+ if (!parameters[0] !== parameters[1])
391
+ throw new Error(
392
+ `Webhook secret failed for ${name}. Unauthorized access attempt.`
393
+ );
394
+
395
+ event = JSON.parse(data.rawBody);
396
+ } else {
397
+ const service = require(config.path);
398
+ let instance;
399
+ if (config.initialize)
400
+ instance = new service[config.initialize](key);
401
+ else instance = new service(key);
402
+
403
+ const methodPath = method.split(".");
404
+
405
+ await this.processOperators(data, "", parameters);
406
+
407
+ event = await executeMethod(
408
+ method,
409
+ methodPath,
410
+ instance,
411
+ parameters
412
+ );
413
+ }
414
+
415
+ let eventName = getValueFromObject(event, eventNameKey);
416
+ if (!eventName)
417
+ throw new Error(
418
+ `Webhook ${name} eventNameKey: ${eventNameKey} could not be found in the event.`
419
+ );
420
+
421
+ let eventData = getValueFromObject(event, eventDataKey);
422
+ if (!eventData)
423
+ throw new Error(
424
+ `Webhook ${name} eventDataKey: ${eventDataKey} could not be found in the event.`
425
+ );
426
+
427
+ let execute = webhook.events[eventName];
428
+ if (execute) {
429
+ execute = await this.processOperators(data, event, execute);
430
+ }
431
+
432
+ data.res.writeHead(200, { "Content-Type": "application/json" });
433
+ data.res.end(
434
+ JSON.stringify({ message: "Webhook received and processed" })
435
+ );
436
+ return data;
437
+ } catch (error) {
438
+ data.error = error.message;
439
+ data.res.writeHead(400, { "Content-Type": "text/plain" });
440
+ data.res.end(error.message);
441
+ return data;
442
+ }
443
+ }
444
+
445
+ async processOperators(data, event, execute) {
446
+ if (Array.isArray(execute)) {
447
+ for (let index = 0; index < execute.length; index++) {
448
+ execute[index] = await this.processOperators(
449
+ data,
450
+ event,
451
+ execute[index]
452
+ );
453
+ }
454
+ } else if (typeof execute === "object" && execute !== null) {
455
+ for (let key of Object.keys(execute)) {
456
+ if (
457
+ key.startsWith("$") &&
458
+ !["$storage", "$database", "$array", "$filter"].includes(
459
+ key
460
+ )
461
+ ) {
462
+ execute[key] = await this.processOperator(
463
+ data,
464
+ event,
465
+ key,
466
+ execute[key]
467
+ );
468
+ } else if (
469
+ typeof execute[key] === "string" &&
470
+ execute[key].startsWith("$") &&
471
+ !["$storage", "$database", "$array", "$filter"].includes(
472
+ execute[key]
473
+ )
474
+ ) {
475
+ execute[key] = await this.processOperator(
476
+ data,
477
+ event,
478
+ execute[key]
479
+ );
480
+ } else if (Array.isArray(execute[key])) {
481
+ execute[key] = await this.processOperators(
482
+ data,
483
+ event,
484
+ execute[key]
485
+ );
486
+ } else if (
487
+ typeof execute[key] === "object" &&
488
+ execute[key] !== null
489
+ ) {
490
+ execute[key] = await this.processOperators(
491
+ data,
492
+ event,
493
+ execute[key]
494
+ );
495
+ }
496
+ }
497
+ } else if (
498
+ typeof execute === "string" &&
499
+ execute.startsWith("$") &&
500
+ !["$storage", "$database", "$array", "$filter"].includes(execute)
501
+ ) {
502
+ execute = await this.processOperator(data, event, execute);
503
+ }
504
+
505
+ return execute;
506
+ }
507
+
508
+ async processOperator(data, event, operator, context) {
509
+ let result;
510
+ if (operator.startsWith("$data.")) {
511
+ result = getValueFromObject(data, operator.substring(6));
512
+ return getValueFromObject(data, operator.substring(6));
513
+ } else if (operator.startsWith("$req")) {
514
+ return getValueFromObject(data, operator.substring(1));
515
+ } else if (operator.startsWith("$header")) {
516
+ return getValueFromObject(data.req, operator.substring(1));
517
+ } else if (operator.startsWith("$rawBody")) {
518
+ return getValueFromObject(data, operator.substring(1));
519
+ } else if (operator.startsWith("$crud")) {
520
+ let results = context;
521
+ let isObject = false;
522
+ if (!Array.isArray(results)) {
523
+ isObject = true;
524
+ results = [results];
525
+ }
526
+
527
+ for (let i = 0; i < results.length; i++) {
528
+ results[i] = await this.processOperators(
529
+ data,
530
+ event,
531
+ results[i]
532
+ );
533
+ results[i] = await this.crud.send(results[i]);
534
+ if (operator.startsWith("$crud."))
535
+ results[i] = getValueFromObject(
536
+ operator,
537
+ operator.substring(6)
538
+ );
539
+ results[i] = await this.processOperators(
540
+ data,
541
+ event,
542
+ results[i]
543
+ );
544
+ }
545
+
546
+ if (isObject) results = results[0];
547
+
548
+ return results;
549
+ } else if (operator.startsWith("$socket")) {
550
+ context = await this.processOperators(data, event, context);
551
+ result = await this.socket.send(context);
552
+ if (operator.startsWith("$socket."))
553
+ result = getValueFromObject(operator, operator.substring(6));
554
+ return await this.processOperators(data, event, result);
555
+ } else if (operator.startsWith("$api")) {
556
+ context = await this.processOperators(data, event, context);
557
+ let name = context.method.split(".")[0];
558
+ result = this.executeScriptWithTimeout(name, context);
559
+ if (operator.startsWith("$api."))
560
+ result = getValueFromObject(event, operator.substring(5));
561
+ return await this.processOperators(data, event, result);
562
+ } else if (operator.startsWith("$event")) {
563
+ if (operator.startsWith("$event."))
564
+ result = getValueFromObject(event, operator.substring(7));
565
+ return await this.processOperators(data, event, result);
566
+ }
567
+
568
+ return operator;
569
+ }
570
+
571
+ async getApiKey(data, name) {
572
+ let organization = await this.crud.getOrganization(data);
573
+ if (organization.error) throw new Error(organization.error);
574
+ if (!organization.apis)
575
+ throw new Error("Missing apis object in organization object");
576
+ if (!organization.apis[name])
577
+ throw new Error(`Missing ${name} in organization apis object`);
578
+ return organization.apis[name];
579
+ }
462
580
  }
463
581
 
464
582
  async function executeMethod(method, methodPath, instance, params) {
465
- try {
466
- switch (methodPath.length) {
467
- case 1:
468
- return await instance[methodPath[0]](...params)
469
- case 2:
470
- return await instance[methodPath[0]][methodPath[1]](...params);
471
- case 3:
472
- return await instance[methodPath[0]][methodPath[1]][methodPath[2]](...params);
473
- case 4:
474
- return await instance[methodPath[0]][methodPath[1]][methodPath[2]][methodPath[3]](...params);
475
- case 5:
476
- return await instance[methodPath[0]][methodPath[1]][methodPath[2]][methodPath[3]][methodPath[4]](...params);
477
- case 6:
478
- return await instance[methodPath[0]][methodPath[1]][methodPath[2]][methodPath[3]][methodPath[4]][methodPath[5]](...params);
479
- case 7:
480
- return await instance[methodPath[0]][methodPath[1]][methodPath[2]][methodPath[3]][methodPath[4]][methodPath[5]][methodPath[6]](...params);
481
- case 8:
482
- return await instance[methodPath[0]][methodPath[1]][methodPath[2]][methodPath[3]][methodPath[4]][methodPath[5]][methodPath[6]][methodPath[7]](...params);
483
- default:
484
- const methodName = methodPath.pop();
485
- let Method = instance
486
- for (let i = 0; i < methodPath.length; i++) {
487
- Method = Method[methodPath[i]]
488
- if (Method === undefined) {
489
- throw new Error(`Method ${methodPath[i]} not found using ${method}.`);
490
- }
491
- }
492
-
493
- if (typeof Method[methodName] !== 'function')
494
- throw new Error(`Method ${method} is not a function.`);
495
-
496
- return await Method[methodName](...params)
497
- }
498
- } catch (error) {
499
- throw new Error(error);
500
- }
583
+ try {
584
+ switch (methodPath.length) {
585
+ case 1:
586
+ return await instance[methodPath[0]](...params);
587
+ case 2:
588
+ return await instance[methodPath[0]][methodPath[1]](...params);
589
+ case 3:
590
+ return await instance[methodPath[0]][methodPath[1]][
591
+ methodPath[2]
592
+ ](...params);
593
+ case 4:
594
+ return await instance[methodPath[0]][methodPath[1]][
595
+ methodPath[2]
596
+ ][methodPath[3]](...params);
597
+ case 5:
598
+ return await instance[methodPath[0]][methodPath[1]][
599
+ methodPath[2]
600
+ ][methodPath[3]][methodPath[4]](...params);
601
+ case 6:
602
+ return await instance[methodPath[0]][methodPath[1]][
603
+ methodPath[2]
604
+ ][methodPath[3]][methodPath[4]][methodPath[5]](...params);
605
+ case 7:
606
+ return await instance[methodPath[0]][methodPath[1]][
607
+ methodPath[2]
608
+ ][methodPath[3]][methodPath[4]][methodPath[5]][methodPath[6]](
609
+ ...params
610
+ );
611
+ case 8:
612
+ return await instance[methodPath[0]][methodPath[1]][
613
+ methodPath[2]
614
+ ][methodPath[3]][methodPath[4]][methodPath[5]][methodPath[6]][
615
+ methodPath[7]
616
+ ](...params);
617
+ default:
618
+ const methodName = methodPath.pop();
619
+ let Method = instance;
620
+ for (let i = 0; i < methodPath.length; i++) {
621
+ Method = Method[methodPath[i]];
622
+ if (Method === undefined) {
623
+ throw new Error(
624
+ `Method ${methodPath[i]} not found using ${method}.`
625
+ );
626
+ }
627
+ }
628
+
629
+ if (typeof Method[methodName] !== "function")
630
+ throw new Error(`Method ${method} is not a function.`);
631
+
632
+ return await Method[methodName](...params);
633
+ }
634
+ } catch (error) {
635
+ throw new Error(error);
636
+ }
501
637
  }
502
638
 
503
639
  function getModuleDependencies(modulePath) {
504
- let moduleObj = require.cache[modulePath];
505
- if (!moduleObj) {
506
- return [];
507
- }
640
+ let moduleObj = require.cache[modulePath];
641
+ if (!moduleObj) {
642
+ return [];
643
+ }
508
644
 
509
- // Get all child module paths
510
- return moduleObj.children.map(child => child.id);
645
+ // Get all child module paths
646
+ return moduleObj.children.map((child) => child.id);
511
647
  }
512
648
 
513
649
  function isModuleUsedElsewhere(modulePath, name) {
514
- return Object.keys(require.cache).some(path => {
515
- const moduleObj = require.cache[path];
516
- // return moduleObj.children.some(child => child.id === modulePath && path !== modulePath);
517
- return moduleObj.children.some(child => {
518
- // let test = child.id === modulePath && path !== modulePath
519
- // if (test)
520
- // return test
521
- return child.id === modulePath && path !== modulePath
522
- });
523
- });
650
+ return Object.keys(require.cache).some((path) => {
651
+ const moduleObj = require.cache[path];
652
+ // return moduleObj.children.some(child => child.id === modulePath && path !== modulePath);
653
+ return moduleObj.children.some((child) => {
654
+ // let test = child.id === modulePath && path !== modulePath
655
+ // if (test)
656
+ // return test
657
+ return child.id === modulePath && path !== modulePath;
658
+ });
659
+ });
524
660
  }
525
661
 
526
662
  function clearModuleCache(name) {
527
- try {
528
- const modulePath = require.resolve(name);
529
- const dependencies = getModuleDependencies(modulePath);
530
-
531
- // Check if the module is a dependency of other modules
532
- // const moduleObj = require.cache[modulePath];
533
- // if (moduleObj && moduleObj.parent) {
534
- // console.log(`Module ${name} is a dependency of other modules.`);
535
- // return;
536
- // }
537
-
538
- // Check if the module is used by other modules
539
- if (isModuleUsedElsewhere(modulePath, name)) {
540
- console.log(`Module ${name} is a dependency of other modules.`);
541
- return;
542
- }
543
-
544
- // Remove the module from the cache
545
- delete require.cache[modulePath];
546
- console.log(`Module ${name} has been removed from cache.`);
547
- // Recursively clear dependencies from cache
548
- dependencies.forEach(depPath => {
549
- clearModuleCache(depPath);
550
- });
551
-
552
- } catch (error) {
553
- console.error(`Error clearing module cache for ${name}: ${error.message}`);
554
- }
663
+ try {
664
+ const modulePath = require.resolve(name);
665
+ const dependencies = getModuleDependencies(modulePath);
666
+
667
+ // Check if the module is a dependency of other modules
668
+ // const moduleObj = require.cache[modulePath];
669
+ // if (moduleObj && moduleObj.parent) {
670
+ // console.log(`Module ${name} is a dependency of other modules.`);
671
+ // return;
672
+ // }
673
+
674
+ // Check if the module is used by other modules
675
+ if (isModuleUsedElsewhere(modulePath, name)) {
676
+ console.log(`Module ${name} is a dependency of other modules.`);
677
+ return;
678
+ }
679
+
680
+ // Remove the module from the cache
681
+ delete require.cache[modulePath];
682
+ console.log(`Module ${name} has been removed from cache.`);
683
+ // Recursively clear dependencies from cache
684
+ dependencies.forEach((depPath) => {
685
+ clearModuleCache(depPath);
686
+ });
687
+ } catch (error) {
688
+ console.error(
689
+ `Error clearing module cache for ${name}: ${error.message}`
690
+ );
691
+ }
555
692
  }
556
693
 
557
694
  // Function to fetch script from database and save to disk
558
695
  async function fetchScriptFromDatabaseAndSave(name, moduleConfig) {
559
- let data = {
560
- method: 'object.read',
561
- host: moduleConfig.object.hostname,
562
- array: moduleConfig.array,
563
- $filter: {
564
- query: {
565
- host: { $in: [moduleConfig.object.hostname, '*'] },
566
- pathname: moduleConfig.object.pathname
567
- },
568
- limit: 1
569
- },
570
- organization_id
571
- };
572
-
573
- let file = await this.crud.send(data);
574
- let src;
575
-
576
- if (file && file.object && file.object[0]) {
577
- src = file.object[0].src;
578
- } else {
579
- throw new Error('Script not found in database');
580
- }
581
-
582
- // Save to disk for future use
583
- const scriptPath = path.join(scriptsDirectory, `${name}.js`);
584
- await fs.writeFile(scriptPath, src);
585
-
586
- return src;
696
+ let data = {
697
+ method: "object.read",
698
+ host: moduleConfig.object.hostname,
699
+ array: moduleConfig.array,
700
+ $filter: {
701
+ query: {
702
+ host: { $in: [moduleConfig.object.hostname, "*"] },
703
+ pathname: moduleConfig.object.pathname
704
+ },
705
+ limit: 1
706
+ },
707
+ organization_id
708
+ };
709
+
710
+ let file = await this.crud.send(data);
711
+ let src;
712
+
713
+ if (file && file.object && file.object[0]) {
714
+ src = file.object[0].src;
715
+ } else {
716
+ throw new Error("Script not found in database");
717
+ }
718
+
719
+ // Save to disk for future use
720
+ const scriptPath = path.join(scriptsDirectory, `${name}.js`);
721
+ await fs.writeFile(scriptPath, src);
722
+
723
+ return src;
587
724
  }
588
725
 
589
-
590
-
591
-
592
- module.exports = CoCreateLazyLoader;
726
+ module.exports = CoCreateLazyLoader;