@@ -3,7 +3,7 @@ const path = require("path");
3
3
const { URL } = require ( "url" ) ;
4
4
const vm = require ( "vm" ) ;
5
5
const Config = require ( "@cocreate/config" ) ;
6
- const { getValueFromObject } = require ( "@cocreate/utils" ) ;
6
+ const { getValueFromObject, objectToSearchParams } = require ( "@cocreate/utils" ) ;
7
7
8
8
class CoCreateLazyLoader {
9
9
constructor ( server , crud , files ) {
@@ -26,12 +26,16 @@ class CoCreateLazyLoader {
26
26
throw error ; // Halt execution if directory creation fails
27
27
}
28
28
29
+ this . wsManager . on ( "endpoint" , ( data ) => {
30
+ this . executeEndpoint ( data ) ;
31
+ } ) ;
32
+
29
33
this . modules = await Config ( "modules" , false , false ) ;
30
34
if ( ! this . modules ) return ;
31
35
else this . modules = this . modules . modules ;
32
36
33
37
for ( let name of Object . keys ( this . modules ) ) {
34
- this . wsManager . on ( this . modules [ name ] . event , async ( data ) => {
38
+ this . wsManager . on ( this . modules [ name ] . event , ( data ) => {
35
39
this . executeScriptWithTimeout ( name , data ) ;
36
40
} ) ;
37
41
}
@@ -86,15 +90,238 @@ class CoCreateLazyLoader {
86
90
}
87
91
}
88
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 options = { method, headers, body, timeout } ;
135
+
136
+ const response = await makeHttpRequest ( url , options ) ;
137
+ data [ name ] = parseResponse ( response ) ;
138
+
139
+ this . wsManager . send ( data ) ;
140
+ } catch ( error ) {
141
+ data . error = error . message ;
142
+ if ( data . req ) {
143
+ data . res . writeHead ( 400 , {
144
+ "Content-Type" : "text/plain"
145
+ } ) ;
146
+ data . res . end ( `Lazyload Error: ${ error . message } ` ) ;
147
+ }
148
+ if ( data . socket ) {
149
+ this . wsManager . send ( data ) ;
150
+ }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Formats the request body payload based on the specified format type.
156
+ *
157
+ * @param {object | string } payload The data intended for the request body.
158
+ * @param {string } [formatType='json'] The desired format ('json', 'form-urlencoded', 'text', 'multipart', 'xml'). Defaults to 'json'.
159
+ * @returns {{ body: string | Buffer | FormData | null, contentTypeHeader: string | null } }
160
+ * An object containing the formatted body and the corresponding Content-Type header.
161
+ * Returns null body/header on error or for unsupported types.
162
+ */
163
+ formatRequestBody ( payload , formatType = "json" ) {
164
+ let body = null ;
165
+ let contentTypeHeader = null ;
166
+
167
+ try {
168
+ switch ( formatType . toLowerCase ( ) ) {
169
+ case "json" :
170
+ body = JSON . stringify ( payload ) ;
171
+ contentTypeHeader = "application/json; charset=utf-8" ;
172
+ break ;
173
+
174
+ case "form-urlencoded" :
175
+ // In Node.js using querystring:
176
+ // const querystring = require('node:querystring');
177
+ // body = querystring.stringify(payload);
178
+ // Or using URLSearchParams (Node/Browser):
179
+ body = new URLSearchParams ( payload ) . toString ( ) ;
180
+ contentTypeHeader =
181
+ "application/x-www-form-urlencoded; charset=utf-8" ;
182
+ break ;
183
+
184
+ case "text" :
185
+ if ( typeof payload === "string" ) {
186
+ body = payload ;
187
+ } else if (
188
+ payload &&
189
+ typeof payload . toString === "function"
190
+ ) {
191
+ // Attempt conversion for simple objects/values, might need refinement
192
+ body = payload . toString ( ) ;
193
+ } else {
194
+ throw new Error (
195
+ "Payload must be a string or convertible to string for 'text' format."
196
+ ) ;
197
+ }
198
+ contentTypeHeader = "text/plain; charset=utf-8" ;
199
+ break ;
200
+
201
+ case "multipart" :
202
+ // COMPLEX: Requires FormData (browser) or form-data library (Node)
203
+ // Needs specific logic to handle payload structure (identifying files vs fields)
204
+ // const formData = buildFormData(payload); // Placeholder for complex logic
205
+ // body = formData; // The FormData object itself or its stream
206
+ // contentTypeHeader = formData.getHeaders ? formData.getHeaders()['content-type'] : 'multipart/form-data; boundary=...'; // Header includes boundary
207
+ console . warn (
208
+ "Multipart formatting requires specific implementation."
209
+ ) ;
210
+ // For now, return null or throw error
211
+ throw new Error (
212
+ "Multipart formatting not implemented in this basic function."
213
+ ) ;
214
+ break ; // Example: Not fully implemented here
215
+
216
+ case "xml" :
217
+ // COMPLEX: Requires an XML serialization library
218
+ // const xmlString = convertObjectToXml(payload); // Placeholder
219
+ // body = xmlString;
220
+ console . warn (
221
+ "XML formatting requires an external library."
222
+ ) ;
223
+ throw new Error (
224
+ "XML formatting not implemented in this basic function."
225
+ ) ;
226
+ break ; // Example: Not fully implemented here
227
+
228
+ default :
229
+ console . error (
230
+ `Unsupported requestBodyFormat: ${ formatType } `
231
+ ) ;
232
+ // Fallback or throw error
233
+ body = JSON . stringify ( payload ) ; // Default to JSON on unknown? Or error?
234
+ contentTypeHeader = "application/json; charset=utf-8" ;
235
+ }
236
+ } catch ( error ) {
237
+ console . error (
238
+ `Error formatting request body as ${ formatType } :` ,
239
+ error
240
+ ) ;
241
+ return { body : null , contentTypeHeader : null } ; // Return nulls on error
242
+ }
243
+
244
+ return { body, contentTypeHeader } ;
245
+ }
246
+
247
+ /**
248
+ * Makes an HTTP request using node-fetch.
249
+ * @param {string } url - The complete URL to request.
250
+ * @param {string } method - The HTTP method (GET, POST, etc.).
251
+ * @param {object } headers - The request headers object.
252
+ * @param {string|Buffer|null|undefined } body - The formatted request body.
253
+ * @param {number } timeout - Request timeout in milliseconds.
254
+ * @returns {Promise<{status: number, data: any}> } - Resolves with status and parsed response data.
255
+ * @throws {Error } If the request fails or returns a non-ok status.
256
+ */
257
+ async makeHttpRequest ( url , options ) {
258
+ const controller = new this . server . AbortController ( ) ;
259
+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , options . timeout ) ;
260
+ options . signal = controller . signal ;
261
+
262
+ // Remove Content-Type header if there's no body (relevant for GET, DELETE etc.)
263
+ if (
264
+ options . body === undefined &&
265
+ options . headers &&
266
+ options . headers [ "Content-Type" ]
267
+ ) {
268
+ delete options . headers [ "Content-Type" ] ;
269
+ }
270
+
271
+ try {
272
+ const response = await this . server . fetch ( url , options ) ;
273
+ clearTimeout ( timeoutId ) ; // Request finished, clear timeout
274
+
275
+ if ( ! response . ok ) {
276
+ // status >= 200 && status < 300
277
+ const error = new Error (
278
+ `HTTP error! Status: ${ response . status } ${ response . statusText } `
279
+ ) ;
280
+ // Attach structured response info to the error
281
+ error . response = {
282
+ status : response . status ,
283
+ statusText : response . statusText ,
284
+ headers : Object . fromEntries ( response . headers . entries ( ) ) ,
285
+ data : parseResponse ( response ) // Include parsed error body
286
+ } ;
287
+ throw error ;
288
+ }
289
+ return response ;
290
+ } catch ( error ) {
291
+ clearTimeout ( timeoutId ) ;
292
+ if ( error . name === "AbortError" ) {
293
+ console . error (
294
+ `Request timed out after ${ options . timeout } ms: ${ options . method } ${ url } `
295
+ ) ;
296
+ throw new Error (
297
+ `Request Timeout: API call exceeded ${ options . timeout } ms`
298
+ ) ;
299
+ }
300
+
301
+ // If it already has response info (from !response.ok), rethrow it
302
+ if ( error . response ) {
303
+ throw error ;
304
+ }
305
+ // Otherwise, wrap other errors (network, DNS, etc.)
306
+ console . error (
307
+ `Network/Request Error: ${ options . method } ${ url } ` ,
308
+ error
309
+ ) ;
310
+ throw new Error ( `Network/Request Error: ${ error . message } ` ) ;
311
+ }
312
+ }
313
+
89
314
async executeScriptWithTimeout ( name , data ) {
90
315
try {
91
316
if (
92
317
this . modules [ name ] . initialize ||
93
318
this . modules [ name ] . initialize === ""
94
319
) {
95
- if ( data . req )
320
+ if ( data . req ) {
96
321
data = await this . webhooks ( this . modules [ name ] , data , name ) ;
97
- else data = await this . api ( this . modules [ name ] , data ) ;
322
+ } else {
323
+ data = await this . api ( this . modules [ name ] , data ) ;
324
+ }
98
325
} else {
99
326
if ( ! this . modules [ name ] . content ) {
100
327
if ( this . modules [ name ] . path )
@@ -124,7 +351,7 @@ class CoCreateLazyLoader {
124
351
}
125
352
126
353
if ( this . modules [ name ] . content ) {
127
- data . apis = await this . getApiKey ( data , name ) ;
354
+ data . apis = await this . getApiConfig ( data , name ) ;
128
355
data . crud = this . crud ;
129
356
data = await this . modules [ name ] . content . send ( data ) ;
130
357
delete data . apis ;
@@ -218,7 +445,7 @@ class CoCreateLazyLoader {
218
445
const methodPath = data . method . split ( "." ) ;
219
446
const name = methodPath . shift ( ) ;
220
447
221
- const apis = await this . getApiKey ( data , name ) ;
448
+ const apis = await this . getApiConfig ( data , name ) ;
222
449
223
450
const key = apis . key ;
224
451
if ( ! key )
@@ -320,7 +547,7 @@ class CoCreateLazyLoader {
320
547
321
548
async webhooks ( config , data , name ) {
322
549
try {
323
- const apis = await this . getApiKey ( data , name ) ;
550
+ const apis = await this . getApiConfig ( data , name ) ;
324
551
325
552
const key = apis . key ;
326
553
if ( ! key )
@@ -568,7 +795,7 @@ class CoCreateLazyLoader {
568
795
return operator ;
569
796
}
570
797
571
- async getApiKey ( data , name ) {
798
+ async getApiConfig ( data , name ) {
572
799
let organization = await this . crud . getOrganization ( data ) ;
573
800
if ( organization . error ) throw new Error ( organization . error ) ;
574
801
if ( ! organization . apis )
0 commit comments