UNPKG

zenstack

Version:

FullStack enhancement for Prisma ORM: seamless integration from database to UI

316 lines 13.8 kB
"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