UNPKG

nexting

Version:

A comprehensive, type-safe full-stack library for TypeScript/JavaScript applications. Provides server actions, API controllers, React hooks, error handling, and professional logging - all with complete type safety and inference.

430 lines (417 loc) 13.8 kB
// src/server/helpers/make-server-action.ts import { makeAsyncErrorState, makeAsyncSuccessState } from "async-xtate"; // src/server/helpers/parse-server-error.ts import { StatusCodes as StatusCodes2 } from "http-status-codes"; // src/server/errors/server-error.ts import { StatusCodes } from "http-status-codes"; var ServerError = class extends Error { constructor(props) { super(props.message); this.name = "ServerError"; this.code = props.code ?? "GENERIC_ERROR"; this.status = props.status ?? StatusCodes.INTERNAL_SERVER_ERROR; this.uiMessage = props.uiMessage; } toJSON() { return { message: this.message, code: this.code, status: this.status, uiMessage: this.uiMessage }; } }; // src/server/helpers/parse-server-error.ts import { ZodError } from "zod"; var ParseServerErrorCode = /* @__PURE__ */ ((ParseServerErrorCode2) => { ParseServerErrorCode2["ValidationError"] = "VALIDATION_ERROR"; ParseServerErrorCode2["GenericError"] = "GENERIC_ERROR"; return ParseServerErrorCode2; })(ParseServerErrorCode || {}); var defaultOptions = { defaultMessage: "An unexpected error occurred", defaultCode: "GENERIC_ERROR" /* GenericError */, defaultStatus: StatusCodes2.INTERNAL_SERVER_ERROR, defaultUiMessage: "An unexpected error occurred" }; var parseServerError = (error, { defaultMessage = defaultOptions.defaultMessage, defaultCode = defaultOptions.defaultCode, defaultStatus = defaultOptions.defaultStatus, defaultUiMessage = defaultOptions.defaultUiMessage, parser } = defaultOptions) => { const customError = parser == null ? void 0 : parser(error); if (customError instanceof ServerError) return customError; if (error instanceof ServerError) return error; if (error instanceof ZodError) { return new ServerError({ message: error.message, code: "VALIDATION_ERROR" /* ValidationError */, status: StatusCodes2.BAD_REQUEST, uiMessage: defaultUiMessage }); } if (error instanceof Error) { return new ServerError({ message: error.message, code: defaultCode, status: defaultStatus, uiMessage: defaultUiMessage }); } return new ServerError({ message: defaultMessage, code: defaultCode, status: defaultStatus, uiMessage: defaultUiMessage }); }; var parse_server_error_default = parseServerError; // src/server/helpers/logger/log-levels.ts var LogLevel = /* @__PURE__ */ ((LogLevel2) => { LogLevel2["ERROR"] = "ERROR"; LogLevel2["WARN"] = "WARN"; LogLevel2["INFO"] = "INFO"; LogLevel2["DEBUG"] = "DEBUG"; LogLevel2["TRACE"] = "TRACE"; return LogLevel2; })(LogLevel || {}); var LOG_LEVEL_PRIORITY = { ["ERROR" /* ERROR */]: 0, ["WARN" /* WARN */]: 1, ["INFO" /* INFO */]: 2, ["DEBUG" /* DEBUG */]: 3, ["TRACE" /* TRACE */]: 4 }; var LOG_LEVEL_COLORS = { ["ERROR" /* ERROR */]: "\x1B[31m", // Red ["WARN" /* WARN */]: "\x1B[33m", // Yellow ["INFO" /* INFO */]: "\x1B[36m", // Cyan ["DEBUG" /* DEBUG */]: "\x1B[35m", // Magenta ["TRACE" /* TRACE */]: "\x1B[37m" // White }; var RESET_COLOR = "\x1B[0m"; // src/server/helpers/logger/transports/console-transport.ts var ConsoleTransport = class { log(formattedMessage, entry) { switch (entry.level) { case "ERROR" /* ERROR */: console.error(formattedMessage); break; case "WARN" /* WARN */: console.warn(formattedMessage); break; case "INFO" /* INFO */: console.info(formattedMessage); break; case "DEBUG" /* DEBUG */: case "TRACE" /* TRACE */: console.debug(formattedMessage); break; default: console.log(formattedMessage); } } }; var console_transport_default = ConsoleTransport; // src/server/helpers/logger/formatters/pretty-formatter.ts var PrettyFormatter = class { format(entry) { const timestamp = entry.timestamp.toISOString(); const color = LOG_LEVEL_COLORS[entry.level]; const levelText = `[${entry.level}]`.padEnd(7); let message = `${color}${timestamp} ${levelText}${RESET_COLOR}`; if (entry.context) { message += ` [${entry.context}]`; } if (entry.requestId) { message += ` [${entry.requestId}]`; } message += ` ${entry.message}`; if (entry.metadata && Object.keys(entry.metadata).length > 0) { message += ` Metadata: ${JSON.stringify(entry.metadata, null, 2)}`; } return message; } }; var pretty_formatter_default = PrettyFormatter; // src/server/helpers/logger/logger.ts var Logger = class _Logger { constructor(config = {}) { this.config = { level: config.level ?? "INFO" /* INFO */, context: config.context ?? "APP", formatter: config.formatter ?? new pretty_formatter_default(), transports: config.transports ?? [new console_transport_default()], includeTimestamp: config.includeTimestamp ?? true, includeContext: config.includeContext ?? true, includeMetadata: config.includeMetadata ?? true }; } shouldLog(level) { return LOG_LEVEL_PRIORITY[level] <= LOG_LEVEL_PRIORITY[this.config.level]; } async writeLog(level, message, metadata, requestId) { if (!this.shouldLog(level)) { return; } const entry = { level, message, timestamp: /* @__PURE__ */ new Date(), ...this.config.includeContext && { context: this.config.context }, ...this.config.includeMetadata && metadata && { metadata }, ...requestId && { requestId } }; const formattedMessage = this.config.formatter.format(entry); const logPromises = this.config.transports.map( (transport) => Promise.resolve(transport.log(formattedMessage, entry)) ); await Promise.all(logPromises); } error(message, metadata, requestId) { return this.writeLog("ERROR" /* ERROR */, message, metadata, requestId); } warn(message, metadata, requestId) { return this.writeLog("WARN" /* WARN */, message, metadata, requestId); } info(message, metadata, requestId) { return this.writeLog("INFO" /* INFO */, message, metadata, requestId); } debug(message, metadata, requestId) { return this.writeLog("DEBUG" /* DEBUG */, message, metadata, requestId); } trace(message, metadata, requestId) { return this.writeLog("TRACE" /* TRACE */, message, metadata, requestId); } child(context) { return new _Logger({ ...this.config, context: `${this.config.context}:${context}` }); } setLevel(level) { this.config.level = level; } setFormatter(formatter) { this.config.formatter = formatter; } addTransport(transport) { this.config.transports.push(transport); } }; var logger_default = Logger; // src/server/helpers/logger/request-logger.ts var RequestLogger = class { constructor(logger) { this.logger = logger.child("REQUEST"); } extractRequestData(request) { const url = new URL(request.url); const userAgent = request.headers.get("user-agent") || void 0; const ip = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || void 0; return { method: request.method, url: url.pathname + url.search, userAgent, ip, headers: Object.fromEntries(request.headers.entries()) }; } generateRequestId() { return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } async logRequest(request) { const requestId = this.generateRequestId(); const requestData = this.extractRequestData(request); await this.logger.info( `Incoming ${requestData.method} ${requestData.url}`, { requestData }, requestId ); return requestId; } async logResponse(requestId, statusCode, duration, additionalData) { const message = `Response ${statusCode} (${duration}ms)`; const metadata = { statusCode, duration, ...additionalData }; if (statusCode >= 400) { await this.logger.warn(message, metadata, requestId); } else { await this.logger.info(message, metadata, requestId); } } async logError(requestId, error, additionalData) { await this.logger.error( `Request error: ${error.message}`, { error: error.name, stack: error.stack, ...additionalData }, requestId ); } }; var request_logger_default = RequestLogger; // src/server/helpers/server-logger.ts var defaultLogger = new logger_default({ level: process.env.NODE_ENV === "production" ? "INFO" /* INFO */ : "DEBUG" /* DEBUG */, context: "SERVER" }); var requestLogger = new request_logger_default(defaultLogger); var serverLogger = async (request) => { return await requestLogger.logRequest(request); }; var createLogger = (config) => { return new logger_default(config); }; var createRequestLogger = (logger) => { return new request_logger_default(logger || defaultLogger); }; var server_logger_default = serverLogger; // src/server/helpers/common-handler.ts var validateInput = (input, schema) => { if (!schema) { return { isValid: true, data: input }; } try { const data = schema.parse(input); return { isValid: true, data }; } catch (error) { const parsedError = parse_server_error_default(error).toJSON(); return { isValid: false, error: parsedError }; } }; var handleError = (error, options) => { const parsedError = parse_server_error_default(error, options == null ? void 0 : options.error).toJSON(); ((options == null ? void 0 : options.logger) || defaultLogger).error(parsedError.message, parsedError); return parsedError; }; // src/server/helpers/make-server-action.ts function makeServerAction(actionFn, options) { return async (props) => { try { if (options && "validationSchema" in options && options.validationSchema) { const validation = validateInput(props, options.validationSchema); if (!validation.isValid) { return makeAsyncErrorState(validation.error); } const result2 = await actionFn( validation.data ); return makeAsyncSuccessState(result2); } const result = await actionFn(); return makeAsyncSuccessState(result); } catch (error) { const parsedError = handleError(error, options); return makeAsyncErrorState(parsedError); } }; } var make_server_action_default = makeServerAction; // src/server/helpers/make-api-controller.ts import { StatusCodes as StatusCodes3 } from "http-status-codes"; import { NextResponse } from "next/server"; function makeApiController(controller, options) { return async (request, props) => { var _a, _b, _c; try { const context = { request }; const hasBody = !!(options && typeof options === "object" && "bodySchema" in options && options.bodySchema); const hasQuery = !!(options && typeof options === "object" && "querySchema" in options && options.querySchema); const hasParams = !!(options && typeof options === "object" && "paramsSchema" in options && options.paramsSchema); const args = {}; if (hasQuery) { const url = new URL(request.url); const rawQuery = {}; for (const [key, value] of url.searchParams.entries()) { if (rawQuery[key]) { rawQuery[key] = Array.isArray(rawQuery[key]) ? [...rawQuery[key], value] : [rawQuery[key], value]; } else { rawQuery[key] = value; } } const validated = validateInput( rawQuery, options.querySchema ); if (!validated.isValid) { return NextResponse.json(validated.error, { status: ((_a = validated.error) == null ? void 0 : _a.status) || StatusCodes3.BAD_REQUEST }); } args.query = validated.data; } if (hasBody) { const rawBody = await request.json(); const validated = validateInput( rawBody, options.bodySchema ); if (!validated.isValid) { return NextResponse.json(validated.error, { status: ((_b = validated.error) == null ? void 0 : _b.status) || StatusCodes3.BAD_REQUEST }); } args.body = validated.data; } if (hasParams) { const validated = validateInput( (props == null ? void 0 : props.params) ?? {}, options.paramsSchema ); if (!validated.isValid) { return NextResponse.json(validated.error, { status: ((_c = validated.error) == null ? void 0 : _c.status) || StatusCodes3.BAD_REQUEST }); } args.params = validated.data; } let result; if (hasBody || hasQuery || hasParams) { result = await controller(args, context); } else { result = await controller(context); } const responseStatus = result.status || StatusCodes3.OK; if (responseStatus === StatusCodes3.NO_CONTENT) { return new NextResponse(null, { status: responseStatus }); } return NextResponse.json(result.data, { status: responseStatus }); } catch (error) { const parsedError = handleError(error, options); return NextResponse.json(parsedError, { status: parsedError.status || StatusCodes3.INTERNAL_SERVER_ERROR }); } }; } var make_api_controller_default = makeApiController; export { LogLevel, logger_default as Logger, ParseServerErrorCode, request_logger_default as RequestLogger, ServerError, createLogger, createRequestLogger, defaultLogger, handleError, make_api_controller_default as makeApiController, make_server_action_default as makeServerAction, parse_server_error_default as parseServerError, requestLogger, server_logger_default as serverLogger, validateInput };