@cocreate/lazy-loader 1.23.7 → 1.24.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/server.js CHANGED
@@ -1,968 +1,753 @@
1
- const fs = require("fs").promises;
2
- const path = require("path");
3
- const { URL } = require("url");
4
- const vm = require("vm");
5
- const Config = require("@cocreate/config");
6
- const { getValueFromObject, objectToSearchParams } = require("@cocreate/utils");
7
-
8
- class CoCreateLazyLoader {
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.wsManager.on("endpoint", (data) => {
30
- this.executeEndpoint(data);
31
- });
32
-
33
- this.modules = await Config("modules", false, false);
34
- if (!this.modules) return;
35
- else this.modules = this.modules.modules;
36
-
37
- for (let name of Object.keys(this.modules)) {
38
- this.wsManager.on(this.modules[name].event, (data) => {
39
- this.executeScriptWithTimeout(name, data);
40
- });
41
- }
42
-
43
- this.server.https.on("request", (req, res) => this.request(req, res));
44
- this.server.http.on("request", (req, res) => this.request(req, res));
45
- }
46
-
47
- async request(req, res) {
48
- try {
49
- // TODO: track usage
50
- const urlObject = new URL(`http://${req.headers.host}${req.url}`);
51
- const hostname = urlObject.hostname;
52
- let organization;
53
-
54
- try {
55
- organization = await this.crud.getOrganization({
56
- host: hostname
57
- });
58
- } catch {
59
- return this.files.send(
60
- req,
61
- res,
62
- this.crud,
63
- organization,
64
- urlObject
65
- );
66
- }
67
-
68
- if (urlObject.pathname.startsWith("/webhooks/")) {
69
- let name = req.url.split("/")[2]; // Assuming URL structure is /webhooks/name/...
70
- if (this.modules[name]) {
71
- this.executeScriptWithTimeout(name, {
72
- req,
73
- res,
74
- host: hostname,
75
- organization,
76
- urlObject,
77
- organization_id: organization._id
78
- });
79
- } else {
80
- // Handle unknown module or missing webhook method
81
- res.writeHead(404, { "Content-Type": "application/json" });
82
- res.end(JSON.stringify({ error: "Not found" }));
83
- }
84
- } else {
85
- this.files.send(req, res, this.crud, organization, urlObject);
86
- }
87
- } catch (error) {
88
- res.writeHead(400, { "Content-Type": "text/plain" });
89
- res.end("Invalid host format");
90
- }
91
- }
92
-
93
- async executeEndpoint(data) {
94
- try {
95
- if (!data.method || !data.endpoint) {
96
- throw new Error("Request missing 'method' or 'endpoint'.");
97
- }
98
-
99
- let name = data.method.split(".")[0];
100
- let method = data.endpoint.split(" ")[0].toUpperCase();
101
-
102
- // data = await this.processOperators(data, "", name);
103
-
104
- let apiConfig = await this.getApiConfig(data, name);
105
- // --- Refined Validation ---
106
- if (!apiConfig) {
107
- throw new Error(`Configuration missing for API: '${name}'.`);
108
- }
109
- if (!apiConfig.url) {
110
- throw new Error(
111
- `Configuration error: Missing base url for API '${name}'.`
112
- );
113
- }
114
- // apiConfig = await this.processOperators(data, getApiConfig, "");
115
-
116
- let override = apiConfig.endpoint?.[data.endpoint] || {};
117
-
118
- let url = apiConfig.url; // Base URL
119
- url = url.endsWith("/") ? url.slice(0, -1) : url;
120
-
121
- let path = override.path || data.endpoint.split(" ")[1];
122
- url += path.startsWith("/") ? path : `/${path}`;
123
-
124
- url += objectToSearchParams(data[name].$searchParams);
125
-
126
- // User's proposed simplification:
127
- let headers = apiConfig.headers; // Default headers
128
- if (override.headers) {
129
- headers = { ...headers, ...override.headers }; // Correct idea for merging
130
- }
131
-
132
- // let body = formatRequestBody(data[name]);
133
-
134
- let formatType = data.formatType || "json";
135
- const timeout = 10000; // Set default timeout in ms (e.g., 10 seconds)
136
- let options = { method, headers, timeout };
137
-
138
- // Only add body for methods that support it (not GET or HEAD)
139
- if (!["GET", "HEAD"].includes(method)) {
140
- let { body } = this.formatRequestBody(data[name], formatType);
141
- options.body = body;
142
- }
143
- // For GET/HEAD, do not create or send a body; all params should be in the URL
144
-
145
- const response = await this.makeHttpRequest(url, options);
146
-
147
- // If the response is not ok, makeHttpRequest will throw and be caught below.
148
- // If you want to include more info in the error, you can log or attach response details here.
149
-
150
- data[name] = await response.json();
151
-
152
- this.wsManager.send(data);
153
- } catch (error) {
154
- // Add more detail to the error for debugging 404s
155
- data.error = error.message;
156
- if (error.response) {
157
- data.status = error.response.status;
158
- data.statusText = error.response.statusText;
159
- data.responseData = error.response.data;
160
- }
161
- if (data.req) {
162
- data.res.writeHead(400, {
163
- "Content-Type": "application/json"
164
- });
165
- data.res.end(
166
- JSON.stringify({
167
- error: data.error,
168
- status: data.status,
169
- statusText: data.statusText,
170
- responseData: data.responseData
171
- })
172
- );
173
- }
174
- if (data.socket) {
175
- this.wsManager.send(data);
176
- }
177
- }
178
- }
179
-
180
- /**
181
- * Formats the request body payload based on the specified format type.
182
- *
183
- * @param {object | string} payload The data intended for the request body.
184
- * @param {string} [formatType='json'] The desired format ('json', 'form-urlencoded', 'text', 'multipart', 'xml'). Defaults to 'json'.
185
- * @returns {{ body: string | Buffer | FormData | null, contentTypeHeader: string | null }}
186
- * An object containing the formatted body and the corresponding Content-Type header.
187
- * Returns null body/header on error or for unsupported types.
188
- */
189
- formatRequestBody(payload, formatType = "json") {
190
- let body = null;
191
- let contentTypeHeader = null;
192
-
193
- try {
194
- switch (formatType.toLowerCase()) {
195
- case "json":
196
- body = JSON.stringify(payload);
197
- contentTypeHeader = "application/json; charset=utf-8";
198
- break;
199
-
200
- case "form-urlencoded":
201
- // In Node.js using querystring:
202
- // const querystring = require('node:querystring');
203
- // body = querystring.stringify(payload);
204
- // Or using URLSearchParams (Node/Browser):
205
- body = new URLSearchParams(payload).toString();
206
- contentTypeHeader =
207
- "application/x-www-form-urlencoded; charset=utf-8";
208
- break;
209
-
210
- case "text":
211
- if (typeof payload === "string") {
212
- body = payload;
213
- } else if (
214
- payload &&
215
- typeof payload.toString === "function"
216
- ) {
217
- // Attempt conversion for simple objects/values, might need refinement
218
- body = payload.toString();
219
- } else {
220
- throw new Error(
221
- "Payload must be a string or convertible to string for 'text' format."
222
- );
223
- }
224
- contentTypeHeader = "text/plain; charset=utf-8";
225
- break;
226
-
227
- case "multipart":
228
- // COMPLEX: Requires FormData (browser) or form-data library (Node)
229
- // Needs specific logic to handle payload structure (identifying files vs fields)
230
- // const formData = buildFormData(payload); // Placeholder for complex logic
231
- // body = formData; // The FormData object itself or its stream
232
- // contentTypeHeader = formData.getHeaders ? formData.getHeaders()['content-type'] : 'multipart/form-data; boundary=...'; // Header includes boundary
233
- console.warn(
234
- "Multipart formatting requires specific implementation."
235
- );
236
- // For now, return null or throw error
237
- throw new Error(
238
- "Multipart formatting not implemented in this basic function."
239
- );
240
- break; // Example: Not fully implemented here
241
-
242
- case "xml":
243
- // COMPLEX: Requires an XML serialization library
244
- // const xmlString = convertObjectToXml(payload); // Placeholder
245
- // body = xmlString;
246
- console.warn(
247
- "XML formatting requires an external library."
248
- );
249
- throw new Error(
250
- "XML formatting not implemented in this basic function."
251
- );
252
- break; // Example: Not fully implemented here
253
-
254
- default:
255
- console.error(
256
- `Unsupported requestBodyFormat: ${formatType}`
257
- );
258
- // Fallback or throw error
259
- body = JSON.stringify(payload); // Default to JSON on unknown? Or error?
260
- contentTypeHeader = "application/json; charset=utf-8";
261
- }
262
- } catch (error) {
263
- console.error(
264
- `Error formatting request body as ${formatType}:`,
265
- error
266
- );
267
- return { body: null, contentTypeHeader: null }; // Return nulls on error
268
- }
269
-
270
- return { body, contentTypeHeader };
271
- }
272
-
273
- /**
274
- * Makes an HTTP request using node-fetch.
275
- * @param {string} url - The complete URL to request.
276
- * @param {string} method - The HTTP method (GET, POST, etc.).
277
- * @param {object} headers - The request headers object.
278
- * @param {string|Buffer|null|undefined} body - The formatted request body.
279
- * @param {number} timeout - Request timeout in milliseconds.
280
- * @returns {Promise<{status: number, data: any}>} - Resolves with status and parsed response data.
281
- * @throws {Error} If the request fails or returns a non-ok status.
282
- */
283
- async makeHttpRequest(url, options) {
284
- let controller, timeoutId;
285
- if (this.server.AbortController) {
286
- controller = new this.server.AbortController();
287
- timeoutId = setTimeout(() => controller.abort(), options.timeout);
288
- options.signal = controller.signal;
289
- }
290
-
291
- // Remove Content-Type header if there's no body (relevant for GET, DELETE etc.)
292
- if (
293
- options.body === undefined &&
294
- options.headers &&
295
- options.headers["Content-Type"]
296
- ) {
297
- delete options.headers["Content-Type"];
298
- }
299
-
300
- const fetchFn = this.server.fetch || global.fetch;
301
- if (typeof fetchFn !== "function") {
302
- throw new Error("No fetch implementation available.");
303
- }
304
-
305
- try {
306
- const response = await fetchFn(url, options);
307
- if (timeoutId) clearTimeout(timeoutId);
308
-
309
- if (!response.ok) {
310
- const text = await response.text();
311
- const error = new Error(
312
- `HTTP error! Status: ${response.status} ${response.statusText}`
313
- );
314
- error.response = {
315
- status: response.status,
316
- statusText: response.statusText,
317
- headers: Object.fromEntries(response.headers.entries()),
318
- data: text
319
- };
320
- throw error;
321
- }
322
- return response;
323
- } catch (error) {
324
- if (timeoutId) clearTimeout(timeoutId);
325
- throw error;
326
- }
327
- }
328
-
329
- async executeScriptWithTimeout(name, data) {
330
- try {
331
- if (
332
- this.modules[name].initialize ||
333
- this.modules[name].initialize === ""
334
- ) {
335
- if (data.req) {
336
- data = await this.webhooks(this.modules[name], data, name);
337
- } else {
338
- data = await this.api(this.modules[name], data);
339
- }
340
- } else {
341
- if (!this.modules[name].content) {
342
- if (this.modules[name].path)
343
- this.modules[name].content = await require(this.modules[
344
- name
345
- ].path);
346
- else {
347
- try {
348
- const scriptPath = path.join(
349
- scriptsDirectory,
350
- `${name}.js`
351
- );
352
- await fs.access(scriptPath);
353
- this.modules[name].content = await fs.readFile(
354
- scriptPath,
355
- "utf8"
356
- );
357
- } catch {
358
- this.modules[name].content =
359
- await fetchScriptFromDatabaseAndSave(
360
- name,
361
- this.modules[name],
362
- data
363
- );
364
- }
365
- }
366
- }
367
-
368
- if (this.modules[name].content) {
369
- data.apis = await this.getApiConfig(data, name);
370
- data.crud = this.crud;
371
- data = await this.modules[name].content.send(data);
372
- delete data.apis;
373
- delete data.crud;
374
- } else return;
375
- }
376
-
377
- if (data.socket) this.wsManager.send(data);
378
-
379
- if (
380
- this.modules[name].unload === false ||
381
- this.modules[name].unload === "false"
382
- )
383
- return;
384
- else if (
385
- this.modules[name].unload === true ||
386
- this.modules[name].unload === "true"
387
- )
388
- console.log("config should unload after completeion ");
389
- else if (
390
- (this.modules[name].unload = parseInt(
391
- this.modules[name].unload,
392
- 10
393
- ))
394
- ) {
395
- // Check if the script is already loaded
396
- if (this.modules[name].timeout) {
397
- clearTimeout(this.modules[name].timeout);
398
- } else if (!this.modules[name].path) {
399
- // Execute the script
400
- this.modules[name].context = new vm.createContext({});
401
- const script = new vm.Script(this.modules[name].context);
402
- script.runInContext(context);
403
- }
404
-
405
- // Reset or set the timeout
406
- const timeout = setTimeout(() => {
407
- // delete this.modules[name]
408
- delete this.modules[name].timeout;
409
- delete this.modules[name].context;
410
- delete this.modules[name].content;
411
- console.log(`Module ${name} removed due to inactivity.`);
412
- clearModuleCache(name);
413
- }, this.modules[name].unload);
414
-
415
- this.modules[name].timeout = timeout;
416
- }
417
- } catch (error) {
418
- data.error = error.message;
419
- if (data.req) {
420
- data.res.writeHead(400, { "Content-Type": "text/plain" });
421
- data.res.end(`Lazyload Error: ${error.message}`);
422
- }
423
- if (data.socket) this.wsManager.send(data);
424
- }
425
- }
426
-
427
- /**
428
- * TODO: Implement Enhanced API Configuration Handling
429
- *
430
- * Description:
431
- * - Implement functionality to dynamically handle API configurations, supporting both complete and base URL endpoints with automatic method-based path appending.
432
- * - Enable dynamic generation of query parameters from a designated object (`stripe` in the examples) when `query` is true.
433
- *
434
- * Requirements:
435
- * 1. Dynamic Endpoint Handling:
436
- * - Check if the endpoint configuration is a complete URL or a base URL.
437
- * - If the `method` derived path is not already included in the endpoint, append it dynamically.
438
- * Example:
439
- * `{ "method": "stripe.accounts.retrieve", "endpoint": "https://api.stripe.com", "query": true, "stripe": { "acct": "acct_123", "name": "John Doe" } }`
440
- * `{ "method": "stripe.accounts.retrieve", "endpoint": "https://api.stripe.com/accounts/retrieve", "query": true, "stripe": { "acct": "acct_123", "name": "John Doe" } }`
441
- * - Develop logic to parse the `method` and check against the endpoint. If necessary, append the appropriate API method segment.
442
- *
443
- * 2. Query Parameter Handling:
444
- * - Dynamically construct and append query parameters from the `stripe` object if `query` is true. Ensure proper URL-encoding of keys and values.
445
- *
446
- * 3. Security:
447
- * - Use the `method` for permission checks, ensuring that each API request complies with security protocols.
448
- *
449
- * 4. Testing:
450
- * - Test both scenarios where the endpoint may or may not include the method path to ensure the dynamic construction works correctly.
451
- * - Ensure that all query parameters are correctly formatted and appended.
452
- *
453
- * Notes:
454
- * - Consider utility functions for parsing and modifying URLs, as well as for encoding parameters.
455
- * - Maintain clear and detailed documentation for each part of the implementation to assist future development and troubleshooting.
456
- */
457
-
458
- async api(config, data) {
459
- try {
460
- const methodPath = data.method.split(".");
461
- const name = methodPath.shift();
462
-
463
- const apis = await this.getApiConfig(data, name);
464
-
465
- const key = apis.key;
466
- if (!key)
467
- throw new Error(
468
- `Missing ${name} key in organization apis object`
469
- );
470
-
471
- // ToDo: if data.endpoint service not required as endpoint will be used
472
- let instance;
473
-
474
- // Try using require() first, for CommonJS modules
475
- try {
476
- instance = require(config.path); // Attempt to require the module
477
- } catch (err) {
478
- if (err.code === "ERR_REQUIRE_ESM") {
479
- // If it's an ESM module, fallback to dynamic import()
480
- instance = await import(config.path);
481
- } else {
482
- throw err; // Re-throw other errors
483
- }
484
- }
485
-
486
- if (config.initialize) {
487
- if (Array.isArray(config.initialize)) {
488
- const initializations = [];
489
- for (let i = 0; i < config.initialize.length; i++) {
490
- const initialize = config.initialize[i].split(".");
491
- initializations.push(instance);
492
- // Traverse the nested structure to reach the correct constructor
493
- for (let j = 0; j < initialize.length; j++) {
494
- if (initializations[i][initialize[j]]) {
495
- initializations[i] =
496
- initializations[i][initialize[j]];
497
- } else {
498
- throw new Error(
499
- `Service path ${config.initialize[i]} is incorrect at ${initialize[j]}`
500
- );
501
- }
502
- }
503
- }
504
- instance = new initializations[1](
505
- new initializations[0](key)
506
- );
507
- } else {
508
- const initialize = config.initialize.split(".");
509
- // Traverse the nested structure to reach the correct constructor
510
- for (let i = 0; i < initialize.length; i++) {
511
- if (instance[initialize[i]]) {
512
- instance = instance[initialize[i]];
513
- } else {
514
- throw new Error(
515
- `Service path ${config.initialize} is incorrect at ${initialize[i]}`
516
- );
517
- }
518
- }
519
- }
520
- // instance = new instance(key);
521
- }
522
- // else
523
- instance = new instance(key);
524
-
525
- let params = [],
526
- mainParam = false;
527
- for (let i = 0; true; i++) {
528
- if (`$param[${i}]` in data[name]) {
529
- params.push(data[name][`$param[${i}]`]);
530
- delete data[name][`$param[${i}]`];
531
- } else if (!mainParam) {
532
- params.push(data[name]);
533
- mainParam = true;
534
- } else {
535
- break;
536
- }
537
- }
538
-
539
- // 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
540
- // data[name] = await processOperators(data, null, data[name]);
541
-
542
- // data[name] = await processOperators(data, null, data[name]);
543
-
544
- // execute = await this.processOperators(data, event, execute);
545
-
546
- data[name] = await executeMethod(
547
- data.method,
548
- methodPath,
549
- instance,
550
- params
551
- );
552
-
553
- // TODO: should run processOperators after in order to perform complex opertions and get data
554
- // data[name] = await processOperators(data, data[name]);
555
-
556
- return data;
557
- } catch (error) {
558
- data.error = error.message;
559
- return data;
560
- }
561
- }
562
-
563
- async webhooks(config, data, name) {
564
- try {
565
- const apis = await this.getApiConfig(data, name);
566
-
567
- const key = apis.key;
568
- if (!key)
569
- throw new Error(
570
- `Missing ${name} key in organization apis object`
571
- );
572
-
573
- let webhookName = data.req.url.split("/");
574
- webhookName = webhookName[webhookName.length - 1];
575
-
576
- const webhook = apis.webhooks[webhookName];
577
- if (!webhook)
578
- throw new Error(
579
- `Webhook ${name} ${webhookName} is not defined`
580
- );
581
-
582
- // eventDataKey is used to access the event data
583
- let eventDataKey = webhook.eventDataKey || apis.eventDataKey;
584
- if (!eventDataKey)
585
- throw new Error(`Webhook ${name} eventKey is not defined`);
586
-
587
- // eventNameKey is used to access the event the event name
588
- let eventNameKey = webhook.eventNameKey || apis.eventNameKey;
589
- if (!eventNameKey)
590
- throw new Error(`Webhook ${name} eventNameKey is not defined`);
591
-
592
- if (!webhook.events)
593
- throw new Error(`Webhook ${name} events are not defined`);
594
-
595
- data.rawBody = "";
596
- await new Promise((resolve, reject) => {
597
- data.req.on("data", (chunk) => {
598
- data.rawBody += chunk.toString();
599
- });
600
- data.req.on("end", () => {
601
- resolve();
602
- });
603
- data.req.on("error", (err) => {
604
- reject(err);
605
- });
606
- });
607
-
608
- let parameters, method;
609
-
610
- if (webhook.authenticate && webhook.authenticate.method) {
611
- method = webhook.authenticate.method;
612
- } else if (apis.authenticate && apis.authenticate.method) {
613
- method = apis.authenticate.method;
614
- } else
615
- throw new Error(
616
- `Webhook ${name} authenticate method is not defined`
617
- );
618
-
619
- if (webhook.authenticate && webhook.authenticate.parameters) {
620
- parameters = webhook.authenticate.parameters;
621
- } else if (apis.authenticate && apis.authenticate.parameters) {
622
- parameters = apis.authenticate.parameters;
623
- } else
624
- throw new Error(
625
- `Webhook ${name} authenticate parameters is not defined`
626
- );
627
-
628
- // TODO: webhook secert could be a key pair
629
-
630
- let event;
631
- if (!method) {
632
- if (!parameters[0] !== parameters[1])
633
- throw new Error(
634
- `Webhook secret failed for ${name}. Unauthorized access attempt.`
635
- );
636
-
637
- event = JSON.parse(data.rawBody);
638
- } else {
639
- const service = require(config.path);
640
- let instance;
641
- if (config.initialize)
642
- instance = new service[config.initialize](key);
643
- else instance = new service(key);
644
-
645
- const methodPath = method.split(".");
646
-
647
- await this.processOperators(data, "", parameters);
648
-
649
- event = await executeMethod(
650
- method,
651
- methodPath,
652
- instance,
653
- parameters
654
- );
655
- }
656
-
657
- let eventName = getValueFromObject(event, eventNameKey);
658
- if (!eventName)
659
- throw new Error(
660
- `Webhook ${name} eventNameKey: ${eventNameKey} could not be found in the event.`
661
- );
662
-
663
- let eventData = getValueFromObject(event, eventDataKey);
664
- if (!eventData)
665
- throw new Error(
666
- `Webhook ${name} eventDataKey: ${eventDataKey} could not be found in the event.`
667
- );
668
-
669
- let execute = webhook.events[eventName];
670
- if (execute) {
671
- execute = await this.processOperators(data, event, execute);
672
- }
673
-
674
- data.res.writeHead(200, { "Content-Type": "application/json" });
675
- data.res.end(
676
- JSON.stringify({ message: "Webhook received and processed" })
677
- );
678
- return data;
679
- } catch (error) {
680
- data.error = error.message;
681
- data.res.writeHead(400, { "Content-Type": "text/plain" });
682
- data.res.end(error.message);
683
- return data;
684
- }
685
- }
686
-
687
- async processOperators(data, event, execute) {
688
- if (Array.isArray(execute)) {
689
- for (let index = 0; index < execute.length; index++) {
690
- execute[index] = await this.processOperators(
691
- data,
692
- event,
693
- execute[index]
694
- );
695
- }
696
- } else if (typeof execute === "object" && execute !== null) {
697
- for (let key of Object.keys(execute)) {
698
- if (
699
- key.startsWith("$") &&
700
- !["$storage", "$database", "$array", "$filter"].includes(
701
- key
702
- )
703
- ) {
704
- execute[key] = await this.processOperator(
705
- data,
706
- event,
707
- key,
708
- execute[key]
709
- );
710
- } else if (
711
- typeof execute[key] === "string" &&
712
- execute[key].startsWith("$") &&
713
- !["$storage", "$database", "$array", "$filter"].includes(
714
- execute[key]
715
- )
716
- ) {
717
- execute[key] = await this.processOperator(
718
- data,
719
- event,
720
- execute[key]
721
- );
722
- } else if (Array.isArray(execute[key])) {
723
- execute[key] = await this.processOperators(
724
- data,
725
- event,
726
- execute[key]
727
- );
728
- } else if (
729
- typeof execute[key] === "object" &&
730
- execute[key] !== null
731
- ) {
732
- execute[key] = await this.processOperators(
733
- data,
734
- event,
735
- execute[key]
736
- );
737
- }
738
- }
739
- } else if (
740
- typeof execute === "string" &&
741
- execute.startsWith("$") &&
742
- !["$storage", "$database", "$array", "$filter"].includes(execute)
743
- ) {
744
- execute = await this.processOperator(data, event, execute);
745
- }
746
-
747
- return execute;
748
- }
749
-
750
- async processOperator(data, event, operator, context) {
751
- let result;
752
- if (operator.startsWith("$data.")) {
753
- result = getValueFromObject(data, operator.substring(6));
754
- return getValueFromObject(data, operator.substring(6));
755
- } else if (operator.startsWith("$req")) {
756
- return getValueFromObject(data, operator.substring(1));
757
- } else if (operator.startsWith("$header")) {
758
- return getValueFromObject(data.req, operator.substring(1));
759
- } else if (operator.startsWith("$rawBody")) {
760
- return getValueFromObject(data, operator.substring(1));
761
- } else if (operator.startsWith("$crud")) {
762
- let results = context;
763
- let isObject = false;
764
- if (!Array.isArray(results)) {
765
- isObject = true;
766
- results = [results];
767
- }
768
-
769
- for (let i = 0; i < results.length; i++) {
770
- results[i] = await this.processOperators(
771
- data,
772
- event,
773
- results[i]
774
- );
775
- results[i] = await this.crud.send(results[i]);
776
- if (operator.startsWith("$crud."))
777
- results[i] = getValueFromObject(
778
- operator,
779
- operator.substring(6)
780
- );
781
- results[i] = await this.processOperators(
782
- data,
783
- event,
784
- results[i]
785
- );
786
- }
787
-
788
- if (isObject) results = results[0];
789
-
790
- return results;
791
- } else if (operator.startsWith("$socket")) {
792
- context = await this.processOperators(data, event, context);
793
- result = await this.socket.send(context);
794
- if (operator.startsWith("$socket."))
795
- result = getValueFromObject(operator, operator.substring(6));
796
- return await this.processOperators(data, event, result);
797
- } else if (operator.startsWith("$api")) {
798
- context = await this.processOperators(data, event, context);
799
- let name = context.method.split(".")[0];
800
- result = this.executeScriptWithTimeout(name, context);
801
- if (operator.startsWith("$api."))
802
- result = getValueFromObject(event, operator.substring(5));
803
- return await this.processOperators(data, event, result);
804
- } else if (operator.startsWith("$event")) {
805
- if (operator.startsWith("$event."))
806
- result = getValueFromObject(event, operator.substring(7));
807
- return await this.processOperators(data, event, result);
808
- }
809
-
810
- return operator;
811
- }
812
-
813
- async getApiConfig(data, name) {
814
- let organization = await this.crud.getOrganization(data);
815
- if (organization.error) throw new Error(organization.error);
816
- if (!organization.apis)
817
- throw new Error("Missing apis object in organization object");
818
- if (!organization.apis[name])
819
- throw new Error(`Missing ${name} in organization apis object`);
820
- return organization.apis[name];
821
- }
1
+ /********************************************************************************
2
+ * Copyright (C) 2023 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ ********************************************************************************/
18
+
19
+ // Commercial Licensing Information:
20
+ // For commercial use of this software without the copyleft provisions of the AGPLv3,
21
+ // you must obtain a commercial license from CoCreate LLC.
22
+ // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
23
+
24
+ import fs from 'node:fs/promises';
25
+ import path from 'node:path';
26
+ import { URL } from 'node:url';
27
+ import vm from 'node:vm';
28
+ import { createRequire } from 'node:module';
29
+
30
+ import Config from '@cocreate/config';
31
+ import { getValueFromObject, objectToSearchParams } from '@cocreate/utils';
32
+
33
+ // Create a require context to handle legacy CommonJS module operations and cache control under ESM
34
+ const require = createRequire(import.meta.url);
35
+
36
+ let server = null;
37
+ let wsManager = null;
38
+ let crud = null;
39
+ const exclusion = { ...require.cache };
40
+ const scriptsDirectory = "./scripts";
41
+ let modulesList = {};
42
+
43
+ /**
44
+ * Initializes the LazyLoader execution environment, loads active configurations,
45
+ * and sets up local runtime sandbox directories.
46
+ * Binds itself directly onto the server instance to satisfy the unified architecture contract.
47
+ * @param {Object} Server - CoCreateServer context instance.
48
+ */
49
+ export async function init(Server) {
50
+ server = Server;
51
+ wsManager = server.wsManager;
52
+ crud = server.crud;
53
+
54
+ try {
55
+ await fs.mkdir(scriptsDirectory, { recursive: true });
56
+ } catch (error) {
57
+ console.error("[@cocreate/lazy-loader] Error creating scripts directory:", error);
58
+ throw error;
59
+ }
60
+
61
+ wsManager.on("endpoint", (data) => {
62
+ executeEndpoint(data);
63
+ });
64
+
65
+
66
+ const retrievedConfig = await Config("modules", false, false);
67
+ if (!retrievedConfig) return;
68
+
69
+ modulesList = retrievedConfig.modules || {};
70
+
71
+ return LazyLoader;
822
72
  }
823
73
 
824
- async function executeMethod(method, methodPath, instance, params) {
825
- try {
826
- switch (methodPath.length) {
827
- case 1:
828
- return await instance[methodPath[0]](...params);
829
- case 2:
830
- return await instance[methodPath[0]][methodPath[1]](...params);
831
- case 3:
832
- return await instance[methodPath[0]][methodPath[1]][
833
- methodPath[2]
834
- ](...params);
835
- case 4:
836
- return await instance[methodPath[0]][methodPath[1]][
837
- methodPath[2]
838
- ][methodPath[3]](...params);
839
- case 5:
840
- return await instance[methodPath[0]][methodPath[1]][
841
- methodPath[2]
842
- ][methodPath[3]][methodPath[4]](...params);
843
- case 6:
844
- return await instance[methodPath[0]][methodPath[1]][
845
- methodPath[2]
846
- ][methodPath[3]][methodPath[4]][methodPath[5]](...params);
847
- case 7:
848
- return await instance[methodPath[0]][methodPath[1]][
849
- methodPath[2]
850
- ][methodPath[3]][methodPath[4]][methodPath[5]][methodPath[6]](
851
- ...params
852
- );
853
- case 8:
854
- return await instance[methodPath[0]][methodPath[1]][
855
- methodPath[2]
856
- ][methodPath[3]][methodPath[4]][methodPath[5]][methodPath[6]][
857
- methodPath[7]
858
- ](...params);
859
- default:
860
- const methodName = methodPath.pop();
861
- let Method = instance;
862
- for (let i = 0; i < methodPath.length; i++) {
863
- Method = Method[methodPath[i]];
864
- if (Method === undefined) {
865
- throw new Error(
866
- `Method ${methodPath[i]} not found using ${method}.`
867
- );
868
- }
869
- }
870
-
871
- if (typeof Method[methodName] !== "function")
872
- throw new Error(`Method ${method} is not a function.`);
873
-
874
- return await Method[methodName](...params);
875
- }
876
- } catch (error) {
877
- throw new Error(error);
878
- }
74
+ /**
75
+ * Webhook Request Gateway.
76
+ * Exposes a clean, callable endpoint for the parent server's central routing orchestrator.
77
+ * Strictly executes and sandboxes incoming third-party webhook requests.
78
+ */
79
+ export async function request(req, res) {
80
+ try {
81
+ const urlObject = new URL(`http://${req.headers.host}${req.url}`);
82
+ const hostname = urlObject.hostname;
83
+
84
+ // Resolve the organization strictly for the webhook context
85
+ let organization;
86
+ try {
87
+ organization = await crud.getOrganization({ host: hostname });
88
+ } catch (error) {
89
+ res.writeHead(404, { "Content-Type": "application/json" });
90
+ res.end(JSON.stringify({ error: "Organization not found for webhook integration" }));
91
+ return;
92
+ }
93
+
94
+ // Expected format: /webhooks/:moduleName
95
+ const name = urlObject.pathname.split("/")[2];
96
+ if (name && modulesList[name]) {
97
+ executeScriptWithTimeout(name, {
98
+ req,
99
+ res,
100
+ host: hostname,
101
+ organization,
102
+ urlObject,
103
+ organization_id: organization._id
104
+ });
105
+ } else {
106
+ res.writeHead(404, { "Content-Type": "application/json" });
107
+ res.end(JSON.stringify({ error: "Webhook module not found" }));
108
+ }
109
+ } catch (error) {
110
+ res.writeHead(400, { "Content-Type": "application/json" });
111
+ res.end(JSON.stringify({ error: "Invalid webhook request format", message: error.message }));
112
+ }
879
113
  }
880
114
 
881
- function getModuleDependencies(modulePath) {
882
- let moduleObj = require.cache[modulePath];
883
- if (!moduleObj) {
884
- return [];
885
- }
115
+ /**
116
+ * Executes external dynamic API Gateway endpoints.
117
+ * Now cleanly decoupled to be invoked by the central Socket Manager (wsManager) routing matrix.
118
+ */
119
+ export async function executeEndpoint(data) {
120
+ try {
121
+ if (!data.method || !data.endpoint) {
122
+ throw new Error("Request missing 'method' or 'endpoint'.");
123
+ }
124
+
125
+ let name = data.method.split(".")[0];
126
+ let method = data.endpoint.split(" ")[0].toUpperCase();
127
+
128
+ let apiConfig = await getApiConfig(data, name);
129
+ if (!apiConfig) {
130
+ throw new Error(`Configuration missing for API: '${name}'.`);
131
+ }
132
+ if (!apiConfig.url) {
133
+ throw new Error(`Configuration error: Missing base url for API '${name}'.`);
134
+ }
135
+
136
+ let override = apiConfig.endpoint?.[data.endpoint] || {};
137
+ let url = apiConfig.url;
138
+ url = url.endsWith("/") ? url.slice(0, -1) : url;
139
+
140
+ let endpointPath = override.path || data.endpoint.split(" ")[1];
141
+ url += endpointPath.startsWith("/") ? endpointPath : `/${endpointPath}`;
142
+ url += objectToSearchParams(data[name].$searchParams);
143
+
144
+ let headers = apiConfig.headers;
145
+ if (override.headers) {
146
+ headers = { ...headers, ...override.headers };
147
+ }
148
+
149
+ let formatType = data.formatType || "json";
150
+ const timeout = 10000;
151
+ let options = { method, headers, timeout };
152
+
153
+ if (!["GET", "HEAD"].includes(method)) {
154
+ let { body } = formatRequestBody(data[name], formatType);
155
+ options.body = body;
156
+ }
157
+
158
+ const response = await makeHttpRequest(url, options);
159
+ data[name] = await response.json();
160
+
161
+ if (wsManager) {
162
+ wsManager.send(data);
163
+ }
164
+ } catch (error) {
165
+ data.error = error.message;
166
+ if (error.response) {
167
+ data.status = error.response.status;
168
+ data.statusText = error.response.statusText;
169
+ data.responseData = error.response.data;
170
+ }
171
+ if (data.req) {
172
+ data.res.writeHead(400, { "Content-Type": "application/json" });
173
+ data.res.end(
174
+ JSON.stringify({
175
+ error: data.error,
176
+ status: data.status,
177
+ statusText: data.statusText,
178
+ responseData: data.responseData
179
+ })
180
+ );
181
+ }
182
+ if (data.socket && wsManager) {
183
+ wsManager.send(data);
184
+ }
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Formats outgoing API body structures based on requested MimeType
190
+ */
191
+ export function formatRequestBody(payload, formatType = "json") {
192
+ let body = null;
193
+ let contentTypeHeader = null;
194
+
195
+ try {
196
+ switch (formatType.toLowerCase()) {
197
+ case "json":
198
+ body = JSON.stringify(payload);
199
+ contentTypeHeader = "application/json; charset=utf-8";
200
+ break;
201
+ case "form-urlencoded":
202
+ body = new URLSearchParams(payload).toString();
203
+ contentTypeHeader = "application/x-www-form-urlencoded; charset=utf-8";
204
+ break;
205
+ case "text":
206
+ if (typeof payload === "string") {
207
+ body = payload;
208
+ } else if (payload && typeof payload.toString === "function") {
209
+ body = payload.toString();
210
+ } else {
211
+ throw new Error("Payload must be a string or convertible to string for 'text' format.");
212
+ }
213
+ contentTypeHeader = "text/plain; charset=utf-8";
214
+ break;
215
+ case "multipart":
216
+ console.warn("Multipart formatting requires specific implementation.");
217
+ throw new Error("Multipart formatting not implemented in this basic function.");
218
+ case "xml":
219
+ console.warn("XML formatting requires an external library.");
220
+ throw new Error("XML formatting not implemented in this basic function.");
221
+ default:
222
+ console.error(`Unsupported requestBodyFormat: ${formatType}`);
223
+ body = JSON.stringify(payload);
224
+ contentTypeHeader = "application/json; charset=utf-8";
225
+ }
226
+ } catch (error) {
227
+ console.error(`Error formatting request body as ${formatType}:`, error);
228
+ return { body: null, contentTypeHeader: null };
229
+ }
230
+
231
+ return { body, contentTypeHeader };
232
+ }
233
+
234
+ /**
235
+ * Unified HTTP Dynamic Fetch requester
236
+ */
237
+ export async function makeHttpRequest(url, options) {
238
+ let controller, timeoutId;
239
+ if (server.AbortController) {
240
+ controller = new server.AbortController();
241
+ timeoutId = setTimeout(() => controller.abort(), options.timeout);
242
+ options.signal = controller.signal;
243
+ }
244
+
245
+ if (options.body === undefined && options.headers && options.headers["Content-Type"]) {
246
+ delete options.headers["Content-Type"];
247
+ }
248
+
249
+ const fetchFn = server.fetch || global.fetch;
250
+ if (typeof fetchFn !== "function") {
251
+ throw new Error("No fetch implementation available.");
252
+ }
253
+
254
+ try {
255
+ const response = await fetchFn(url, options);
256
+ if (timeoutId) clearTimeout(timeoutId);
257
+
258
+ if (!response.ok) {
259
+ const text = await response.text();
260
+ const error = new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
261
+ error.response = {
262
+ status: response.status,
263
+ statusText: response.statusText,
264
+ headers: Object.fromEntries(response.headers.entries()),
265
+ data: text
266
+ };
267
+ throw error;
268
+ }
269
+ return response;
270
+ } catch (error) {
271
+ if (timeoutId) clearTimeout(timeoutId);
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Executes VM sandboxed or dynamically imported modules with custom inactivity timeouts
278
+ */
279
+ export async function executeScriptWithTimeout(name, data) {
280
+ try {
281
+ if (modulesList[name].initialize || modulesList[name].initialize === "") {
282
+ if (data.req) {
283
+ data = await webhooks(modulesList[name], data, name);
284
+ } else {
285
+ data = await api(modulesList[name], data);
286
+ }
287
+ } else {
288
+ if (!modulesList[name].content) {
289
+ if (modulesList[name].path) {
290
+ modulesList[name].content = require(modulesList[name].path);
291
+ } else {
292
+ try {
293
+ const scriptPath = path.join(scriptsDirectory, `${name}.js`);
294
+ await fs.access(scriptPath);
295
+ modulesList[name].content = await fs.readFile(scriptPath, "utf8");
296
+ } catch {
297
+ modulesList[name].content = await fetchScriptFromDatabaseAndSave(
298
+ name,
299
+ modulesList[name],
300
+ data
301
+ );
302
+ }
303
+ }
304
+ }
305
+
306
+ if (modulesList[name].content) {
307
+ data.apis = await getApiConfig(data, name);
308
+ data.crud = crud;
309
+ data = await modulesList[name].content.send(data);
310
+ delete data.apis;
311
+ delete data.crud;
312
+ } else return;
313
+ }
314
+
315
+ if (data.socket && wsManager) wsManager.send(data);
316
+
317
+ if (modulesList[name].unload === false || modulesList[name].unload === "false") {
318
+ return;
319
+ } else if (modulesList[name].unload === true || modulesList[name].unload === "true") {
320
+ console.log("config should unload after completion");
321
+ } else if ((modulesList[name].unload = parseInt(modulesList[name].unload, 10))) {
322
+ if (modulesList[name].timeout) {
323
+ clearTimeout(modulesList[name].timeout);
324
+ } else if (!modulesList[name].path) {
325
+ modulesList[name].context = vm.createContext({});
326
+ const script = new vm.Script(modulesList[name].content);
327
+ script.runInContext(modulesList[name].context);
328
+ }
329
+
330
+ const timeout = setTimeout(() => {
331
+ delete modulesList[name].timeout;
332
+ delete modulesList[name].context;
333
+ delete modulesList[name].content;
334
+ console.log(`Module ${name} removed due to inactivity.`);
335
+ clearModuleCache(name);
336
+ }, modulesList[name].unload);
337
+
338
+ modulesList[name].timeout = timeout;
339
+ }
340
+ } catch (error) {
341
+ data.error = error.message;
342
+ if (data.req) {
343
+ data.res.writeHead(400, { "Content-Type": "text/plain" });
344
+ data.res.end(`Lazyload Error: ${error.message}`);
345
+ }
346
+ if (data.socket && wsManager) wsManager.send(data);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Resolves and builds dynamic library integrations (e.g. Stripe, Sendgrid SDKs)
352
+ */
353
+ export async function api(config, data) {
354
+ try {
355
+ const methodPath = data.method.split(".");
356
+ const name = methodPath.shift();
357
+
358
+ const apis = await getApiConfig(data, name);
359
+ const key = apis.key;
360
+ if (!key) throw new Error(`Missing ${name} key in organization apis object`);
361
+
362
+ let instance;
363
+ try {
364
+ instance = require(config.path);
365
+ } catch (err) {
366
+ if (err.code === "ERR_REQUIRE_ESM") {
367
+ instance = await import(config.path);
368
+ } else {
369
+ throw err;
370
+ }
371
+ }
372
+
373
+ if (config.initialize) {
374
+ if (Array.isArray(config.initialize)) {
375
+ const initializations = [];
376
+ for (let i = 0; i < config.initialize.length; i++) {
377
+ const initialize = config.initialize[i].split(".");
378
+ initializations.push(instance);
379
+ for (let j = 0; j < initialize.length; j++) {
380
+ if (initializations[i][initialize[j]]) {
381
+ initializations[i] = initializations[i][initialize[j]];
382
+ } else {
383
+ throw new Error(`Service path ${config.initialize[i]} is incorrect at ${initialize[j]}`);
384
+ }
385
+ }
386
+ }
387
+ instance = new initializations[1](new initializations[0](key));
388
+ } else {
389
+ const initialize = config.initialize.split(".");
390
+ for (let i = 0; i < initialize.length; i++) {
391
+ if (instance[initialize[i]]) {
392
+ instance = instance[initialize[i]];
393
+ } else {
394
+ throw new Error(`Service path ${config.initialize} is incorrect at ${initialize[i]}`);
395
+ }
396
+ }
397
+ instance = new instance(key);
398
+ }
399
+ } else {
400
+ instance = new instance(key);
401
+ }
402
+
403
+ let params = [], mainParam = false;
404
+ for (let i = 0; true; i++) {
405
+ if (`$param[${i}]` in data[name]) {
406
+ params.push(data[name][`$param[${i}]`]);
407
+ delete data[name][`$param[${i}]`];
408
+ } else if (!mainParam) {
409
+ params.push(data[name]);
410
+ mainParam = true;
411
+ } else {
412
+ break;
413
+ }
414
+ }
415
+
416
+ data[name] = await executeMethod(data.method, methodPath, instance, params);
417
+ return data;
418
+ } catch (error) {
419
+ data.error = error.message;
420
+ return data;
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Resolves incoming platform webhooks dynamically
426
+ */
427
+ export async function webhooks(config, data, name) {
428
+ try {
429
+ const apis = await getApiConfig(data, name);
430
+ const key = apis.key;
431
+ if (!key) throw new Error(`Missing ${name} key in organization apis object`);
432
+
433
+ let webhookName = data.req.url.split("/");
434
+ webhookName = webhookName[webhookName.length - 1];
435
+
436
+ const webhook = apis.webhooks[webhookName];
437
+ if (!webhook) throw new Error(`Webhook ${name} ${webhookName} is not defined`);
438
+
439
+ let eventDataKey = webhook.eventDataKey || apis.eventDataKey;
440
+ if (!eventDataKey) throw new Error(`Webhook ${name} eventKey is not defined`);
441
+
442
+ let eventNameKey = webhook.eventNameKey || apis.eventNameKey;
443
+ if (!eventNameKey) throw new Error(`Webhook ${name} eventNameKey is not defined`);
444
+
445
+ if (!webhook.events) throw new Error(`Webhook ${name} events are not defined`);
446
+
447
+ data.rawBody = "";
448
+ await new Promise((resolve, reject) => {
449
+ data.req.on("data", (chunk) => {
450
+ data.rawBody += chunk.toString();
451
+ });
452
+ data.req.on("end", () => {
453
+ resolve();
454
+ });
455
+ data.req.on("error", (err) => {
456
+ reject(err);
457
+ });
458
+ });
459
+
460
+ let parameters, method;
461
+
462
+ if (webhook.authenticate && webhook.authenticate.method) {
463
+ method = webhook.authenticate.method;
464
+ } else if (apis.authenticate && apis.authenticate.method) {
465
+ method = apis.authenticate.method;
466
+ } else {
467
+ throw new Error(`Webhook ${name} authenticate method is not defined`);
468
+ }
469
+
470
+ if (webhook.authenticate && webhook.authenticate.parameters) {
471
+ parameters = webhook.authenticate.parameters;
472
+ } else if (apis.authenticate && apis.authenticate.parameters) {
473
+ parameters = apis.authenticate.parameters;
474
+ } else {
475
+ throw new Error(`Webhook ${name} authenticate parameters is not defined`);
476
+ }
477
+
478
+ let event;
479
+ if (!method) {
480
+ if (parameters[0] !== parameters[1]) {
481
+ throw new Error("Webhook secret failed for " + name + ". Unauthorized access attempt.");
482
+ }
483
+ event = JSON.parse(data.rawBody);
484
+ } else {
485
+ const service = require(config.path);
486
+ let instance;
487
+ if (config.initialize) {
488
+ instance = new service[config.initialize](key);
489
+ } else {
490
+ instance = new service(key);
491
+ }
492
+
493
+ const methodPath = method.split(".");
494
+ await processOperators(data, "", parameters);
495
+ event = await executeMethod(method, methodPath, instance, parameters);
496
+ }
497
+
498
+ let eventName = getValueFromObject(event, eventNameKey);
499
+ if (!eventName) {
500
+ throw new Error(`Webhook ${name} eventNameKey: ${eventNameKey} could not be found in the event.`);
501
+ }
502
+
503
+ let eventData = getValueFromObject(event, eventDataKey);
504
+ if (!eventData) {
505
+ throw new Error(`Webhook ${name} eventDataKey: ${eventDataKey} could not be found in the event.`);
506
+ }
507
+
508
+ let execute = webhook.events[eventName];
509
+ if (execute) {
510
+ execute = await processOperators(data, event, execute);
511
+ }
512
+
513
+ data.res.writeHead(200, { "Content-Type": "application/json" });
514
+ data.res.end(JSON.stringify({ message: "Webhook received and processed" }));
515
+ return data;
516
+ } catch (error) {
517
+ data.error = error.message;
518
+ data.res.writeHead(400, { "Content-Type": "text/plain" });
519
+ data.res.end(error.message);
520
+ return data;
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Dynamic Operator router (resolves custom DB/Socket variables in JSON trees)
526
+ */
527
+ export async function processOperators(data, event, execute) {
528
+ if (Array.isArray(execute)) {
529
+ for (let index = 0; index < execute.length; index++) {
530
+ execute[index] = await processOperators(data, event, execute[index]);
531
+ }
532
+ } else if (typeof execute === "object" && execute !== null) {
533
+ for (let key of Object.keys(execute)) {
534
+ if (key.startsWith("$") && !["$storage", "$database", "$array", "$filter"].includes(key)) {
535
+ execute[key] = await processOperator(data, event, key, execute[key]);
536
+ } else if (typeof execute[key] === "string" && execute[key].startsWith("$") && !["$storage", "$database", "$array", "$filter"].includes(execute[key])) {
537
+ execute[key] = await processOperator(data, event, execute[key]);
538
+ } else if (Array.isArray(execute[key])) {
539
+ execute[key] = await processOperators(data, event, execute[key]);
540
+ } else if (typeof execute[key] === "object" && execute[key] !== null) {
541
+ execute[key] = await processOperators(data, event, execute[key]);
542
+ }
543
+ }
544
+ } else if (typeof execute === "string" && execute.startsWith("$") && !["$storage", "$database", "$array", "$filter"].includes(execute)) {
545
+ execute = await processOperator(data, event, execute);
546
+ }
547
+
548
+ return execute;
549
+ }
550
+
551
+ /**
552
+ * Resolves individual operator payloads dynamically
553
+ */
554
+ export async function processOperator(data, event, operator, context) {
555
+ let result;
556
+ if (operator.startsWith("$data.")) {
557
+ return getValueFromObject(data, operator.substring(6));
558
+ } else if (operator.startsWith("$req")) {
559
+ return getValueFromObject(data, operator.substring(1));
560
+ } else if (operator.startsWith("$header")) {
561
+ return getValueFromObject(data.req, operator.substring(1));
562
+ } else if (operator.startsWith("$rawBody")) {
563
+ return getValueFromObject(data, operator.substring(1));
564
+ } else if (operator.startsWith("$crud")) {
565
+ let results = context;
566
+ let isObject = false;
567
+ if (!Array.isArray(results)) {
568
+ isObject = true;
569
+ results = [results];
570
+ }
571
+
572
+ for (let i = 0; i < results.length; i++) {
573
+ results[i] = await processOperators(data, event, results[i]);
574
+ results[i] = await crud.send(results[i]);
575
+ if (operator.startsWith("$crud.")) {
576
+ results[i] = getValueFromObject(operator, operator.substring(6));
577
+ }
578
+ results[i] = await processOperators(data, event, results[i]);
579
+ }
580
+
581
+ if (isObject) results = results[0];
582
+ return results;
583
+ } else if (operator.startsWith("$socket")) {
584
+ context = await processOperators(data, event, context);
585
+ result = await wsManager.send(context);
586
+ if (operator.startsWith("$socket.")) {
587
+ result = getValueFromObject(operator, operator.substring(6));
588
+ }
589
+ return await processOperators(data, event, result);
590
+ } else if (operator.startsWith("$api")) {
591
+ context = await processOperators(data, event, context);
592
+ let name = context.method.split(".")[0];
593
+ result = executeScriptWithTimeout(name, context);
594
+ if (operator.startsWith("$api.")) {
595
+ result = getValueFromObject(event, operator.substring(5));
596
+ }
597
+ return await processOperators(data, event, result);
598
+ } else if (operator.startsWith("$event")) {
599
+ if (operator.startsWith("$event.")) {
600
+ result = getValueFromObject(event, operator.substring(7));
601
+ }
602
+ return await processOperators(data, event, result);
603
+ }
604
+
605
+ return operator;
606
+ }
607
+
608
+ /**
609
+ * Resolves organization integration configuration variables
610
+ */
611
+ export async function getApiConfig(data, name) {
612
+ let organization = await crud.getOrganization(data);
613
+ if (organization.error) throw new Error(organization.error);
614
+ if (!organization.apis) throw new Error("Missing apis object in organization object");
615
+ if (!organization.apis[name]) throw new Error(`Missing ${name} in organization apis object`);
616
+ return organization.apis[name];
617
+ }
618
+
619
+ /**
620
+ * Dynamically pulls missing server scripts from database configurations and caches to local disk
621
+ */
622
+ export async function fetchScriptFromDatabaseAndSave(name, moduleConfig, dataContext) {
623
+ const organization_id = dataContext?.organization_id || moduleConfig?.object?.organization_id;
624
+ let queryData = {
625
+ method: "object.read",
626
+ host: moduleConfig.object.hostname,
627
+ array: moduleConfig.array,
628
+ $filter: {
629
+ query: {
630
+ host: { $in: [moduleConfig.object.hostname, "*"] },
631
+ pathname: moduleConfig.object.pathname
632
+ },
633
+ limit: 1
634
+ },
635
+ organization_id
636
+ };
637
+
638
+ let file = await crud.send(queryData);
639
+ let src;
640
+
641
+ if (file && file.object && file.object[0]) {
642
+ src = file.object[0].src;
643
+ } else {
644
+ throw new Error("Script not found in database");
645
+ }
646
+
647
+ const scriptPath = path.join(scriptsDirectory, `${name}.js`);
648
+ await fs.writeFile(scriptPath, src);
649
+
650
+ return src;
651
+ }
886
652
 
887
- // Get all child module paths
888
- return moduleObj.children.map((child) => child.id);
653
+ /**
654
+ * Evaluates module tree dependencies
655
+ */
656
+ export function getModuleDependencies(modulePath) {
657
+ let moduleObj = require.cache[modulePath];
658
+ if (!moduleObj) return [];
659
+ return moduleObj.children.map((child) => child.id);
889
660
  }
890
661
 
891
- function isModuleUsedElsewhere(modulePath, name) {
892
- return Object.keys(require.cache).some((path) => {
893
- const moduleObj = require.cache[path];
894
- // return moduleObj.children.some(child => child.id === modulePath && path !== modulePath);
895
- return moduleObj.children.some((child) => {
896
- // let test = child.id === modulePath && path !== modulePath
897
- // if (test)
898
- // return test
899
- return child.id === modulePath && path !== modulePath;
900
- });
901
- });
662
+ /**
663
+ * Asserts whether a dynamic script is locked by adjacent procedures
664
+ */
665
+ export function isModuleUsedElsewhere(modulePath) {
666
+ return Object.keys(require.cache).some((cachePath) => {
667
+ const moduleObj = require.cache[cachePath];
668
+ return moduleObj.children.some((child) => child.id === modulePath && cachePath !== modulePath);
669
+ });
902
670
  }
903
671
 
904
- function clearModuleCache(name) {
905
- try {
906
- const modulePath = require.resolve(name);
907
- const dependencies = getModuleDependencies(modulePath);
908
-
909
- // Check if the module is a dependency of other modules
910
- // const moduleObj = require.cache[modulePath];
911
- // if (moduleObj && moduleObj.parent) {
912
- // console.log(`Module ${name} is a dependency of other modules.`);
913
- // return;
914
- // }
915
-
916
- // Check if the module is used by other modules
917
- if (isModuleUsedElsewhere(modulePath, name)) {
918
- console.log(`Module ${name} is a dependency of other modules.`);
919
- return;
920
- }
921
-
922
- // Remove the module from the cache
923
- delete require.cache[modulePath];
924
- console.log(`Module ${name} has been removed from cache.`);
925
- // Recursively clear dependencies from cache
926
- dependencies.forEach((depPath) => {
927
- clearModuleCache(depPath);
928
- });
929
- } catch (error) {
930
- console.error(
931
- `Error clearing module cache for ${name}: ${error.message}`
932
- );
933
- }
672
+ /**
673
+ * Safely flushes dynamic script memory footprints from Node processes
674
+ */
675
+ export function clearModuleCache(moduleName) {
676
+ try {
677
+ const modulePath = require.resolve(moduleName);
678
+ const dependencies = getModuleDependencies(modulePath);
679
+
680
+ if (isModuleUsedElsewhere(modulePath)) {
681
+ console.log(`Module ${moduleName} is a dependency of other modules.`);
682
+ return;
683
+ }
684
+
685
+ delete require.cache[modulePath];
686
+ console.log(`Module ${moduleName} has been removed from cache.`);
687
+ dependencies.forEach((depPath) => {
688
+ clearModuleCache(depPath);
689
+ });
690
+ } catch (error) {
691
+ console.error(`Error clearing module cache for ${moduleName}: ${error.message}`);
692
+ }
934
693
  }
935
694
 
936
- // Function to fetch script from database and save to disk
937
- async function fetchScriptFromDatabaseAndSave(name, moduleConfig) {
938
- let data = {
939
- method: "object.read",
940
- host: moduleConfig.object.hostname,
941
- array: moduleConfig.array,
942
- $filter: {
943
- query: {
944
- host: { $in: [moduleConfig.object.hostname, "*"] },
945
- pathname: moduleConfig.object.pathname
946
- },
947
- limit: 1
948
- },
949
- organization_id
950
- };
951
-
952
- let file = await this.crud.send(data);
953
- let src;
954
-
955
- if (file && file.object && file.object[0]) {
956
- src = file.object[0].src;
957
- } else {
958
- throw new Error("Script not found in database");
959
- }
960
-
961
- // Save to disk for future use
962
- const scriptPath = path.join(scriptsDirectory, `${name}.js`);
963
- await fs.writeFile(scriptPath, src);
964
-
965
- return src;
695
+ /**
696
+ * Execution helper: Dynamic deep-method invocation loop
697
+ */
698
+ export async function executeMethod(method, methodPath, instance, params) {
699
+ try {
700
+ switch (methodPath.length) {
701
+ case 1:
702
+ return await instance[methodPath[0]](...params);
703
+ case 2:
704
+ return await instance[methodPath[0]][methodPath[1]](...params);
705
+ case 3:
706
+ return await instance[methodPath[0]][methodPath[1]][methodPath[2]](...params);
707
+ case 4:
708
+ return await instance[methodPath[0]][methodPath[1]][methodPath[2]][methodPath[3]](...params);
709
+ case 5:
710
+ return await instance[methodPath[0]][methodPath[1]][methodPath[2]][methodPath[3]][methodPath[4]](...params);
711
+ case 6:
712
+ return await instance[methodPath[0]][methodPath[1]][methodPath[2]][methodPath[3]][methodPath[4]][methodPath[5]](...params);
713
+ case 7:
714
+ return await instance[methodPath[0]][methodPath[1]][methodPath[2]][methodPath[3]][methodPath[4]][methodPath[5]][methodPath[6]](...params);
715
+ case 8:
716
+ return await instance[methodPath[0]][methodPath[1]][methodPath[2]][methodPath[3]][methodPath[4]][methodPath[5]][methodPath[6]][methodPath[7]](...params);
717
+ default:
718
+ const methodName = methodPath.pop();
719
+ let Method = instance;
720
+ for (let i = 0; i < methodPath.length; i++) {
721
+ Method = Method[methodPath[i]];
722
+ if (Method === undefined) {
723
+ throw new Error(`Method ${methodPath[i]} not found using ${method}.`);
724
+ }
725
+ }
726
+
727
+ if (typeof Method[methodName] !== "function") {
728
+ throw new Error(`Method ${method} is not a function.`);
729
+ }
730
+
731
+ return await Method[methodName](...params);
732
+ }
733
+ } catch (error) {
734
+ throw new Error(error);
735
+ }
966
736
  }
967
737
 
968
- module.exports = CoCreateLazyLoader;
738
+ const LazyLoader = {
739
+ init,
740
+ request,
741
+ executeEndpoint,
742
+ formatRequestBody,
743
+ makeHttpRequest,
744
+ executeScriptWithTimeout,
745
+ api,
746
+ webhooks,
747
+ processOperators,
748
+ processOperator,
749
+ getApiConfig,
750
+ fetchScriptFromDatabaseAndSave
751
+ };
752
+
753
+ export default LazyLoader;