UNPKG

zenstack

Version:

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

732 lines 30.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExpressionWriter = exports.FALSE = exports.TRUE = void 0; const ast_1 = require("@zenstackhq/language/ast"); const runtime_1 = require("@zenstackhq/runtime"); const sdk_1 = require("@zenstackhq/sdk"); const lower_case_first_1 = require("lower-case-first"); const tiny_invariant_1 = __importDefault(require("tiny-invariant")); const __1 = require(".."); const ast_utils_1 = require("../../../utils/ast-utils"); // { OR: [] } filters to nothing, { AND: [] } includes everything // https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined#the-effect-of-null-and-undefined-on-conditionals exports.TRUE = '{ AND: [] }'; exports.FALSE = '{ OR: [] }'; /** * Utility for writing ZModel expression as Prisma query argument objects into a ts-morph writer */ class ExpressionWriter { /** * Constructs a new ExpressionWriter */ constructor(writer, options) { this.writer = writer; this.options = options; this.plainExprBuilder = new sdk_1.TypeScriptExpressionTransformer({ context: sdk_1.ExpressionContext.AccessPolicy, isPostGuard: this.options.isPostGuard, // in post-guard context, `this` references pre-update value thisExprContext: this.options.isPostGuard ? 'context.preValue' : undefined, operationContext: this.options.operationContext, }); } /** * Writes the given ZModel expression. */ write(expr) { switch (expr.$type) { case ast_1.StringLiteral: case ast_1.NumberLiteral: case ast_1.BooleanLiteral: this.writeLiteral(expr); break; case ast_1.UnaryExpr: this.writeUnary(expr); break; case ast_1.BinaryExpr: this.writeBinary(expr); break; case ast_1.ReferenceExpr: this.writeReference(expr); break; case ast_1.MemberAccessExpr: this.writeMemberAccess(expr); break; case ast_1.InvocationExpr: this.writeInvocation(expr); break; default: throw new Error(`Not implemented: ${expr.$type}`); } } writeReference(expr) { if ((0, ast_1.isEnumField)(expr.target.ref)) { throw new Error('We should never get here'); } else { this.block(() => { const ref = expr.target.ref; (0, tiny_invariant_1.default)(ref); if (this.isFieldReferenceToDelegateModel(ref)) { const thisModel = ref.$container; const targetBase = ref.$inheritedFrom; this.writeBaseHierarchy(thisModel, targetBase, () => this.writer.write(`${ref.name}: true`)); } else { this.writer.write(`${ref.name}: true`); } }); } } writeBaseHierarchy(thisModel, targetBase, conditionWriter) { if (!targetBase || thisModel === targetBase) { conditionWriter(); return; } const base = this.getDelegateBase(thisModel); if (!base) { throw new sdk_1.PluginError(__1.name, `Failed to resolve delegate base model for "${thisModel.name}"`); } this.writer.write(`${`${runtime_1.DELEGATE_AUX_RELATION_PREFIX}_${(0, lower_case_first_1.lowerCaseFirst)(base.name)}`}: `); this.writer.block(() => { this.writeBaseHierarchy(base, targetBase, conditionWriter); }); } getDelegateBase(model) { var _a; return (_a = model.superTypes.map((t) => t.ref).filter((t) => t && (0, sdk_1.isDelegateModel)(t))) === null || _a === void 0 ? void 0 : _a[0]; } isFieldReferenceToDelegateModel(ref) { return (0, ast_1.isDataModelField)(ref) && !!ref.$inheritedFrom && (0, sdk_1.isDelegateModel)(ref.$inheritedFrom); } writeMemberAccess(expr) { if (this.isAuthOrAuthMemberAccess(expr)) { // member access of `auth()`, generate plain expression this.guard(() => this.plain(expr), true); } else { this.block(() => { // must be a boolean member this.writeFieldCondition(expr.operand, () => { this.block(() => { var _a; this.writer.write(`${(_a = expr.member.ref) === null || _a === void 0 ? void 0 : _a.name}: true`); }); }); }); } } writeExprList(exprs) { this.writer.write('['); for (let i = 0; i < exprs.length; i++) { this.write(exprs[i]); if (i !== exprs.length - 1) { this.writer.write(','); } } this.writer.write(']'); } writeBinary(expr) { switch (expr.operator) { case '&&': case '||': this.writeLogical(expr, expr.operator); break; case '==': case '!=': case '>': case '>=': case '<': case '<=': this.writeComparison(expr, expr.operator); break; case 'in': this.writeIn(expr); break; case '?': case '!': case '^': this.writeCollectionPredicate(expr, expr.operator); break; } } writeIn(expr) { const leftIsFieldAccess = this.isFieldAccess(expr.left); const rightIsFieldAccess = this.isFieldAccess(expr.right); if (!leftIsFieldAccess && !rightIsFieldAccess) { // 'in' without referencing fields this.guard(() => this.plain(expr)); } else { this.block(() => { var _a, _b; if (leftIsFieldAccess && !rightIsFieldAccess) { // 'in' with left referencing a field, right is an array literal this.writeFieldCondition(expr.left, () => { this.plain(expr.right); }, 'in'); } else if (!leftIsFieldAccess && rightIsFieldAccess) { // 'in' with right referencing an array field, left is a literal // transform it into a 'has' filter this.writeFieldCondition(expr.right, () => { this.plain(expr.left); }, 'has'); } else if ((0, sdk_1.isDataModelFieldReference)(expr.left) && (0, sdk_1.isDataModelFieldReference)(expr.right) && ((_a = expr.left.target.ref) === null || _a === void 0 ? void 0 : _a.$container) === ((_b = expr.right.target.ref) === null || _b === void 0 ? void 0 : _b.$container)) { // comparing two fields of the same model this.writeFieldCondition(expr.left, () => { this.writeFieldReference(expr.right); }, 'in'); } else { throw new sdk_1.PluginError(__1.name, '"in" operator cannot be used with field references on both sides'); } }); } } writeCollectionPredicate(expr, operator) { // check if the operand should be compiled to a relation query // or a plain expression const compileToRelationQuery = // expression rooted to `auth()` is always compiled to plain expression !this.isAuthOrAuthMemberAccess(expr.left) && // `future()` in post-update context ((this.options.isPostGuard && this.isFutureMemberAccess(expr.left)) || // non-`future()` in pre-update context (!this.options.isPostGuard && !this.isFutureMemberAccess(expr.left))); if (compileToRelationQuery) { this.block(() => { this.writeFieldCondition(expr.left, () => { // inner scope of collection expression is always compiled as non-post-guard const innerWriter = new ExpressionWriter(this.writer, { isPostGuard: false, operationContext: this.options.operationContext, }); innerWriter.write(expr.right); }, operator === '?' ? 'some' : operator === '!' ? 'every' : 'none'); }); } else { const plain = this.plainExprBuilder.transform(expr); this.writer.write(`${plain} ? ${exports.TRUE} : ${exports.FALSE}`); } } isFieldAccess(expr) { if ((0, ast_1.isThisExpr)(expr)) { return true; } if ((0, ast_1.isMemberAccessExpr)(expr)) { if ((0, sdk_1.isFutureExpr)(expr.operand) && this.options.isPostGuard) { // when writing for post-update, future().field.x is a field access return true; } else { return this.isFieldAccess(expr.operand); } } if ((0, sdk_1.isDataModelFieldReference)(expr) && !this.options.isPostGuard) { return true; } return false; } guard(condition, cast = false) { if (cast) { this.writer.write('!!'); condition(); } else { condition(); } this.writer.write(` ? ${exports.TRUE} : ${exports.FALSE}`); } plain(expr) { try { this.writer.write(this.plainExprBuilder.transform(expr)); } catch (err) { if (err instanceof sdk_1.TypeScriptExpressionTransformerError) { throw new sdk_1.PluginError(__1.name, err.message); } else { throw err; } } } writeIdFieldsCheck(model, value) { const idFields = this.requireIdFields(model); idFields.forEach((idField, idx) => { // eg: id: user.id this.writer.write(`${idField.name}:`); this.plain(value); this.writer.write(`.${idField.name}`); if (idx !== idFields.length - 1) { this.writer.write(','); } }); } writeComparison(expr, operator) { var _a; const leftIsFieldAccess = this.isFieldAccess(expr.left); const rightIsFieldAccess = this.isFieldAccess(expr.right); if (!leftIsFieldAccess && !rightIsFieldAccess) { // compile down to a plain expression this.guard(() => { this.plain(expr); }); return; } let fieldAccess; let operand; if (leftIsFieldAccess) { fieldAccess = expr.left; operand = expr.right; } else { fieldAccess = expr.right; operand = expr.left; operator = this.negateOperator(operator); } // future()...field should be treated as the "field" directly, so we // strip 'future().' and synthesize a reference/member-access expr fieldAccess = this.stripFutureCall(fieldAccess); // guard member access of `auth()` with null check if (this.isAuthOrAuthMemberAccess(operand) && !((_a = fieldAccess.$resolvedType) === null || _a === void 0 ? void 0 : _a.nullable)) { try { this.writer.write(`(${this.plainExprBuilder.transform(operand)} == null) ? ${ // auth().x != user.x is true when auth().x is null and user is not nullable // other expressions are evaluated to false when null is involved operator === '!=' ? exports.TRUE : exports.FALSE} : `); } catch (err) { if (err instanceof sdk_1.TypeScriptExpressionTransformerError) { throw new sdk_1.PluginError(__1.name, err.message); } else { throw err; } } } this.block(() => { this.writeFieldCondition(fieldAccess, () => { this.block(() => { var _a; const dataModel = this.isModelTyped(fieldAccess); if (dataModel && (0, sdk_1.isAuthInvocation)(operand)) { // right now this branch only serves comparison with `auth`, like // @@allow('all', owner == auth()) if (operator !== '==' && operator !== '!=') { throw new sdk_1.PluginError(__1.name, 'Only == and != operators are allowed'); } if (!(0, ast_1.isThisExpr)(fieldAccess)) { this.writer.writeLine(operator === '==' ? 'is:' : 'isNot:'); const fieldIsNullable = !!((_a = fieldAccess.$resolvedType) === null || _a === void 0 ? void 0 : _a.nullable); if (fieldIsNullable) { // if field is nullable, we can generate "null" check condition this.writer.write(`(user == null) ? null : `); } } this.block(() => { if ((0, ast_1.isThisExpr)(fieldAccess) && operator === '!=') { // negate this.writer.writeLine('isNot:'); this.block(() => this.writeIdFieldsCheck(dataModel, operand)); } else { this.writeIdFieldsCheck(dataModel, operand); } }); } else { if (this.equivalentRefs(fieldAccess, operand)) { // f == f or f != f // this == this or this != this this.writer.write(operator === '!=' ? exports.TRUE : exports.FALSE); } else { this.writeOperator(operator, fieldAccess, () => { if ((0, sdk_1.isDataModelFieldReference)(operand) && !this.options.isPostGuard) { // if operand is a field reference and we're not generating for post-update guard, // we should generate a field reference (comparing fields in the same model) this.writeFieldReference(operand); } else { if (dataModel && this.isModelTyped(operand)) { // the comparison is between model types, generate id fields comparison block this.block(() => this.writeIdFieldsCheck(dataModel, operand)); } else { // scalar value, just generate the plain expression this.plain(operand); } } }); } } }, !(0, ast_1.isThisExpr)(fieldAccess)); }); }, // "this" expression is compiled away (to .id access), so we should // avoid generating a new layer !(0, ast_1.isThisExpr)(fieldAccess)); } stripFutureCall(fieldAccess) { if (!this.isFutureMemberAccess(fieldAccess)) { return fieldAccess; } const memberAccessStack = []; let current = fieldAccess; while ((0, ast_1.isMemberAccessExpr)(current)) { memberAccessStack.push(current); current = current.operand; } const top = memberAccessStack.pop(); // turn the inner-most member access into a reference expr (strip 'future()') let result = { $type: ast_1.ReferenceExpr, $container: top.$container, target: top.member, $resolvedType: top.$resolvedType, args: [], }; // eslint-disable-next-line @typescript-eslint/no-explicit-any result.$future = true; // re-apply member accesses for (const memberAccess of memberAccessStack.reverse()) { result = Object.assign(Object.assign({}, memberAccess), { operand: result }); } return result; } isFutureMemberAccess(expr) { if (!(0, ast_1.isMemberAccessExpr)(expr)) { return false; } if ((0, sdk_1.isFutureExpr)(expr.operand)) { return true; } return this.isFutureMemberAccess(expr.operand); } requireIdFields(dataModel) { const idFields = (0, sdk_1.getIdFields)(dataModel); if (!idFields || idFields.length === 0) { throw new sdk_1.PluginError(__1.name, `Data model ${dataModel.name} does not have an id field`); } return idFields; } equivalentRefs(expr1, expr2) { if ((0, ast_1.isThisExpr)(expr1) && (0, ast_1.isThisExpr)(expr2)) { return true; } if ((0, ast_1.isReferenceExpr)(expr1) && (0, ast_1.isReferenceExpr)(expr2) && expr1.target.ref === expr2.target.ref && // eslint-disable-next-line @typescript-eslint/no-explicit-any expr1.$future === expr2.$future // either both future or both not ) { return true; } return false; } // https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#compare-columns-in-the-same-table writeFieldReference(expr) { if (!expr.target.ref) { throw new sdk_1.PluginError(__1.name, `Unresolved reference "${expr.target.$refText}"`); } const containingModel = expr.target.ref.$container; this.writer.write(`db.${(0, lower_case_first_1.lowerCaseFirst)(containingModel.name)}.fields.${expr.target.ref.name}`); } isAuthOrAuthMemberAccess(expr) { // recursive check for auth().x.y.z return (0, sdk_1.isAuthInvocation)(expr) || ((0, ast_1.isMemberAccessExpr)(expr) && this.isAuthOrAuthMemberAccess(expr.operand)); } writeOperator(operator, fieldAccess, writeOperand) { var _a; if ((0, ast_1.isDataModel)((_a = fieldAccess.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl)) { if (operator === '==') { this.writer.write('is: '); } else if (operator === '!=') { this.writer.write('isNot: '); } else { throw new sdk_1.PluginError(__1.name, 'Only == and != operators are allowed for data model comparison'); } writeOperand(); } else { if (operator === '!=') { // wrap a 'not' this.writer.write('not: '); this.block(() => { this.writer.write(`${this.mapOperator('==')}: `); writeOperand(); }); } else { this.writer.write(`${this.mapOperator(operator)}: `); writeOperand(); } } } writeFieldCondition(fieldAccess, writeCondition, filterOp, extraArgs) { // let selector: string | undefined; let operand; let fieldWriter; if ((0, ast_1.isThisExpr)(fieldAccess)) { // pass on writeCondition(); return; } else if ((0, ast_1.isReferenceExpr)(fieldAccess)) { const ref = fieldAccess.target.ref; (0, tiny_invariant_1.default)(ref); if (this.isFieldReferenceToDelegateModel(ref)) { const thisModel = ref.$container; const targetBase = ref.$inheritedFrom; fieldWriter = (conditionWriter) => this.writeBaseHierarchy(thisModel, targetBase, () => { this.writer.write(`${ref.name}: `); conditionWriter(); }); } else { fieldWriter = (conditionWriter) => { this.writer.write(`${ref.name}: `); conditionWriter(); }; } } else if ((0, ast_1.isMemberAccessExpr)(fieldAccess)) { if (!(0, sdk_1.isFutureExpr)(fieldAccess.operand)) { // future().field should be treated as the "field" operand = fieldAccess.operand; } fieldWriter = (conditionWriter) => { var _a; this.writer.write(`${(_a = fieldAccess.member.ref) === null || _a === void 0 ? void 0 : _a.name}: `); conditionWriter(); }; } else { throw new sdk_1.PluginError(__1.name, `Unsupported expression type: ${fieldAccess.$type}`); } if (!fieldWriter) { throw new sdk_1.PluginError(__1.name, `Failed to write FieldAccess expression`); } const writerFilterOutput = () => { // this.writer.write(selector + ': '); fieldWriter(() => { if (filterOp) { this.block(() => { this.writer.write(`${filterOp}: `); writeCondition(); if (extraArgs) { for (const [k, v] of Object.entries(extraArgs)) { this.writer.write(`,\n${k}: `); this.plain(v); } } }); } else { writeCondition(); } }); }; if (operand) { // member access expression this.writeFieldCondition(operand, () => { this.block(writerFilterOutput, // if operand is "this", it doesn't really generate a new layer of query, // so we should avoid generating a new block !(0, ast_1.isThisExpr)(operand)); }); } else { writerFilterOutput(); } } block(write, condition = true) { if (condition) { this.writer.inlineBlock(write); } else { write(); } } isModelTyped(expr) { var _a, _b; return (0, ast_1.isDataModel)((_a = expr.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl) ? (_b = expr.$resolvedType) === null || _b === void 0 ? void 0 : _b.decl : undefined; } mapOperator(operator) { switch (operator) { case '==': return 'equals'; case '!=': throw new Error('Operation != should have been compiled away'); case '>': return 'gt'; case '>=': return 'gte'; case '<': return 'lt'; case '<=': return 'lte'; } } negateOperator(operator) { switch (operator) { case '>': return '<='; case '<': return '>='; case '>=': return '<'; case '<=': return '>'; default: return operator; } } writeLogical(expr, operator) { // TODO: do we need short-circuit for logical operators? if (operator === '&&') { // // && short-circuit: left && right -> left ? right : FALSE // if (!this.hasFieldAccess(expr.left)) { // this.plain(expr.left); // this.writer.write(' ? '); // this.write(expr.right); // this.writer.write(' : '); // this.block(() => this.guard(() => this.writer.write('false'))); // } else { this.block(() => { this.writer.write('AND:'); this.writeExprList([expr.left, expr.right]); }); // } } else { // // || short-circuit: left || right -> left ? TRUE : right // if (!this.hasFieldAccess(expr.left)) { // this.plain(expr.left); // this.writer.write(' ? '); // this.block(() => this.guard(() => this.writer.write('true'))); // this.writer.write(' : '); // this.write(expr.right); // } else { this.block(() => { this.writer.write('OR:'); this.writeExprList([expr.left, expr.right]); }); // } } } writeUnary(expr) { if (expr.operator !== '!') { throw new sdk_1.PluginError(__1.name, `Unary operator "${expr.operator}" is not supported`); } this.block(() => { this.writer.write('NOT: '); this.write(expr.operand); }); } writeLiteral(expr) { if (expr.value === true) { this.writer.write(exports.TRUE); } else if (expr.value === false) { this.writer.write(exports.FALSE); } else { this.guard(() => { this.plain(expr); }); } } writeInvocation(expr) { var _a, _b; const funcDecl = expr.function.ref; if (!funcDecl) { throw new sdk_1.PluginError(__1.name, `Failed to resolve function declaration`); } const functionAllowedContext = (0, sdk_1.getFunctionExpressionContext)(funcDecl); if (functionAllowedContext.includes(sdk_1.ExpressionContext.AccessPolicy) || functionAllowedContext.includes(sdk_1.ExpressionContext.ValidationRule)) { if ((0, ast_utils_1.isCheckInvocation)(expr)) { this.writeRelationCheck(expr); return; } if (!expr.args.some((arg) => this.isFieldAccess(arg.value))) { // filter functions without referencing fields this.guard(() => this.plain(expr)); return; } let valueArg = (_a = expr.args[1]) === null || _a === void 0 ? void 0 : _a.value; // isEmpty function is zero arity, it's mapped to a boolean literal if ((0, sdk_1.isFromStdlib)(funcDecl) && funcDecl.name === 'isEmpty') { valueArg = { $type: ast_1.BooleanLiteral, value: true }; } // contains function has a 3rd argument that indicates whether the comparison should be case-insensitive let extraArgs = undefined; if ((0, sdk_1.isFromStdlib)(funcDecl) && funcDecl.name === 'contains') { if ((0, sdk_1.getLiteral)((_b = expr.args[2]) === null || _b === void 0 ? void 0 : _b.value) === true) { extraArgs = { mode: { $type: ast_1.StringLiteral, value: 'insensitive' } }; } } this.block(() => { this.writeFieldCondition(expr.args[0].value, () => { this.plain(valueArg); }, funcDecl.name, extraArgs); }); } else { throw new sdk_1.PluginError(__1.name, `Unsupported function ${funcDecl.name}`); } } writeRelationCheck(expr) { var _a, _b; if (!(0, sdk_1.isDataModelFieldReference)(expr.args[0].value)) { throw new sdk_1.PluginError(__1.name, `First argument of check() must be a field`); } if (!(0, ast_1.isDataModel)((_a = expr.args[0].value.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl)) { throw new sdk_1.PluginError(__1.name, `First argument of check() must be a relation field`); } const fieldRef = expr.args[0].value; const targetModel = (_b = fieldRef.$resolvedType) === null || _b === void 0 ? void 0 : _b.decl; let operation; if (expr.args[1]) { const literal = (0, sdk_1.getLiteral)(expr.args[1].value); if (!literal) { throw new sdk_1.TypeScriptExpressionTransformerError(`Second argument of check() must be a string literal`); } if (!['read', 'create', 'update', 'delete'].includes(literal)) { throw new sdk_1.TypeScriptExpressionTransformerError(`Invalid check() operation "${literal}"`); } operation = literal; } else { if (!this.options.operationContext) { throw new sdk_1.TypeScriptExpressionTransformerError('Unable to determine CRUD operation from context'); } operation = this.options.operationContext; } this.block(() => this.writeFieldCondition(fieldRef, () => { if (operation === 'postUpdate') { // 'postUpdate' policies are not delegated to relations, just use constant `false` here // e.g.: // @@allow('all', check(author)) should not delegate "postUpdate" to author this.writer.write(exports.FALSE); } else { const targetGuardFunc = (0, sdk_1.getQueryGuardFunctionName)(targetModel, undefined, false, operation); this.writer.write(`${targetGuardFunc}(context, db)`); } })); } } exports.ExpressionWriter = ExpressionWriter; //# sourceMappingURL=expression-writer.js.map