@withstudiocms/effect
Version:
Effect-TS Utilities for Astro
139 lines (138 loc) • 4.51 kB
JavaScript
import { runEffect } from "../index.js";
import { getCorsHeaders, validateRequest } from "./utils/api-route.js";
const defineAPIRoute = (context) => async (fn) => await runEffect(fn(context));
const createEffectAPIRoute = (fn) => {
return async (context) => {
return await runEffect(fn(context));
};
};
const withEffectAPI = (fn, options = {}) => {
return async (context) => {
try {
const corsHeaders = getCorsHeaders(context, options.cors);
if (context.request.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: corsHeaders
});
}
let processedContext = context;
if (options.onBeforeEffect) {
processedContext = await options.onBeforeEffect(context);
}
if (options.validate) {
const validationError = await validateRequest(processedContext, options.validate);
if (validationError) {
const errorResponse = new Response(
JSON.stringify({ error: "Validation failed", details: validationError }),
{ status: 400, headers: { "Content-Type": "application/json", ...corsHeaders } }
);
return errorResponse;
}
}
let effectPromise = runEffect(fn(processedContext));
if (options.timeout) {
effectPromise = Promise.race([
effectPromise,
new Promise(
(_, reject) => setTimeout(() => reject(new Error("Request timeout")), options.timeout)
)
]);
}
const response = await effectPromise;
if (corsHeaders && Object.keys(corsHeaders).length > 0) {
Object.entries(corsHeaders).forEach(([key, value]) => {
response.headers.set(key, value);
});
}
return options.onSuccess ? await options.onSuccess(response, processedContext) : response;
} catch (error) {
const corsHeaders = getCorsHeaders(context, options.cors);
if (options.onError) {
const errorResponse = await options.onError(error, context);
Object.entries(corsHeaders).forEach(([key, value]) => {
errorResponse.headers.set(key, value);
});
return errorResponse;
}
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : "Internal Server Error"
}),
{
status: error instanceof Error && error.message === "Request timeout" ? 408 : 500,
headers: {
"Content-Type": "application/json",
...corsHeaders
}
}
);
}
};
};
const createEffectAPIRoutes = (handlers, config = {}) => {
const routes = {};
for (const [method, handler] of Object.entries(handlers)) {
if (handler) {
const methodKey = method;
const methodConfig = {
...config,
...config.methods?.[methodKey] || {}
};
const { methods: _droppedMethods, ...finalConfig } = methodConfig;
routes[methodKey] = withEffectAPI(handler, finalConfig);
}
}
return routes;
};
class EffectAPIRouteBuilder {
handlers;
config = {};
constructor(handlers = {}) {
this.handlers = handlers;
}
// Set global configuration
withGlobalConfig(config) {
this.config = { ...this.config, ...config };
return this;
}
addHandler(method, handler, options) {
const newHandlers = { ...this.handlers, [method]: handler };
const newBuilder = new EffectAPIRouteBuilder(newHandlers);
newBuilder.config = { ...this.config };
if (options) {
newBuilder.config.methods = { ...this.config.methods, [method]: options };
}
return newBuilder;
}
// Add handlers with fluent interface - each method returns a new type that includes the added method
get(handler, options) {
return this.addHandler("GET", handler, options);
}
post(handler, options) {
return this.addHandler("POST", handler, options);
}
put(handler, options) {
return this.addHandler("PUT", handler, options);
}
delete(handler, options) {
return this.addHandler("DELETE", handler, options);
}
patch(handler, options) {
return this.addHandler("PATCH", handler, options);
}
all(handler, options) {
return this.addHandler("ALL", handler, options);
}
// Build the final routes with precise typing
build() {
return createEffectAPIRoutes(this.handlers, this.config);
}
}
export {
EffectAPIRouteBuilder,
createEffectAPIRoute,
createEffectAPIRoutes,
defineAPIRoute,
withEffectAPI
};