restifyx.js
Version:
Advanced API endpoint handler system with automatic documentation
504 lines • 17.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Endpoint = void 0;
exports.CreateEndpoint = CreateEndpoint;
const joi_1 = __importDefault(require("joi"));
const logger_1 = require("../utils/logger");
/**
* Fluent Function Code for defining endpoints
*
* @example
* \`\`\`typescript
* const endpoint = new Endpoint()
* .setName('Get User')
* .setDescription('Get user by ID')
* .setPath('/api/users/:id')
* .setMethod('GET')
* .setTags(['users'])
* .addResponse({
* 200: { message: 'Success' },
* 404: 'User not found'
* })
* .addQuery({
* include: 'string'
* })
* .setHandler(async (header, body, query, params) => {
* // Implementation
* SendData.json({ user: { id: params.id } });
* })
* \`\`\`
*/
class Endpoint {
constructor() {
this.metadata = {};
this.querySchema = {};
this.bodySchema = {};
this.headerSchema = {};
this.cookieSchema = {};
this.paramsSchema = {};
this.responses = {};
}
/**
* Set the name of the endpoint
*
* @param name - The name of the endpoint
* @returns The Endpoint instance for chaining
*/
setName(name) {
this.metadata.name = name;
return this;
}
/**
* Set the description of the endpoint
*
* @param description - The description of the endpoint
* @returns The Endpoint instance for chaining
*/
setDescription(description) {
this.metadata.description = description;
return this;
}
/**
* Set the path of the endpoint
*
* @param path - The path of the endpoint (e.g., '/api/users/:id')
* @returns The Endpoint instance for chaining
*/
setPath(path) {
this.metadata.path = path;
return this;
}
/**
* Set the HTTP method of the endpoint
*
* @param method - The HTTP method (GET, POST, PUT, DELETE, etc.)
* @returns The Endpoint instance for chaining
*/
setMethod(method) {
this.metadata.method = method;
return this;
}
/**
* Set tags for the endpoint (used for Swagger documentation)
*
* @param tags - Array of tags
* @returns The Endpoint instance for chaining
*/
setTags(tags) {
this.metadata.tags = tags;
return this;
}
/**
* Add middleware to the endpoint
*
* @param middleware - Express middleware function(s)
* @returns The Endpoint instance for chaining
*/
addMiddleware(...middleware) {
if (!this.metadata.middleware) {
this.metadata.middleware = [];
}
this.metadata.middleware.push(...middleware);
return this;
}
/**
* Set rate limiting options for the endpoint
*
* @param options - Rate limiting options
* @returns The Endpoint instance for chaining
*/
setRateLimit(options) {
this.rateLimit = options;
return this;
}
/**
* Set caching options for the endpoint
*
* @param duration - Cache duration in seconds
* @param keyGenerator - Optional function to generate cache key
* @returns The Endpoint instance for chaining
*/
setCache(duration, keyGenerator) {
this.cacheOptions = { duration, keyGenerator };
return this;
}
/**
* Define expected query parameters and their types
*
* @param schema - Schema definition for query parameters
* @returns The Endpoint instance for chaining
*/
addQuery(schema) {
this.querySchema = { ...this.querySchema, ...schema };
return this;
}
/**
* Define expected body parameters and their types
*
* @param schema - Schema definition for request body
* @returns The Endpoint instance for chaining
*/
addBody(schema) {
this.bodySchema = { ...this.bodySchema, ...schema };
return this;
}
/**
* Define expected header parameters and their types
*
* @param schema - Schema definition for request headers
* @returns The Endpoint instance for chaining
*/
addHeader(schema) {
this.headerSchema = { ...this.headerSchema, ...schema };
return this;
}
/**
* Define expected cookie parameters and their types
*
* @param schema - Schema definition for cookies
* @returns The Endpoint instance for chaining
*/
addCookie(schema) {
this.cookieSchema = { ...this.cookieSchema, ...schema };
return this;
}
/**
* Define expected path parameters and their types
*
* @param schema - Schema definition for path parameters
* @returns The Endpoint instance for chaining
*/
addParams(schema) {
this.paramsSchema = { ...this.paramsSchema, ...schema };
return this;
}
/**
* Define response schemas for different status codes
*
* @param responses - Object mapping status codes to response schemas
* @returns The Endpoint instance for chaining
*/
addResponse(responses) {
this.responses = { ...this.responses, ...responses };
return this;
}
/**
* Set the handler function for the endpoint
*
* @param handler - The function that handles the endpoint request
* @returns The Endpoint instance for chaining
*/
setHandler(handler) {
this.handler = handler;
return this;
}
/**
* Build the endpoint metadata and handler
*
* @returns The endpoint metadata
* @internal
*/
build() {
if (!this.metadata.path || !this.metadata.method) {
throw new Error("Endpoint must have a path and method");
}
if (!this.handler) {
throw new Error("Endpoint must have a handler function");
}
const joiSchemas = {};
if (Object.keys(this.querySchema).length > 0) {
joiSchemas.query = this.createJoiSchema(this.querySchema);
}
if (Object.keys(this.bodySchema).length > 0) {
joiSchemas.body = this.createJoiSchema(this.bodySchema);
}
if (Object.keys(this.headerSchema).length > 0) {
joiSchemas.headers = this.createJoiSchema(this.headerSchema);
}
if (Object.keys(this.cookieSchema).length > 0) {
joiSchemas.cookies = this.createJoiSchema(this.cookieSchema);
}
if (Object.keys(this.paramsSchema).length > 0) {
joiSchemas.params = this.createJoiSchema(this.paramsSchema);
}
const middleware = [];
if (Object.keys(joiSchemas).length > 0) {
const { validateRequest } = require("../core/validation");
middleware.push(validateRequest(joiSchemas));
}
if (this.rateLimit) {
const { rateLimitMiddleware } = require("../utils/rate-limiter");
middleware.push(rateLimitMiddleware(this.rateLimit));
}
if (this.cacheOptions) {
const { cacheMiddleware } = require("../utils/cache");
middleware.push(cacheMiddleware(this.cacheOptions));
}
const { setupEndpointContext } = require("./EndpointHandler");
middleware.unshift(setupEndpointContext);
const handlerFn = async (req, res, next) => {
try {
const logger = (0, logger_1.getLogger)();
if (!global.CURRENT_REQUEST || !global.CURRENT_RESPONSE) {
logger.debug("Setting up endpoint context manually in handler");
global.CURRENT_REQUEST = req;
global.CURRENT_RESPONSE = res;
const originalEnd = res.end;
res.end = function (chunk, encoding, cb) {
delete global.CURRENT_REQUEST;
delete global.CURRENT_RESPONSE;
return originalEnd.call(this, chunk, encoding, cb);
};
}
res.locals.endpoint = {
name: this.metadata.name,
path: this.metadata.path,
method: this.metadata.method,
};
const context = { req, res, next };
await this.handler(req.headers, req.body || {}, req.query || {}, req.cookies || {}, req.params || {}, context);
}
catch (error) {
next(error);
}
};
const endpointMetadata = {
path: this.metadata.path,
method: this.metadata.method,
handler: handlerFn,
name: this.metadata.name,
description: this.metadata.description,
tags: this.metadata.tags,
middleware: [...(this.metadata.middleware || []), ...middleware],
responses: this.buildResponsesMetadata(),
parameters: this.buildParametersMetadata(),
requestBody: this.buildRequestBodyMetadata(),
};
return endpointMetadata;
}
createJoiSchema(schema) {
const joiSchema = {};
for (const [key, value] of Object.entries(schema)) {
if (typeof value === "string") {
joiSchema[key] = this.createJoiSchemaForType(value);
}
else {
let validator = this.createJoiSchemaForType(value.type);
if (value.required) {
validator = validator.required();
}
if (value.min !== undefined) {
if (value.type === "string") {
validator = validator.min(value.min);
}
else if (value.type === "number") {
validator = validator.min(value.min);
}
else if (value.type === "array") {
validator = validator.min(value.min);
}
else if (value.type === "date") {
validator = validator.min(value.min);
}
}
if (value.max !== undefined) {
if (value.type === "string" || value.type === "number" || value.type === "array" || value.type === "date") {
if (typeof validator.max === "function") {
validator = validator.max(value.max);
}
}
}
if (value.pattern) {
if (value.type === "string" && typeof validator.pattern === "function") {
validator = validator.pattern(value.pattern);
}
}
if (value.default !== undefined) {
validator = validator.default(value.default);
}
if (value.allow) {
validator = validator.allow(...value.allow);
}
joiSchema[key] = validator;
}
}
return joi_1.default.object(joiSchema);
}
createJoiSchemaForType(type) {
switch (type) {
case "string":
return joi_1.default.string();
case "number":
return joi_1.default.number();
case "boolean":
return joi_1.default.boolean();
case "date":
return joi_1.default.date();
case "decimal":
return joi_1.default.number().precision(2);
case "object":
return joi_1.default.object();
case "array":
return joi_1.default.array();
default:
return joi_1.default.any();
}
}
buildResponsesMetadata() {
const responses = {};
for (const [statusCode, response] of Object.entries(this.responses)) {
responses[statusCode] = {
description: typeof response === "string" ? response : `${statusCode} response`,
content: typeof response === "string"
? undefined
: {
"application/json": {
schema: {
type: "object",
example: response,
},
},
},
};
}
if (!responses["400"] &&
(Object.keys(this.querySchema).length > 0 ||
Object.keys(this.bodySchema).length > 0 ||
Object.keys(this.headerSchema).length > 0 ||
Object.keys(this.paramsSchema).length > 0)) {
responses["400"] = {
description: "Bad Request - Validation Error",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: { type: "string", example: "error" },
statusCode: { type: "number", example: 400 },
message: { type: "string", example: "Validation error" },
errorCode: { type: "string", example: "VALIDATION_ERROR" },
details: { type: "array", items: { type: "string" } },
timestamp: { type: "number" },
},
},
},
},
};
}
if (!responses["500"]) {
responses["500"] = {
description: "Internal Server Error",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: { type: "string", example: "error" },
statusCode: { type: "number", example: 500 },
message: { type: "string", example: "Internal server error" },
errorCode: { type: "string", example: "INTERNAL_ERROR" },
timestamp: { type: "number" },
},
},
},
},
};
}
return responses;
}
buildParametersMetadata() {
const parameters = [];
for (const [name, schema] of Object.entries(this.querySchema)) {
parameters.push({
name,
in: "query",
required: typeof schema === "object" ? schema.required : false,
schema: this.buildParameterSchema(schema),
});
}
for (const [name, schema] of Object.entries(this.paramsSchema)) {
parameters.push({
name,
in: "path",
required: true,
schema: this.buildParameterSchema(schema),
});
}
for (const [name, schema] of Object.entries(this.headerSchema)) {
parameters.push({
name,
in: "header",
required: typeof schema === "object" ? schema.required : false,
schema: this.buildParameterSchema(schema),
});
}
for (const [name, schema] of Object.entries(this.cookieSchema)) {
parameters.push({
name,
in: "cookie",
required: typeof schema === "object" ? schema.required : false,
schema: this.buildParameterSchema(schema),
});
}
return parameters;
}
buildRequestBodyMetadata() {
if (Object.keys(this.bodySchema).length === 0) {
return undefined;
}
const properties = {};
const required = [];
for (const [name, schema] of Object.entries(this.bodySchema)) {
properties[name] = this.buildParameterSchema(schema);
if (typeof schema === "object" && schema.required) {
required.push(name);
}
}
return {
required: true,
content: {
"application/json": {
schema: {
type: "object",
properties,
required: required.length > 0 ? required : undefined,
},
},
},
};
}
buildParameterSchema(schema) {
const type = typeof schema === "string" ? schema : schema.type;
switch (type) {
case "string":
return { type: "string" };
case "number":
return { type: "number" };
case "boolean":
return { type: "boolean" };
case "date":
return { type: "string", format: "date-time" };
case "decimal":
return { type: "number", format: "float" };
case "object":
return { type: "object" };
case "array":
return { type: "array", items: { type: "string" } };
default:
return { type: "string" };
}
}
}
exports.Endpoint = Endpoint;
/**
* Create a new endpoint with a fluent function
*
* @returns A new Endpoint instance
*/
function CreateEndpoint() {
return new Endpoint();
}
//# sourceMappingURL=Endpoint.js.map