@avonjs/avonjs
Version:
A fluent Node.js API generator.
418 lines (417 loc) • 13 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const collect_js_1 = __importDefault(require("collect.js"));
const joi_1 = __importStar(require("joi"));
const Avon_1 = __importDefault(require("../Avon"));
const FieldCollection_1 = __importDefault(require("../Collections/FieldCollection"));
const Exceptions_1 = require("../Exceptions");
const ValidationException_1 = __importDefault(require("../Exceptions/ValidationException"));
const Responses_1 = require("../Http/Responses");
const ActionResponse_1 = __importDefault(require("../Http/Responses/ActionResponse"));
const AuthorizedToSee_1 = __importDefault(require("../Mixins/AuthorizedToSee"));
const Models_1 = require("../Models");
class Action extends (0, AuthorizedToSee_1.default)(class {
}) {
/**
* The callback used to authorize running the action.
*/
runCallback;
/**
* Indicates if the action can be run without any models.
*/
standaloneAction = false;
/**
* Indicates if the action can be run on a single model.
*/
inlineAction = false;
/**
* Indicates if the action destroy some resources.
*/
destructiveAction = false;
/**
* Indicates the response status code.
*/
responseCode = 200;
/**
* Indicates the response content type.
*/
responseType = 'application/json';
/**
* Execute the action for the given request.
*/
async handleRequest(request) {
const models = await this.resolveModels(request);
// Authorize action
await this.authorizeAction(request, models);
// prepare changes for log
const changes = this.getChanges(request, models);
// handle action
const results = await this.handle(this.resolveFields(request), changes.map(({ resource }) => resource));
// record action changes on the resource
await this.recordChanges(request, changes);
// finish!
return results instanceof Responses_1.AvonResponse ? results : this.respondSuccess();
}
/**
* Get models for incoming action.
*/
async resolveModels(request) {
return this.isStandalone() ? [] : request.models();
}
/**
* Authorize models before running action.
*/
async authorizeAction(request, models) {
try {
if (this.isStandalone()) {
await this.authorizeStandaloneAction(request);
}
else if (this.isInline()) {
await this.authorizeInlineAction(request, models[0]);
}
else {
await this.authorizeBulkAction(request, models);
}
}
catch (error) {
throw this.handleAuthorizationError(error);
}
}
/**
* Authorize a standalone action.
*/
async authorizeStandaloneAction(request) {
const isAuthorized = await this.authorizedToRun(request, null);
Exceptions_1.ForbiddenException.unless(isAuthorized, this.unauthorizedMessage());
}
/**
* Authorize an inline action.
*/
async authorizeInlineAction(request, model) {
const isAuthorized = await this.authorizedToRun(request, model);
Exceptions_1.ForbiddenException.unless(isAuthorized, this.unauthorizedMessage());
}
/**
* Authorize a bulk action.
*/
async authorizeBulkAction(request, models) {
await this.authorizationValidator(request).validateAsync(models, {
abortEarly: false,
allowUnknown: true,
});
}
/**
* Handle authorization errors.
*/
handleAuthorizationError(error) {
if (error instanceof joi_1.ValidationError) {
return new ValidationException_1.default(error);
}
return error;
}
/**
* Make Joi validator to authorize the models.
*/
authorizationValidator(request) {
return joi_1.default.array().items(joi_1.default.any().external(async (model, helpers) => {
// Authorization check logic (async)
const isAuthorized = await this.authorizedToRun(request, model);
if (!isAuthorized) {
return helpers.error('any.custom', {
error: new Error(this.unauthorizedMessage(model)),
});
}
}, 'Authorization check'));
}
/**
* Get unauthorized to run message.
*/
unauthorizedMessage(model = null) {
return model
? `unauthorized to run action on resource with ID:'${model.getKey()}'`
: 'Unauthorized to run action';
}
/**
* Prepare change log for incoming action.
*/
getChanges(request, resources) {
return resources.map((resource) => {
return {
resource,
previous: request.newModel({ ...resource.getAttributes() }),
};
});
}
/**
* Resolve the creation fields.
*/
resolveFields(request) {
const model = new Models_1.Fluent();
this.availableFields(request)
.authorized(request)
.each((field) => field.fillForAction(request, model));
return model;
}
/**
* Store changes for incoming action.
*/
async recordChanges(request, changes = []) {
if (this.isStandalone()) {
await request
.resource()
.recordStandaloneActionEvent(this, request.all(), Avon_1.default.userId(request));
}
else {
await request
.resource()
.recordBulkActionEvent(this, changes, request.all(), Avon_1.default.userId(request));
}
}
/**
* Determine if the action is executable for the given request.
*/
async authorizedToRun(request, model = null) {
return this.runCallback != null
? this.runCallback.apply(this, [request, model])
: true;
}
/**
* Validate an action for incoming request.
*
* @throws {ValidationException}
*/
async validate(request) {
try {
const value = await this.validator(request).validateAsync(this.dataForValidation(request), { abortEarly: false, allowUnknown: true });
this.afterValidation(request, value);
}
catch (error) {
throw new ValidationException_1.default(error);
}
}
/**
* Create a validator instance for a resource creation request.
*/
validator(request) {
return joi_1.default.object(this.rules(request));
}
/**
* Get the validation rules for a resource creation request.
*/
rules(request) {
return this.formatRules(request, this.prepareRulesForValidator(this.availableFields(request)
.flatMap((field) => field.getCreationRules(request))
.all()));
}
/**
* Prepare given rules for validator.
*/
prepareRulesForValidator(rules) {
return (0, collect_js_1.default)(rules)
.flatMap((rules) => Object.keys(rules).map((key) => [key, rules[key]]))
.mapWithKeys((rules) => rules)
.all();
}
/**
* Perform any final formatting of the given validation rules.
*/
formatRules(request, rules) {
return rules;
}
/**
* Prepare given rules for validator.
*/
dataForValidation(request) {
return request.all();
}
/**
* Handle any post-validation processing.
*/
afterValidation(request, validator) {
//
}
/**
* Get the fields that are available for the given request.
*/
availableFields(request) {
return new FieldCollection_1.default(this.fields(request));
}
/**
* Get the fields available on the action.
*/
fields(request) {
return [];
}
/**
* Set the callback to be run to authorize running the action.
*/
canRun(callback) {
this.runCallback = callback;
return this;
}
/**
* Set the callback to be run to authorize viewing the filter or action.
*/
canSee(callback) {
return super.canSee(callback);
}
/**
* Get the displayable name of the action.
*/
name() {
return this.constructor.name;
}
/**
* Get the URI key for the action.
*/
uriKey() {
return this.name().replace(/[A-Z]/g, (matched, offset) => (offset > 0 ? '-' : '') + matched.toLowerCase());
}
/**
* Mark the action as a standalone action.
*
* @return this
*/
standalone() {
this.standaloneAction = true;
return this;
}
/**
* Determine if the action is a "standalone" action.
*
* @return bool
*/
isStandalone() {
return this.standaloneAction;
}
/**
* Mark the action as a "inline" action.
*
* @return this
*/
inline() {
this.inlineAction = true;
return this;
}
/**
* Determine if the action is a "inline" action.
*
* @return bool
*/
isInline() {
return this.inlineAction;
}
/**
* Mark the action as a "destructive" action.
*
* @return this
*/
destructive() {
this.destructiveAction = true;
return this;
}
/**
* Determine if the action is a "destructive" action.
*
* @return bool
*/
isDestructive() {
return this.destructiveAction;
}
/**
* Prepare the action for JSON serialization.
*/
serializeForIndex(request) {
return {
uriKey: this.uriKey(),
isStandalone: this.isStandalone(),
isInline: this.isInline(),
fields: this.availableFields(request)
.mapWithKeys((field) => [field.attribute, field])
.all(),
};
}
/**
* Get successful response.
*/
respondSuccess(data) {
return new ActionResponse_1.default(data ?? { message: 'Your action successfully ran.' }, { message: 'Your action successfully ran.' });
}
/**
* Get the swagger-ui response schema.
*/
responseSchema(request) {
return {
[this.responseCode]: {
description: `Action ${this.name()} ran successfully`,
content: {
[this.responseType]: {
schema: {
type: 'object',
properties: {
code: { type: 'number', default: this.responseCode },
data: this.schema(request),
meta: {
type: 'object',
properties: {
message: {
type: 'string',
default: 'Your action ran successfully.',
},
},
},
},
},
},
},
},
};
}
/**
* Get the swagger-ui schema.
*/
schema(request) {
return {
type: 'object',
properties: {
message: {
type: 'string',
default: 'Your action ran successfully.',
},
},
};
}
/**
* Get the swagger-ui possible request body contents.
*/
accepts() {
return ['application/json', 'multipart/form-data'];
}
}
exports.default = Action;