@cocreate/cli 1.62.0 → 1.64.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.
@@ -291,9 +291,9 @@ module.exports = {
291
291
  "fs/webpack"
292
292
  ]
293
293
  },
294
- certificates: {
295
- path: "../CoCreate-certificates",
296
- repo: "github.com/CoCreate-app/CoCreate-certificates.git",
294
+ "server-tls": {
295
+ path: "../CoCreate-server-tls",
296
+ repo: "github.com/CoCreate-app/CoCreate-server-tls.git",
297
297
  exclude: [
298
298
  "fs/webpack"
299
299
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/cli",
3
- "version": "1.62.0",
3
+ "version": "1.64.0",
4
4
  "description": "Polyrepo management bash CLI tool. Run all git commands and yarn commands on multiple repositories. Also includes a few custom macros for cloning, installing, etc.",
5
5
  "keywords": [
6
6
  "cli",
@@ -48,9 +48,9 @@
48
48
  "coc": "src/coc.js"
49
49
  },
50
50
  "dependencies": {
51
- "@cocreate/certificates": "^1.10.0",
52
- "@cocreate/config": "^1.17.0",
53
- "@cocreate/file": "^1.23.0",
51
+ "@cocreate/certificates": "^1.11.0",
52
+ "@cocreate/config": "^1.19.0",
53
+ "@cocreate/file": "^1.24.0",
54
54
  "@google/genai": "2.15.0"
55
55
  }
56
56
  }
@@ -0,0 +1,753 @@
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.get("modules");
67
+ if (!retrievedConfig) return;
68
+
69
+ modulesList = retrievedConfig.modules || {};
70
+
71
+ return LazyLoader;
72
+ }
73
+
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
+ }
113
+ }
114
+
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
+ }
652
+
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);
660
+ }
661
+
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
+ });
670
+ }
671
+
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
+ }
693
+ }
694
+
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
+ }
736
+ }
737
+
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;
@@ -0,0 +1,126 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { getConfig } = require("../getConfig");
4
+
5
+ module.exports = async function storage(directory, args) {
6
+ if (args && !Array.isArray(args)) args = [args];
7
+
8
+ const isWatch =
9
+ args &&
10
+ (args.includes("-w") || args.includes("--watch"));
11
+
12
+ const cwd = process.cwd();
13
+
14
+ // CRUD Server storage directory
15
+ const storageDestinationDirectory = path.resolve(cwd, "storage");
16
+
17
+ await fs.promises.mkdir(storageDestinationDirectory, {
18
+ recursive: true
19
+ });
20
+
21
+ const config = await getConfig(cwd);
22
+
23
+ if (!config || !config.modules) {
24
+ console.error("Failed to read or parse CoCreate.config.js.");
25
+ return;
26
+ }
27
+
28
+ const storages = [];
29
+
30
+ for (const module of Object.values(config.modules)) {
31
+ if (!module.path) continue;
32
+
33
+ const normalizedPath = path.normalize(module.path);
34
+
35
+ // Only process storage repositories
36
+ if (
37
+ !normalizedPath.includes(
38
+ `${path.sep}CoCreate-storages${path.sep}`
39
+ )
40
+ )
41
+ continue;
42
+
43
+ // Repository name (ex: CoCreate-mongodb)
44
+ const repositoryName = path.basename(normalizedPath);
45
+
46
+ // Storage name (ex: mongodb)
47
+ const storageName = repositoryName.replace(/^CoCreate-/, "");
48
+
49
+ storages.push({
50
+ name: storageName,
51
+ storageSource: path.resolve(
52
+ module.path,
53
+ "dist",
54
+ `${storageName}.js`
55
+ ),
56
+ storageDestination: path.resolve(
57
+ storageDestinationDirectory,
58
+ `${storageName}.js`
59
+ )
60
+ });
61
+ }
62
+
63
+ if (!storages.length) {
64
+ console.log("No storage repositories found.");
65
+ return;
66
+ }
67
+
68
+ // Initial copy
69
+ for (const storage of storages) {
70
+ await copyStorage(storage);
71
+ }
72
+
73
+ if (!isWatch) return;
74
+
75
+ console.log("\nWatching storage files...\n");
76
+
77
+ for (const storage of storages) {
78
+ if (!fs.existsSync(storage.storageSource)) {
79
+ console.warn(
80
+ `Storage source not found: ${storage.storageSource}`
81
+ );
82
+ continue;
83
+ }
84
+
85
+ let debounce;
86
+
87
+ fs.watch(storage.storageSource, (eventType) => {
88
+ if (eventType !== "change") return;
89
+
90
+ clearTimeout(debounce);
91
+
92
+ debounce = setTimeout(async () => {
93
+ await copyStorage(storage);
94
+ }, 100);
95
+ });
96
+
97
+ console.log(
98
+ `Watching ${storage.name}: ${storage.storageSource}`
99
+ );
100
+ }
101
+ };
102
+
103
+ async function copyStorage(storage) {
104
+ try {
105
+ if (!fs.existsSync(storage.storageSource)) {
106
+ console.warn(
107
+ `Storage source not found: ${storage.storageSource}`
108
+ );
109
+ return;
110
+ }
111
+
112
+ await fs.promises.copyFile(
113
+ storage.storageSource,
114
+ storage.storageDestination
115
+ );
116
+
117
+ console.log(
118
+ `Copied ${storage.name}.js`
119
+ );
120
+ } catch (err) {
121
+ console.error(
122
+ `Failed copying ${storage.name}:`,
123
+ err.message
124
+ );
125
+ }
126
+ }
@@ -2,6 +2,10 @@ let fileModule = require("@cocreate/file");
2
2
  // Safely resolve default export if it's wrapped as an ESM module
3
3
  const file = fileModule && fileModule.default ? fileModule.default : fileModule;
4
4
 
5
+ // Required to watch more files than the default limit on Linux systems. You may need to run these commands in your terminal:
6
+ // echo "fs.inotify.max_user_watches=524288" | sudo tee -a /etc/sysctl.conf
7
+ // echo "fs.inotify.max_user_instances=1024" | sudo tee -a /etc/sysctl.conf
8
+ // sudo sysctl -p
5
9
  const path = require("path");
6
10
  const fs = require("fs");
7
11
  const { getConfig } = require("../getConfig");