UNPKG

@avonjs/avonjs

Version:

A fluent Node.js API generator.

418 lines (417 loc) 13 kB
"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;