@spfn/core
Version:
SPFN Framework Core - File-based routing, transactions, repository pattern
371 lines (366 loc) • 12.6 kB
JavaScript
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers.js';
// src/client/nextjs/proxy.ts
// src/client/nextjs/interceptor.ts
function matchPath(path, pattern) {
if (pattern === "*") {
return true;
}
if (pattern instanceof RegExp) {
return pattern.test(path);
}
const regexPattern = pattern.replace(/\*/g, ".*").replace(/:[^/]+/g, "[^/]+").replace(/\//g, "\\/");
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(path);
}
function matchMethod(method, pattern) {
if (!pattern) {
return true;
}
if (typeof pattern === "string") {
return method.toUpperCase() === pattern.toUpperCase();
}
return pattern.some((m) => m.toUpperCase() === method.toUpperCase());
}
function filterMatchingInterceptors(rules, path, method) {
return rules.filter((rule) => {
return matchPath(path, rule.pathPattern) && matchMethod(method, rule.method);
});
}
async function executeRequestInterceptors(context, interceptors) {
let index = 0;
const next = async () => {
if (index >= interceptors.length) {
return;
}
const interceptor = interceptors[index];
index++;
await interceptor(context, next);
};
await next();
}
async function executeResponseInterceptors(context, interceptors) {
let index = 0;
const next = async () => {
if (index >= interceptors.length) {
return;
}
const interceptor = interceptors[index];
index++;
await interceptor(context, next);
};
await next();
}
// src/client/nextjs/registry.ts
var InterceptorRegistry = class {
interceptors = /* @__PURE__ */ new Map();
/**
* Register interceptors for a package
*
* @param packageName - Unique package identifier (e.g., 'auth', 'storage')
* @param interceptors - Array of interceptor rules
*
* @example
* ```typescript
* registerInterceptors('auth', [
* {
* pathPattern: '/_auth/*',
* request: async (ctx, next) => { ... }
* }
* ]);
* ```
*/
register(packageName, interceptors) {
console.log(`[SPFN Registry] Registering ${interceptors.length} interceptors for package "${packageName}"`);
if (this.interceptors.has(packageName)) {
console.warn(
`[SPFN Registry] Interceptors for "${packageName}" already registered. Overwriting.`
);
}
this.interceptors.set(packageName, interceptors);
console.log(`[SPFN Registry] Successfully registered interceptors for "${packageName}". Total packages: ${this.getPackageNames().length}`);
}
/**
* Get all registered interceptors
*
* @param exclude - Package names to exclude
* @returns Flat array of all interceptor rules
*/
getAll(exclude = []) {
const all = [];
for (const [packageName, interceptors] of this.interceptors.entries()) {
if (!exclude.includes(packageName)) {
all.push(...interceptors);
}
}
return all;
}
/**
* Get interceptors for specific package
*
* @param packageName - Package identifier
* @returns Interceptor rules or undefined
*/
get(packageName) {
return this.interceptors.get(packageName);
}
/**
* Get list of registered package names
*/
getPackageNames() {
return Array.from(this.interceptors.keys());
}
/**
* Check if package has registered interceptors
*/
has(packageName) {
return this.interceptors.has(packageName);
}
/**
* Unregister interceptors for a package
*
* @param packageName - Package identifier
*/
unregister(packageName) {
this.interceptors.delete(packageName);
}
/**
* Clear all registered interceptors
*
* Useful for testing
*/
clear() {
this.interceptors.clear();
}
/**
* Get total count of registered interceptors
*/
count() {
let total = 0;
for (const interceptors of this.interceptors.values()) {
total += interceptors.length;
}
return total;
}
};
var interceptorRegistry = new InterceptorRegistry();
function registerInterceptors(packageName, interceptors) {
interceptorRegistry.register(packageName, interceptors);
}
// src/client/nextjs/proxy.ts
function getApiUrl(config) {
return config?.apiUrl || process.env.SERVER_API_URL || process.env.SPFN_API_URL || "http://localhost:8790";
}
async function handleProxy(request, context, method, config) {
try {
const params = "then" in context.params ? await context.params : context.params;
const pathSegments = params.path;
const path = `/${pathSegments.join("/")}`;
const query = {};
request.nextUrl.searchParams.forEach((value, key) => {
const existing = query[key];
if (existing) {
query[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
} else {
query[key] = value;
}
});
const cookieStore = await cookies();
const cookieMap = /* @__PURE__ */ new Map();
cookieStore.getAll().forEach((cookie) => {
cookieMap.set(cookie.name, cookie.value);
});
const headers = {
"Content-Type": "application/json"
};
let body = void 0;
const contentType = request.headers.get("Content-Type");
if (method === "POST" || method === "PUT" || method === "PATCH") {
if (contentType?.includes("application/json")) {
const text = await request.text();
if (text) {
try {
body = JSON.parse(text);
} catch (error) {
body = text;
}
}
} else if (contentType?.includes("multipart/form-data")) {
body = await request.formData();
} else {
body = await request.text();
}
}
const requestContext = {
path,
method,
headers,
body,
query,
cookies: cookieMap,
request,
metadata: {}
};
const rules = config?.interceptors || [];
console.log(`[SPFN Proxy] Handling ${method} ${path}`);
console.log(`[SPFN Proxy] Total available interceptor rules: ${rules.length}`);
const matchedRules = filterMatchingInterceptors(rules, path, method);
console.log(`[SPFN Proxy] Matched ${matchedRules.length} interceptor rules for this request`);
const requestInterceptors = matchedRules.map((rule) => rule.request).filter((interceptor) => !!interceptor);
console.log(`[SPFN Proxy] Executing ${requestInterceptors.length} request interceptors`);
await executeRequestInterceptors(requestContext, requestInterceptors);
const apiUrl = getApiUrl(config);
const queryString = Object.entries(query).flatMap(
([key, value]) => Array.isArray(value) ? value.map((v) => `${key}=${v}`) : [`${key}=${value}`]
).join("&");
const url = `${apiUrl}${path}${queryString ? `?${queryString}` : ""}`;
const init = {
method,
headers: requestContext.headers
};
if (requestContext.body !== void 0) {
if (requestContext.body instanceof FormData) {
init.body = requestContext.body;
delete requestContext.headers["Content-Type"];
} else if (typeof requestContext.body === "string") {
init.body = requestContext.body;
} else {
init.body = JSON.stringify(requestContext.body);
}
}
if (config?.debug) {
console.log(`[SPFN Proxy] Calling ${url}`);
console.log(`[SPFN Proxy] Headers:`, requestContext.headers);
}
const response = await fetch(url, init);
const responseText = await response.text();
let responseBody;
try {
responseBody = JSON.parse(responseText);
} catch (error) {
responseBody = responseText;
}
const responseContext = {
path,
method,
request: {
headers: requestContext.headers,
body: requestContext.body
},
response: {
status: response.status,
statusText: response.statusText,
headers: response.headers,
body: responseBody
},
setCookies: [],
metadata: requestContext.metadata
// Pass metadata from request
};
const responseInterceptors = matchedRules.map((rule) => rule.response).filter((interceptor) => !!interceptor);
if (config?.debug) {
console.log(`[SPFN Proxy] Response interceptors: ${responseInterceptors.length}`);
}
await executeResponseInterceptors(responseContext, responseInterceptors);
const nextResponse = NextResponse.json(responseContext.response.body, {
status: responseContext.response.status,
statusText: responseContext.response.statusText
});
for (const cookie of responseContext.setCookies) {
const cookieString = [`${cookie.name}=${cookie.value}`];
if (cookie.options?.httpOnly) {
cookieString.push("HttpOnly");
}
if (cookie.options?.secure) {
cookieString.push("Secure");
}
if (cookie.options?.sameSite) {
cookieString.push(`SameSite=${cookie.options.sameSite}`);
}
if (cookie.options?.maxAge !== void 0) {
cookieString.push(`Max-Age=${cookie.options.maxAge}`);
}
if (cookie.options?.path) {
cookieString.push(`Path=${cookie.options.path}`);
}
if (cookie.options?.domain) {
cookieString.push(`Domain=${cookie.options.domain}`);
}
nextResponse.headers.append("Set-Cookie", cookieString.join("; "));
}
const setCookieHeaders = response.headers.get("Set-Cookie");
if (setCookieHeaders) {
nextResponse.headers.append("Set-Cookie", setCookieHeaders);
}
if (config?.debug) {
console.log(`[SPFN Proxy] Response: ${responseContext.response.status}`);
}
return nextResponse;
} catch (error) {
console.error("[SPFN Proxy] Error:", error);
return NextResponse.json(
{
success: false,
error: {
code: "PROXY_ERROR",
message: error instanceof Error ? error.message : "Unknown proxy error"
}
},
{ status: 500 }
);
}
}
function createProxy(config) {
const finalConfig = {
autoDiscoverInterceptors: true,
...config
};
let allInterceptors = [];
console.log("[SPFN Proxy] Creating proxy with config:", {
autoDiscoverInterceptors: finalConfig.autoDiscoverInterceptors,
customInterceptors: finalConfig.interceptors?.length || 0,
disableAutoInterceptors: finalConfig.disableAutoInterceptors || []
});
if (finalConfig.autoDiscoverInterceptors) {
const registeredPackages = interceptorRegistry.getPackageNames();
console.log("[SPFN Proxy] Registered packages in registry:", registeredPackages);
const autoInterceptors = interceptorRegistry.getAll(
finalConfig.disableAutoInterceptors || []
);
allInterceptors.push(...autoInterceptors);
console.log("[SPFN Proxy] Auto-discovered interceptors from packages:", registeredPackages);
console.log(`[SPFN Proxy] Total auto-discovered interceptors: ${autoInterceptors.length}`);
}
if (finalConfig.interceptors) {
allInterceptors.push(...finalConfig.interceptors);
console.log(`[SPFN Proxy] Custom interceptors: ${finalConfig.interceptors.length}`);
}
const proxyConfig = {
...finalConfig,
interceptors: allInterceptors
};
console.log(`[SPFN Proxy] Total interceptors loaded: ${allInterceptors.length}`);
return {
GET: async (request, context) => handleProxy(request, context, "GET", proxyConfig),
POST: async (request, context) => handleProxy(request, context, "POST", proxyConfig),
PUT: async (request, context) => handleProxy(request, context, "PUT", proxyConfig),
PATCH: async (request, context) => handleProxy(request, context, "PATCH", proxyConfig),
DELETE: async (request, context) => handleProxy(request, context, "DELETE", proxyConfig)
};
}
var defaultProxy = null;
function getDefaultProxy() {
if (!defaultProxy) {
console.log("[SPFN Proxy] Initializing default proxy with auto-discovery");
defaultProxy = createProxy();
}
return defaultProxy;
}
var GET = async (request, context) => getDefaultProxy().GET(request, context);
var POST = async (request, context) => getDefaultProxy().POST(request, context);
var PUT = async (request, context) => getDefaultProxy().PUT(request, context);
var PATCH = async (request, context) => getDefaultProxy().PATCH(request, context);
var DELETE = async (request, context) => getDefaultProxy().DELETE(request, context);
export { DELETE, GET, PATCH, POST, PUT, createProxy, executeRequestInterceptors, executeResponseInterceptors, filterMatchingInterceptors, interceptorRegistry, matchMethod, matchPath, registerInterceptors };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map