@avonjs/avonjs
Version:
A fluent Node.js API generator.
603 lines (602 loc) • 22.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const express_1 = __importDefault(require("express"));
const express_jwt_1 = require("express-jwt");
const joi_1 = __importDefault(require("joi"));
const jsonwebtoken_1 = require("jsonwebtoken");
const FieldCollection_1 = __importDefault(require("./Collections/FieldCollection"));
const ResourceCollection_1 = require("./Collections/ResourceCollection");
const Exceptions_1 = require("./Exceptions");
const ValidationException_1 = __importDefault(require("./Exceptions/ValidationException"));
const Fields_1 = require("./Fields");
const LoginRequest_1 = __importDefault(require("./Http/Requests/Auth/LoginRequest"));
const LoginResponse_1 = __importDefault(require("./Http/Responses/Auth/LoginResponse"));
const Models_1 = require("./Models");
const Resource_1 = __importDefault(require("./Resource"));
const RouteRegistrar_1 = __importDefault(require("./Route/RouteRegistrar"));
const helpers_1 = require("./helpers");
const debug_1 = __importDefault(require("./support/debug"));
// TODO: may i have to export new class instance instead of static method
// biome-ignore lint/complexity/noStaticOnlyClass:
class Avon {
/**
* Indicates application current version.
*/
static VERSION = '3.5.1';
/**
* Array of available resources.
*/
static resourceInstances = [];
/**
* Map of available resources.
*/
static resourceMap = {};
/**
* The error handler callback.
*/
static errorHandler = (error) => console.error(error);
/**
* The user resolver callback.
*/
static userResolver = () => null;
/**
* Extended swagger paths.
*/
static paths = {};
/**
* List of routes without authorization.
*/
static excepts = [
/.*\/schema/,
/.*\/login/,
];
/**
* The login attempt callback.
*/
static attemptCallback = async () => undefined;
/**
* Set application secret key.
*/
static appKey = 'Avon';
/**
* Indicates JWT params.
*/
static jwtSignOptions = {
algorithm: 'HS256',
};
/**
* Indicates JWT verify params.
*/
static jwtVerifyOptions;
/**
* The login attempt callback.
*/
static authFields = [
new Fields_1.Email().default(() => 'zarehesmaiel@gmail.com'),
new Fields_1.Text('password').default(() => 'zarehesmaiel@gmail.com'),
];
/**
* Extended swagger info.
*/
static info = {
version: Avon.version(),
title: 'My Application API',
description: 'Another Avonjs Application',
contact: {
name: 'Ismail Zare',
email: 'zarehesmaiel@gmail.com',
},
};
/**
* Get the Avon version.
*/
static version() {
return Avon.VERSION;
}
/**
* Register array of new resources.
*/
static resources(resources = []) {
Avon.resourceInstances = [...Avon.resourceInstances, ...resources];
debug_1.default.dump('Resources registered: %O', Avon.resourceCollection().keys().implode(', '));
return Avon;
}
/**
* Find resource for given uriKey.
*/
static resourceForKey(key) {
if (Avon.resourceMap[key] === undefined) {
Avon.resourceMap[key] = Avon.resourceCollection().first((resource) => resource.uriKey() === key);
}
return Avon.resourceMap[key];
}
/**
* Get collection of available resources.
*/
static resourceCollection() {
return new ResourceCollection_1.ResourceCollection(Avon.resourceInstances);
}
/**
* Get express instance.
*/
static routes(withAuthentication = false) {
return Avon.express(withAuthentication);
}
/**
* Get express instance.
*/
static express(withAuthentication = false, middlewares = []) {
const app = (0, express_1.default)();
app.set('query parser', 'extended');
// Apply provided middlewares
middlewares.forEach((middleware) => app.use(middleware));
// Define an auth-specific router
const authRouter = express_1.default.Router();
if (withAuthentication) {
// Login route should be available before authentication middleware
authRouter.post('/login', Avon.login);
// Apply authentication middleware only after login
app.use(Avon.expressjwt());
app.use(helpers_1.handleAuthenticationError);
}
// Register other application routes
new RouteRegistrar_1.default(authRouter).register();
// Register the authRouter under '/api'
app.use(authRouter);
return app;
}
/**
* Get JWT middleware.
*/
static expressjwt() {
const verifyOptions = Object.assign({ secret: Avon.appKey, algorithms: ['HS256'] }, Avon.jwtVerifyOptions);
return (0, express_jwt_1.expressjwt)(verifyOptions).unless({ path: Avon.excepts });
}
/**
* Handle the given error.
*/
static handleError(error) {
Avon.errorHandler(error);
return Avon;
}
/**
* Handle the given error.
*/
static handleErrorUsing(errorHandler) {
Avon.errorHandler = errorHandler;
return Avon;
}
/**
* Get the user id.
*/
static userId(request) {
return request.getRequest().auth?.id;
}
/**
* Resolve the user for incoming request to share in the app.
*/
static resolveUser(request) {
return Avon.userResolver(request);
}
/**
* Set the user resolver callback.
*/
static resolveUserUsing(userResolver) {
Avon.userResolver = userResolver;
return Avon;
}
/**
* Register resource from given path.
*/
static resourceIn(path) {
debug_1.default.dump(`Searching resources in directory: ${path}`);
const files = (0, node_fs_1.readdirSync)(path);
const resources = [];
// check paths
for (const file of files) {
const filePath = (0, node_path_1.join)(path, file);
const stat = (0, node_fs_1.statSync)(filePath);
// check sub directories
if (stat.isDirectory()) {
Avon.resourceIn(filePath);
continue;
}
// check file type
if (!['.js', '.ts'].includes((0, node_path_1.extname)(file))) {
continue;
}
const resourceClass = require(filePath).default || require(filePath);
// validate resource
if (resourceClass.prototype instanceof Resource_1.default) {
resources.push(new resourceClass());
}
}
// register resources
if (resources.length) {
debug_1.default.dump(`Registering resources from directory: ${path}`);
Avon.resources(resources);
}
else {
debug_1.default.dump(`No resources found in directory: ${path}`);
}
return Avon;
}
/**
* Set login fields.
*/
static credentials(authFields) {
Avon.authFields = authFields;
return Avon;
}
/**
* Get login fields.
*/
static fieldsForLogin() {
return new FieldCollection_1.default(Avon.authFields);
}
/**
* Set JWT secret.
*/
static key(appKey) {
Avon.appKey = appKey;
return Avon;
}
/**
* Extend swagger paths.
*/
static extend(paths) {
Avon.paths = paths;
return Avon;
}
/**
* Extend swagger paths.
*/
static describe(info) {
Avon.info = { ...Avon.info, ...info };
return Avon;
}
/**
* Set the JWT sign options.
*/
static signOptions(signOptions) {
Avon.jwtSignOptions = { ...Avon.jwtSignOptions, ...signOptions };
return Avon;
}
/**
* Set the JWT verify options.
*/
static verifyOptions(verifyOptions) {
Avon.jwtVerifyOptions = { ...Avon.jwtVerifyOptions, ...verifyOptions };
return Avon;
}
/**
* Set the JWT options.
*/
static except(path) {
Avon.excepts.push(path);
return Avon;
}
/**
* Set attempt callback.
*/
static attemptUsing(attemptCallback) {
Avon.attemptCallback = attemptCallback;
return Avon;
}
/**
* Handle login request.
*/
static async login(req, res) {
const request = new LoginRequest_1.default(req);
const payload = new Models_1.Fluent();
// validate credentials
await Avon.performValidation(request)
.then(() => {
// resolve credentials
Avon.fieldsForLogin().each((field) => field.fill(request, payload));
// attempt login
Avon.attempt(payload.getAttributes())
.then((response) => (0, helpers_1.send)(res, response))
.catch((error) => {
if (error instanceof Exceptions_1.ResponsableException) {
(0, helpers_1.send)(res, error.toResponse());
}
else {
Avon.handleError(error);
res
.status(500)
.send({ message: error.message, name: 'InternalServerError' });
}
});
})
.catch((error) => {
(0, helpers_1.send)(res, new ValidationException_1.default(error).toResponse());
});
}
/**
* Perform login request validation.
*/
static async performValidation(request) {
await joi_1.default.object(Avon.fieldsForLogin()
.map((field) => field.getRules(request))
.flatMap((rules) => Object.keys(rules).map((key) => [key, rules[key]]))
.mapWithKeys((rules) => rules)
.all()).validateAsync(request.all(), { abortEarly: false });
}
/**
* Set attempt callback.
*/
static async attempt(payload) {
try {
const user = await Avon.attemptCallback(payload);
Exceptions_1.NotFoundException.unless(user);
return new LoginResponse_1.default({ token: Avon.sign(user) });
}
catch (err) {
debug_1.default.error(err);
throw new Exceptions_1.AuthenticationException();
}
}
/**
* Make JWT token.
*/
static sign(payload) {
return (0, jsonwebtoken_1.sign)(payload, Avon.appKey, Avon.jwtSignOptions);
}
/**
* Get the schema for open API.
*/
static schema(request) {
return {
openapi: '3.0.0',
security: [{ BearerAuth: [] }],
paths: Avon.resourceCollection().reduce((paths, resource) => Object.assign({}, paths, resource.schema(request)), { ...Avon.paths, ...Avon.loginSchema(request) }),
info: Avon.info,
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
responses: {
Forbidden: {
description: 'This action is unauthorized.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: { type: 'number', default: 403 },
message: {
type: 'string',
default: 'This action is unauthorized.',
},
name: { type: 'string', default: 'Forbidden' },
meta: {
type: 'object',
properties: {
stack: { type: 'object' },
},
},
},
},
},
},
},
Unauthenticated: {
description: 'The user is unauthenticated.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: { type: 'number', default: 401 },
message: {
type: 'string',
default: 'The user is unauthenticated.',
},
name: { type: 'string', default: 'Unauthenticated' },
meta: {
type: 'object',
properties: {
stack: { type: 'object' },
},
},
},
},
},
},
},
NotFound: {
description: 'Requested resource not found.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: { type: 'number', default: 404 },
message: {
type: 'string',
default: 'Requested resource not found.',
},
name: { type: 'string', default: 'NotFound' },
meta: {
type: 'object',
properties: {
stack: { type: 'object' },
},
},
},
},
},
},
},
InternalServerError: {
description: 'Internal server error.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: { type: 'number', default: 500 },
message: {
type: 'string',
default: 'Something went wrong.',
},
name: { type: 'string', default: 'InternalServerError' },
meta: {
type: 'object',
properties: {
stack: { type: 'object' },
},
},
},
},
},
},
},
UnprocessableContent: {
description: 'Validation failed.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: { type: 'number', default: 422 },
message: {
type: 'string',
default: 'The given data was invalid.',
},
name: { type: 'string', default: 'UnprocessableContent' },
meta: {
type: 'object',
properties: {
errors: { type: 'object' },
},
},
},
},
},
},
},
EmptyResponse: {
description: 'Nothing to show',
content: {
'application/json': {},
},
},
BadRequest: {
description: 'Bad request error.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: { type: 'number', default: 400 },
message: {
type: 'string',
default: 'Request payload are invalid.',
},
name: { type: 'string', default: 'BadRequest' },
meta: {
type: 'object',
properties: {
stack: { type: 'object' },
},
},
},
},
},
},
},
MethodNotAllowed: {
description: 'Method not allowed error.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: { type: 'number', default: 405 },
message: {
type: 'string',
default: 'The requested method is not supported for this endpoint.',
},
name: { type: 'string', default: 'MethodNotAllowed' },
meta: {
type: 'object',
properties: {
stack: { type: 'object' },
},
},
},
},
},
},
},
},
schemas: {
PrimaryKey: {
anyOf: [{ type: 'number' }, { type: 'string' }],
},
},
},
};
}
/**
* Get the login swagger schema.
*/
static loginSchema(request) {
const fields = Avon.fieldsForLogin();
return {
[`${request.getRequest().baseUrl}/login`]: {
post: {
tags: ['auth'],
description: 'Login to get JWT token',
operationId: 'attempt',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
required: fields.map((field) => field.attribute).all(),
properties: fields.payloadSchemas(request),
},
},
},
},
responses: {
...(0, helpers_1.errorsResponses)(),
...(0, helpers_1.validationResponses)(),
200: {
description: 'Get JWT token',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
token: { type: 'string' },
meta: { type: 'object' },
},
},
},
},
},
},
},
},
},
},
};
}
}
exports.default = Avon;