zenstack
Version:
FullStack enhancement for Prisma ORM: seamless integration from database to UI
229 lines • 11.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("@zenstackhq/language/ast");
const sdk_1 = require("@zenstackhq/sdk");
const langium_1 = require("langium");
const ast_utils_1 = require("../../utils/ast-utils");
const utils_1 = require("./utils");
/**
* Validates expressions.
*/
class ExpressionValidator {
validate(expr, accept) {
// deal with a few cases where reference resolution fail silently
if (!expr.$resolvedType) {
if ((0, sdk_1.isAuthInvocation)(expr)) {
// check was done at link time
accept('error', 'auth() cannot be resolved because no model marked with "@@auth()" or named "User" is found', { node: expr });
}
else {
const hasReferenceResolutionError = (0, langium_1.streamAst)(expr).some((node) => {
if ((0, ast_1.isMemberAccessExpr)(node)) {
return !!node.member.error;
}
if ((0, ast_1.isReferenceExpr)(node)) {
return !!node.target.error;
}
return false;
});
if (!hasReferenceResolutionError) {
// report silent errors not involving linker errors
accept('error', 'Expression cannot be resolved', {
node: expr,
});
}
}
}
// extra validations by expression type
switch (expr.$type) {
case 'BinaryExpr':
this.validateBinaryExpr(expr, accept);
break;
}
}
validateBinaryExpr(expr, accept) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
switch (expr.operator) {
case 'in': {
if (typeof ((_a = expr.left.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl) !== 'string' && !(0, ast_1.isEnum)((_b = expr.left.$resolvedType) === null || _b === void 0 ? void 0 : _b.decl)) {
accept('error', 'left operand of "in" must be of scalar type', { node: expr.left });
}
if (!((_c = expr.right.$resolvedType) === null || _c === void 0 ? void 0 : _c.array)) {
accept('error', 'right operand of "in" must be an array', {
node: expr.right,
});
}
this.validateCrossModelFieldComparison(expr, accept);
break;
}
case '>':
case '>=':
case '<':
case '<=':
case '&&':
case '||': {
if ((_d = expr.left.$resolvedType) === null || _d === void 0 ? void 0 : _d.array) {
accept('error', 'operand cannot be an array', { node: expr.left });
break;
}
if ((_e = expr.right.$resolvedType) === null || _e === void 0 ? void 0 : _e.array) {
accept('error', 'operand cannot be an array', { node: expr.right });
break;
}
let supportedShapes;
if (['>', '>=', '<', '<='].includes(expr.operator)) {
supportedShapes = ['Int', 'Float', 'DateTime', 'Any'];
}
else {
supportedShapes = ['Boolean', 'Any'];
}
if (typeof ((_f = expr.left.$resolvedType) === null || _f === void 0 ? void 0 : _f.decl) !== 'string' ||
!supportedShapes.includes(expr.left.$resolvedType.decl)) {
accept('error', `invalid operand type for "${expr.operator}" operator`, {
node: expr.left,
});
return;
}
if (typeof ((_g = expr.right.$resolvedType) === null || _g === void 0 ? void 0 : _g.decl) !== 'string' ||
!supportedShapes.includes(expr.right.$resolvedType.decl)) {
accept('error', `invalid operand type for "${expr.operator}" operator`, {
node: expr.right,
});
return;
}
// DateTime comparison is only allowed between two DateTime values
if (expr.left.$resolvedType.decl === 'DateTime' && expr.right.$resolvedType.decl !== 'DateTime') {
accept('error', 'incompatible operand types', { node: expr });
}
else if (expr.right.$resolvedType.decl === 'DateTime' &&
expr.left.$resolvedType.decl !== 'DateTime') {
accept('error', 'incompatible operand types', { node: expr });
}
if (expr.operator !== '&&' && expr.operator !== '||') {
this.validateCrossModelFieldComparison(expr, accept);
}
break;
}
case '==':
case '!=': {
if (this.isInValidationContext(expr)) {
// in validation context, all fields are optional, so we should allow
// comparing any field against null
if (((0, sdk_1.isDataModelFieldReference)(expr.left) && (0, ast_1.isNullExpr)(expr.right)) ||
((0, sdk_1.isDataModelFieldReference)(expr.right) && (0, ast_1.isNullExpr)(expr.left))) {
return;
}
}
if (!!((_h = expr.left.$resolvedType) === null || _h === void 0 ? void 0 : _h.array) !== !!((_j = expr.right.$resolvedType) === null || _j === void 0 ? void 0 : _j.array)) {
accept('error', 'incompatible operand types', { node: expr });
break;
}
if (!this.validateCrossModelFieldComparison(expr, accept)) {
break;
}
if ((((_k = expr.left.$resolvedType) === null || _k === void 0 ? void 0 : _k.nullable) && (0, ast_1.isNullExpr)(expr.right)) ||
(((_l = expr.right.$resolvedType) === null || _l === void 0 ? void 0 : _l.nullable) && (0, ast_1.isNullExpr)(expr.left))) {
// comparing nullable field with null
return;
}
if (typeof ((_m = expr.left.$resolvedType) === null || _m === void 0 ? void 0 : _m.decl) === 'string' &&
typeof ((_o = expr.right.$resolvedType) === null || _o === void 0 ? void 0 : _o.decl) === 'string') {
// scalar types assignability
if (!(0, utils_1.typeAssignable)(expr.left.$resolvedType.decl, expr.right.$resolvedType.decl) &&
!(0, utils_1.typeAssignable)(expr.right.$resolvedType.decl, expr.left.$resolvedType.decl)) {
accept('error', 'incompatible operand types', { node: expr });
}
return;
}
// disallow comparing model type with scalar type or comparison between
// incompatible model types
const leftType = (_p = expr.left.$resolvedType) === null || _p === void 0 ? void 0 : _p.decl;
const rightType = (_q = expr.right.$resolvedType) === null || _q === void 0 ? void 0 : _q.decl;
if ((0, ast_1.isDataModel)(leftType) && (0, ast_1.isDataModel)(rightType)) {
if (leftType != rightType) {
// incompatible model types
// TODO: inheritance case?
accept('error', 'incompatible operand types', { node: expr });
}
// not supported:
// - foo == bar
// - foo == this
if ((0, sdk_1.isDataModelFieldReference)(expr.left) &&
((0, ast_1.isThisExpr)(expr.right) || (0, sdk_1.isDataModelFieldReference)(expr.right))) {
accept('error', 'comparison between model-typed fields are not supported', { node: expr });
}
else if ((0, sdk_1.isDataModelFieldReference)(expr.right) &&
((0, ast_1.isThisExpr)(expr.left) || (0, sdk_1.isDataModelFieldReference)(expr.left))) {
accept('error', 'comparison between model-typed fields are not supported', { node: expr });
}
}
else if (((0, ast_1.isDataModel)(leftType) && !(0, ast_1.isNullExpr)(expr.right)) ||
((0, ast_1.isDataModel)(rightType) && !(0, ast_1.isNullExpr)(expr.left))) {
// comparing model against scalar (except null)
accept('error', 'incompatible operand types', { node: expr });
}
break;
}
case '?':
case '!':
case '^':
this.validateCollectionPredicate(expr, accept);
break;
}
}
validateCrossModelFieldComparison(expr, accept) {
// not supported in "read" rules:
// - foo.a == bar
// - foo.user.id == userId
// except:
// - future().userId == userId
if (((0, ast_1.isMemberAccessExpr)(expr.left) &&
(0, ast_1.isDataModelField)(expr.left.member.ref) &&
expr.left.member.ref.$container != (0, ast_utils_1.getContainingDataModel)(expr)) ||
((0, ast_1.isMemberAccessExpr)(expr.right) &&
(0, ast_1.isDataModelField)(expr.right.member.ref) &&
expr.right.member.ref.$container != (0, ast_utils_1.getContainingDataModel)(expr))) {
// foo.user.id == auth().id
// foo.user.id == "123"
// foo.user.id == null
// foo.user.id == EnumValue
if (!(this.isNotModelFieldExpr(expr.left) || this.isNotModelFieldExpr(expr.right))) {
const containingPolicyAttr = (0, ast_utils_1.findUpAst)(expr, (node) => (0, ast_1.isDataModelAttribute)(node) && ['@@allow', '@@deny'].includes(node.decl.$refText));
if (containingPolicyAttr) {
const operation = (0, sdk_1.getAttributeArgLiteral)(containingPolicyAttr, 'operation');
if ((operation === null || operation === void 0 ? void 0 : operation.split(',').includes('all')) || (operation === null || operation === void 0 ? void 0 : operation.split(',').includes('read'))) {
accept('error', 'comparison between fields of different models is not supported in model-level "read" rules', {
node: expr,
});
return false;
}
}
}
}
return true;
}
validateCollectionPredicate(expr, accept) {
if (!expr.$resolvedType) {
accept('error', 'collection predicate can only be used on an array of model type', { node: expr });
return;
}
}
isInValidationContext(node) {
return (0, ast_utils_1.findUpAst)(node, (n) => (0, ast_1.isDataModelAttribute)(n) && n.decl.$refText === '@@validate');
}
isNotModelFieldExpr(expr) {
return (
// literal
(0, ast_1.isLiteralExpr)(expr) ||
// enum field
(0, sdk_1.isEnumFieldReference)(expr) ||
// null
(0, ast_1.isNullExpr)(expr) ||
// `auth()` access
(0, utils_1.isAuthOrAuthMemberAccess)(expr) ||
// array
((0, ast_1.isArrayExpr)(expr) && expr.items.every((item) => this.isNotModelFieldExpr(item))));
}
}
exports.default = ExpressionValidator;
//# sourceMappingURL=expression-validator.js.map