zenstack
Version:
FullStack enhancement for Prisma ORM: seamless integration from database to UI
316 lines • 13.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConstraintTransformer = void 0;
const sdk_1 = require("@zenstackhq/sdk");
const ast_1 = require("@zenstackhq/sdk/ast");
const ts_pattern_1 = require("ts-pattern");
const __1 = require("..");
const ast_utils_1 = require("../../../utils/ast-utils");
/**
* Transform a set of allow and deny rules into a single constraint expression.
*/
class ConstraintTransformer {
constructor(options) {
this.options = options;
// a counter for generating unique variable names
this.varCounter = 0;
}
/**
* Transforms a set of allow and deny rules into a single constraint expression.
*/
transformRules(allows, denies) {
// reset state
this.varCounter = 0;
if (allows.length === 0) {
// unconditionally deny
return this.value('false', 'boolean');
}
let result;
// transform allow rules
const allowConstraints = allows.map((allow) => this.transformExpression(allow));
if (allowConstraints.length > 1) {
result = this.or(...allowConstraints);
}
else {
result = allowConstraints[0];
}
// transform deny rules and compose
if (denies.length > 0) {
const denyConstraints = denies.map((deny) => this.transformExpression(deny));
result = this.and(result, ...denyConstraints.map((c) => this.not(c)));
}
// DEBUG:
// console.log(`Constraint transformation result:\n${JSON.stringify(result, null, 2)}`);
return result;
}
and(...constraints) {
if (constraints.length === 0) {
throw new Error('No expressions to combine');
}
return constraints.length === 1 ? constraints[0] : `{ kind: 'and', children: [ ${constraints.join(', ')} ] }`;
}
or(...constraints) {
if (constraints.length === 0) {
throw new Error('No expressions to combine');
}
return constraints.length === 1 ? constraints[0] : `{ kind: 'or', children: [ ${constraints.join(', ')} ] }`;
}
not(constraint) {
return `{ kind: 'not', children: [${constraint}] }`;
}
transformExpression(expression) {
return ((0, ts_pattern_1.match)(expression)
.when(ast_1.isBinaryExpr, (expr) => this.transformBinary(expr))
.when(ast_1.isUnaryExpr, (expr) => this.transformUnary(expr))
// top-level boolean literal
.when(ast_1.isLiteralExpr, (expr) => this.transformLiteral(expr))
// top-level boolean reference expr
.when(ast_1.isReferenceExpr, (expr) => this.transformReference(expr))
// top-level boolean member access expr
.when(ast_1.isMemberAccessExpr, (expr) => this.transformMemberAccess(expr))
// `check()` invocation on a relation field
.when(ast_utils_1.isCheckInvocation, (expr) => this.transformCheckInvocation(expr))
.otherwise(() => this.nextVar()));
}
transformLiteral(expr) {
return (0, ts_pattern_1.match)(expr.$type)
.with(ast_1.NumberLiteral, () => {
const parsed = parseFloat(expr.value);
if (isNaN(parsed) || parsed < 0 || !Number.isInteger(parsed)) {
// only non-negative integers are supported, for other cases,
// transform into a free variable
return this.nextVar('number');
}
return this.value(expr.value.toString(), 'number');
})
.with(ast_1.StringLiteral, () => this.value(`'${expr.value}'`, 'string'))
.with(ast_1.BooleanLiteral, () => this.value(expr.value.toString(), 'boolean'))
.exhaustive();
}
transformReference(expr) {
// top-level reference is transformed into a named variable
return this.variable(expr.target.$refText, 'boolean');
}
transformMemberAccess(expr) {
// "this.x" is transformed into a named variable
if ((0, ast_1.isThisExpr)(expr.operand)) {
return this.variable(expr.member.$refText, 'boolean');
}
// top-level auth access
const authAccess = this.getAuthAccess(expr);
if (authAccess) {
return this.value(`${authAccess} ?? false`, 'boolean');
}
// other top-level member access expressions are not supported
// and thus transformed into a free variable
return this.nextVar();
}
transformBinary(expr) {
return ((0, ts_pattern_1.match)(expr.operator)
.with('&&', () => this.and(this.transformExpression(expr.left), this.transformExpression(expr.right)))
.with('||', () => this.or(this.transformExpression(expr.left), this.transformExpression(expr.right)))
.with(ts_pattern_1.P.union('==', '!=', '<', '<=', '>', '>='), () => this.transformComparison(expr))
// unsupported operators (e.g., collection predicate) are transformed into a free variable
.otherwise(() => this.nextVar()));
}
transformUnary(expr) {
return (0, ts_pattern_1.match)(expr.operator)
.with('!', () => this.not(this.transformExpression(expr.operand)))
.exhaustive();
}
transformComparison(expr) {
if ((0, sdk_1.isAuthInvocation)(expr.left) || (0, sdk_1.isAuthInvocation)(expr.right)) {
// handle the case if any operand is `auth()` invocation
const authComparison = this.transformAuthComparison(expr);
return authComparison !== null && authComparison !== void 0 ? authComparison : this.nextVar();
}
const leftOperand = this.getComparisonOperand(expr.left);
const rightOperand = this.getComparisonOperand(expr.right);
const op = this.mapOperatorToConstraintKind(expr.operator);
const result = `{ kind: '${op}', left: ${leftOperand}, right: ${rightOperand} }`;
// `auth()` member access can be undefined, when that happens, we assume a false condition
// for the comparison
const leftAuthAccess = this.getAuthAccess(expr.left);
const rightAuthAccess = this.getAuthAccess(expr.right);
if (leftAuthAccess && rightOperand) {
// `auth().f op x` => `auth().f !== undefined && auth().f op x`
return this.and(this.value(`${this.normalizeToNull(leftAuthAccess)} !== null`, 'boolean'), result);
}
else if (rightAuthAccess && leftOperand) {
// `x op auth().f` => `auth().f !== undefined && x op auth().f`
return this.and(this.value(`${this.normalizeToNull(rightAuthAccess)} !== null`, 'boolean'), result);
}
if (leftOperand === undefined || rightOperand === undefined) {
// if either operand is not supported, transform into a free variable
return this.nextVar();
}
return result;
}
transformAuthComparison(expr) {
if (this.isAuthEqualNull(expr)) {
// `auth() == null` => `user === null`
return this.value(`${this.options.authAccessor} === null`, 'boolean');
}
if (this.isAuthNotEqualNull(expr)) {
// `auth() != null` => `user !== null`
return this.value(`${this.options.authAccessor} !== null`, 'boolean');
}
// auth() equality check against a relation, translate to id-fk comparison
const operand = (0, sdk_1.isAuthInvocation)(expr.left) ? expr.right : expr.left;
if (!(0, sdk_1.isDataModelFieldReference)(operand)) {
return undefined;
}
// get id-fk field pairs from the relation field
const relationField = operand.target.ref;
const idFkPairs = (0, sdk_1.getRelationKeyPairs)(relationField);
// build id-fk field comparison constraints
const fieldConstraints = [];
idFkPairs.forEach(({ id, foreignKey }) => {
const idFieldType = this.mapType(id.type.type);
if (!idFieldType) {
return;
}
const fkFieldType = this.mapType(foreignKey.type.type);
if (!fkFieldType) {
return;
}
const op = this.mapOperatorToConstraintKind(expr.operator);
const authIdAccess = `${this.options.authAccessor}?.${id.name}`;
fieldConstraints.push(this.and(
// `auth()?.id != null` guard
this.value(`${this.normalizeToNull(authIdAccess)} !== null`, 'boolean'),
// `auth()?.id [op] fkField`
`{ kind: '${op}', left: ${this.value(authIdAccess, idFieldType)}, right: ${this.variable(foreignKey.name, fkFieldType)} }`));
});
// combine field constraints
if (fieldConstraints.length > 0) {
return this.and(...fieldConstraints);
}
return undefined;
}
transformCheckInvocation(expr) {
// transform `check()` invocation to a special "delegate" constraint kind
// to be evaluated at runtime
var _a;
const field = expr.args[0].value;
if (!field) {
throw new sdk_1.PluginError(__1.name, 'Invalid check invocation');
}
const fieldType = (_a = field.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl;
let operation = undefined;
if (expr.args[1]) {
operation = (0, sdk_1.getLiteral)(expr.args[1].value);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = { kind: 'delegate', model: fieldType.name, relation: field.target.$refText };
if (operation) {
// operation can be explicitly specified or inferred from the context
result.operation = operation;
}
return JSON.stringify(result);
}
// normalize `auth()` access undefined value to null
normalizeToNull(expr) {
return `(${expr} ?? null)`;
}
isAuthEqualNull(expr) {
return (expr.operator === '==' &&
(((0, sdk_1.isAuthInvocation)(expr.left) && (0, ast_1.isNullExpr)(expr.right)) ||
((0, sdk_1.isAuthInvocation)(expr.right) && (0, ast_1.isNullExpr)(expr.left))));
}
isAuthNotEqualNull(expr) {
return (expr.operator === '!=' &&
(((0, sdk_1.isAuthInvocation)(expr.left) && (0, ast_1.isNullExpr)(expr.right)) ||
((0, sdk_1.isAuthInvocation)(expr.right) && (0, ast_1.isNullExpr)(expr.left))));
}
getComparisonOperand(expr) {
if ((0, ast_1.isLiteralExpr)(expr)) {
return this.transformLiteral(expr);
}
if ((0, sdk_1.isEnumFieldReference)(expr)) {
return this.value(`'${expr.target.$refText}'`, 'string');
}
const fieldAccess = this.getFieldAccess(expr);
if (fieldAccess) {
// model field access is transformed into a named variable
const mappedType = this.mapExpressionType(expr);
if (mappedType) {
return this.variable(fieldAccess.name, mappedType);
}
else {
return undefined;
}
}
const authAccess = this.getAuthAccess(expr);
if (authAccess) {
const mappedType = this.mapExpressionType(expr);
if (mappedType) {
return `${this.value(authAccess, mappedType)}`;
}
else {
return undefined;
}
}
return undefined;
}
mapExpressionType(expression) {
var _a, _b;
if ((0, ast_1.isEnum)((_a = expression.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl)) {
return 'string';
}
else {
return this.mapType((_b = expression.$resolvedType) === null || _b === void 0 ? void 0 : _b.decl);
}
}
mapType(type) {
return (0, ts_pattern_1.match)(type)
.with('Boolean', () => 'boolean')
.with('Int', () => 'number')
.with('String', () => 'string')
.otherwise(() => undefined);
}
mapOperatorToConstraintKind(operator) {
return (0, ts_pattern_1.match)(operator)
.with('==', () => 'eq')
.with('!=', () => 'ne')
.with('<', () => 'lt')
.with('<=', () => 'lte')
.with('>', () => 'gt')
.with('>=', () => 'gte')
.otherwise(() => {
throw new Error(`Unsupported operator: ${operator}`);
});
}
getFieldAccess(expr) {
if ((0, ast_1.isReferenceExpr)(expr)) {
return (0, ast_1.isDataModelField)(expr.target.ref) ? { name: expr.target.$refText } : undefined;
}
if ((0, ast_1.isMemberAccessExpr)(expr)) {
return (0, ast_1.isThisExpr)(expr.operand) ? { name: expr.member.$refText } : undefined;
}
return undefined;
}
getAuthAccess(expr) {
if (!(0, ast_1.isMemberAccessExpr)(expr)) {
return undefined;
}
if ((0, sdk_1.isAuthInvocation)(expr.operand)) {
return `${this.options.authAccessor}?.${expr.member.$refText}`;
}
else {
const operand = this.getAuthAccess(expr.operand);
return operand ? `${operand}?.${expr.member.$refText}` : undefined;
}
}
nextVar(type = 'boolean') {
return this.variable(`__var${this.varCounter++}`, type);
}
variable(name, type) {
return `{ kind: 'variable', name: '${name}', type: '${type}' }`;
}
value(value, type) {
return `{ kind: 'value', value: ${value}, type: '${type}' }`;
}
}
exports.ConstraintTransformer = ConstraintTransformer;
//# sourceMappingURL=constraint-transformer.js.map