@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
102 lines • 5.23 kB
JavaScript
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
;