@@ -3,7 +3,7 @@ const path = require("path");
33const { URL } = require ( "url" ) ;
44const vm = require ( "vm" ) ;
55const Config = require ( "@cocreate/config" ) ;
6- const { getValueFromObject } = require ( "@cocreate/utils" ) ;
6+ const { getValueFromObject, objectToSearchParams } = require ( "@cocreate/utils" ) ;
77
88class CoCreateLazyLoader {
99 constructor ( server , crud , files ) {
@@ -26,12 +26,16 @@ class CoCreateLazyLoader {
2626 throw error ; // Halt execution if directory creation fails
2727 }
2828
29+ this . wsManager . on ( "endpoint" , ( data ) => {
30+ this . executeEndpoint ( data ) ;
31+ } ) ;
32+
2933 this . modules = await Config ( "modules" , false , false ) ;
3034 if ( ! this . modules ) return ;
3135 else this . modules = this . modules . modules ;
3236
3337 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 ) => {
3539 this . executeScriptWithTimeout ( name , data ) ;
3640 } ) ;
3741 }
@@ -86,15 +90,238 @@ class CoCreateLazyLoader {
8690 }
8791 }
8892
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+
89314 async executeScriptWithTimeout ( name , data ) {
90315 try {
91316 if (
92317 this . modules [ name ] . initialize ||
93318 this . modules [ name ] . initialize === ""
94319 ) {
95- if ( data . req )
320+ if ( data . req ) {
96321 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+ }
98325 } else {
99326 if ( ! this . modules [ name ] . content ) {
100327 if ( this . modules [ name ] . path )
@@ -124,7 +351,7 @@ class CoCreateLazyLoader {
124351 }
125352
126353 if ( this . modules [ name ] . content ) {
127- data . apis = await this . getApiKey ( data , name ) ;
354+ data . apis = await this . getApiConfig ( data , name ) ;
128355 data . crud = this . crud ;
129356 data = await this . modules [ name ] . content . send ( data ) ;
130357 delete data . apis ;
@@ -218,7 +445,7 @@ class CoCreateLazyLoader {
218445 const methodPath = data . method . split ( "." ) ;
219446 const name = methodPath . shift ( ) ;
220447
221- const apis = await this . getApiKey ( data , name ) ;
448+ const apis = await this . getApiConfig ( data , name ) ;
222449
223450 const key = apis . key ;
224451 if ( ! key )
@@ -320,7 +547,7 @@ class CoCreateLazyLoader {
320547
321548 async webhooks ( config , data , name ) {
322549 try {
323- const apis = await this . getApiKey ( data , name ) ;
550+ const apis = await this . getApiConfig ( data , name ) ;
324551
325552 const key = apis . key ;
326553 if ( ! key )
@@ -568,7 +795,7 @@ class CoCreateLazyLoader {
568795 return operator ;
569796 }
570797
571- async getApiKey ( data , name ) {
798+ async getApiConfig ( data , name ) {
572799 let organization = await this . crud . getOrganization ( data ) ;
573800 if ( organization . error ) throw new Error ( organization . error ) ;
574801 if ( ! organization . apis )
0 commit comments