@bitzonegaming/roleplay-engine-framework
Version:
Roleplay Engine Framework
156 lines (155 loc) • 6.82 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApiServer = void 0;
require("reflect-metadata");
const fastify_1 = __importDefault(require("fastify"));
const cors_1 = __importDefault(require("@fastify/cors"));
const types_1 = require("./types");
const error_handler_1 = require("./middleware/error-handler");
const auth_1 = require("./middleware/auth");
const decorators_1 = require("./decorators");
/**
* API Server that manages HTTP endpoints using Fastify and decorators
*/
class ApiServer {
constructor(context, config) {
this.controllers = new Map();
this.context = context;
this.config = config;
this.logger = context.logger;
this.fastify = (0, fastify_1.default)({
logger: false, // We use our own logger
});
this.fastify.setErrorHandler((0, error_handler_1.createErrorHandler)(context));
this.fastify.register(cors_1.default, {
origin: config.cors?.origin ?? '*',
methods: config.cors?.methods ?? ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
credentials: config.cors?.credentials ?? true,
allowedHeaders: config.cors?.allowedHeaders ?? '*',
});
}
/**
* Registers a controller with the API server
*/
registerController(ControllerCtor) {
const controllerMetadata = Reflect.getMetadata(types_1.METADATA_KEYS.CONTROLLER, ControllerCtor);
if (!controllerMetadata) {
throw new Error(`${ControllerCtor.name} is not decorated with @Controller`);
}
const controller = new ControllerCtor(this.context);
this.controllers.set(ControllerCtor, controller);
const routes = Reflect.getMetadata(types_1.METADATA_KEYS.ROUTES, Object.getPrototypeOf(controller)) || [];
for (const route of routes) {
const fullPath = this.joinPaths(controllerMetadata.path, route.path);
const methodName = Reflect.getMetadata(`route:${route.method}:${route.path}`, Object.getPrototypeOf(controller));
if (!methodName) {
this.logger.warn(`No method found for route ${route.method} ${fullPath}`);
continue;
}
const handler = controller[methodName];
if (typeof handler !== 'function') {
this.logger.warn(`Method ${methodName} is not a function on controller`);
continue;
}
const authMetadata = (0, decorators_1.getAuthorizationMetadata)(Object.getPrototypeOf(controller), methodName);
this.fastify.route({
method: route.method,
url: fullPath,
handler: async (request, reply) => {
const authorizedRequest = request;
if (authMetadata?.apiKey) {
await (0, auth_1.validateApiKey)(request, this.config.gamemodeApiKeyHash);
}
if (authMetadata?.sessionToken) {
await (0, auth_1.validateSessionToken)(authorizedRequest, this.context, authMetadata.sessionToken.scope, authMetadata.sessionToken.accessPolicy);
}
const paramMetadata = (0, decorators_1.getParamMetadata)(Object.getPrototypeOf(controller), methodName);
const args = [];
for (const param of paramMetadata.sort((a, b) => a.index - b.index)) {
switch (param.type) {
case decorators_1.ParamType.BODY:
args[param.index] = request.body;
break;
case decorators_1.ParamType.QUERY:
args[param.index] = param.property
? request.query[param.property]
: request.query;
break;
case decorators_1.ParamType.PARAMS:
args[param.index] = param.property
? request.params[param.property]
: request.params;
break;
case decorators_1.ParamType.HEADERS:
args[param.index] = param.property
? request.headers[param.property.toLowerCase()]
: request.headers;
break;
case decorators_1.ParamType.REQUEST:
args[param.index] = authorizedRequest;
break;
case decorators_1.ParamType.REPLY:
args[param.index] = reply;
break;
}
}
const result = await handler.call(controller, ...args);
if (route.statusCode) {
reply.status(route.statusCode);
}
return result;
},
});
this.logger.info(`Registered route: ${route.method} ${fullPath}`);
}
return this;
}
/**
* Starts the API server
*/
async start() {
const port = this.config.port ?? 3000;
const host = this.config.host || '0.0.0.0';
await this.fastify.listen({ port, host });
this.logger.info(`API server listening on ${host}:${port}`);
}
/**
* Stops the API server
*/
async stop() {
for (const controller of this.controllers.values()) {
if (controller.dispose) {
try {
await controller.dispose();
}
catch (error) {
this.logger.error(`Error disposing controller:`, error);
}
}
}
await this.fastify.close();
this.logger.info('API server stopped');
}
/**
* Gets the Fastify instance (for advanced configuration)
*/
getFastify() {
return this.fastify;
}
/**
* Joins paths ensuring proper slashes
*/
joinPaths(base, path) {
if (!base)
return path || '/';
if (!path || path === '/')
return base || '/';
const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base;
const cleanPath = path.startsWith('/') ? path : `/${path}`;
return `${cleanBase}${cleanPath}`;
}
}
exports.ApiServer = ApiServer;