zenstack
Version:
FullStack enhancement for Prisma ORM: seamless integration from database to UI
732 lines • 30.4 kB
JavaScript
"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