UNPKG

@clickup/ent-framework

Version:

A PostgreSQL graph-database-alike library with microsharding and row-level security

102 lines 5.23 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Validation = void 0; const flatten_1 = __importDefault(require("lodash/flatten")); const pickBy_1 = __importDefault(require("lodash/pickBy")); const types_1 = require("../types"); const EntNotInsertableError_1 = require("./errors/EntNotInsertableError"); const EntNotReadableError_1 = require("./errors/EntNotReadableError"); const EntNotUpdatableError_1 = require("./errors/EntNotUpdatableError"); const EntValidationError_1 = require("./errors/EntValidationError"); const evaluate_1 = require("./rules/evaluate"); const Require_1 = require("./rules/Require"); const Triggers_1 = require("./Triggers"); class Validation { constructor(entName, rules) { this.entName = entName; this.tenantPrincipalField = rules.tenantPrincipalField; this.inferPrincipal = rules.inferPrincipal; this.load = rules.load; this.insert = rules.insert; this.update = rules.update || this.insert; this.delete = rules.delete || this.update; this.validate = (rules.validate || []).map((pred) => new Require_1.Require(pred)); } async validateLoad(vc, row) { await this.validatePrivacyImpl("load", this.load, vc, row, "sequential", EntNotReadableError_1.EntNotReadableError); } async validateInsert(vc, input) { await this.validateUserInputImpl(vc, input, input); await this.validatePrivacyImpl("insert", this.insert, vc, input, "parallel", EntNotInsertableError_1.EntNotInsertableError); } async validateUpdate(vc, old, input, privacyOnly = false) { // Simulate the update, as if it's applied to the ent. const newRow = (0, Triggers_1.buildUpdateNewRow)(old, input); if (!privacyOnly) { await this.validateUserInputImpl(vc, newRow, input); } await this.validatePrivacyImpl("update", this.update, vc, newRow, "parallel", EntNotUpdatableError_1.EntNotUpdatableError); } async validateDelete(vc, row) { await this.validatePrivacyImpl("delete", this.delete, vc, row, "parallel", EntNotUpdatableError_1.EntNotUpdatableError); } async validatePrivacyImpl(op, rules, vc, row, fashion, ExceptionClass) { this.validateTenantUserIDImpl(vc, row, ExceptionClass); const { allow, cause } = rules.length > 0 ? await (0, evaluate_1.evaluate)(vc, row, rules, fashion) : { allow: false, cause: `No "${op}" rules defined` }; if (allow) { return; } throw new ExceptionClass(this.entName, vc.toString(), { [types_1.ID]: "?", ...row }, cause); } validateTenantUserIDImpl(vc, row, ExceptionClass) { if (this.tenantPrincipalField === undefined) { return; } const rowTenantUserID = row[this.tenantPrincipalField]; if (rowTenantUserID === vc.principal) { return; } throw new ExceptionClass(this.entName, vc.toString(), { [types_1.ID]: "?", ...row }, `${this.tenantPrincipalField} is expected to be ` + JSON.stringify(vc.principal) + ", but got " + JSON.stringify(rowTenantUserID)); } async validateUserInputImpl(vc, newRow, input) { // Validation error details (like field name and message) are propagated // through results[].cause which is EntValidationError. const { allow, results } = await (0, evaluate_1.evaluate)(vc, newRow, this.validate, "parallel"); if (allow) { // Quick path (expected to fire most of the time). return; } // If some predicates failed, we ensure that they relate to the fields which // we actually touched. This makes sense for e.g. UPDATE: if we don't update // some field, it doesn't make sense to user-validate it. const touchedFields = Object.keys((0, pickBy_1.default)(input, (v) => v !== undefined)); const errors = (0, flatten_1.default)(results .filter(({ decision }) => decision === "DENY") .map(({ cause, rule }) => { // It's safe to cast to AbstractIs, because it's how we build // this.validate array in our constructor. const { name, field, message } = rule.predicate; return cause instanceof EntValidationError_1.EntValidationError ? cause.errors : cause === null ? // The Predicate just returned false. [{ field, message: message ?? `${name} returned false` }] : // Some other error; we must not expose error message details, // but can at least hint on the error class name. [{ field, message: cause.name }]; })).filter(({ field }) => field === null || touchedFields.includes(field)); if (errors.length > 0) { throw new EntValidationError_1.EntValidationError(this.entName, errors); } } } exports.Validation = Validation; //# sourceMappingURL=Validation.js.map