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
JavaScript
// 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
};