Skip to content

Commit ffc3223

Browse files
committed
feat:: add executeEndpoint, formatRequestBody, and makeHttpRequest functions for improved API interaction
- Added async function `executeEndpoint` to handle API requests based on given method and endpoint. It processes operators, retrieves API configurations, and constructs the request URL and options before sending an HTTP request. Handles exceptions to provide error messages through WebSocket or HTTP response. - Introduced `formatRequestBody` to format different types of request body payloads based on specified format type like 'json', 'form-urlencoded', 'text', 'multipart', 'xml'. It generates the body content string or buffer and the corresponding Content-Type header. - Implemented `makeHttpRequest` to manage making HTTP requests using node-fetch with support for method, headers, body, and timeout. It uses AbortController for handling timeouts and processes request and response data including error handling for unsuccessful responses or network errors.
1 parent b66cfb3 commit ffc3223

File tree

1 file changed

+235
-8
lines changed

1 file changed

+235
-8
lines changed

src/server.js

Lines changed: 235 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const path = require("path");
33
const { URL } = require("url");
44
const vm = require("vm");
55
const Config = require("@cocreate/config");
6-
const { getValueFromObject } = require("@cocreate/utils");
6+
const { getValueFromObject, objectToSearchParams } = require("@cocreate/utils");
77

88
class 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

Comments
 (0)