diff --git a/packages/plugins/policy/src/expression-transformer.ts b/packages/plugins/policy/src/expression-transformer.ts index c885b504c..119341472 100644 --- a/packages/plugins/policy/src/expression-transformer.ts +++ b/packages/plugins/policy/src/expression-transformer.ts @@ -326,14 +326,13 @@ export class ExpressionTransformer { const rightFieldDef = this.getFieldDefFromFieldRef(normalizedRight, context); // Map ZModel operator to SQL operator string const sqlOp = op === '==' ? '=' : op; + // `before()` enums are text in the VALUES table and pg can't compare text to a native enum + const asText = + this.isBeforeEnumField(normalizedLeft, context) || this.isBeforeEnumField(normalizedRight, context); + const operand = (node: OperationNode) => + asText ? this.dialect.castText(new ExpressionWrapper(node)) : new ExpressionWrapper(node); return this.dialect - .buildComparison( - new ExpressionWrapper(left), - leftFieldDef, - sqlOp, - new ExpressionWrapper(right), - rightFieldDef, - ) + .buildComparison(operand(left), leftFieldDef, sqlOp, operand(right), rightFieldDef) .toOperationNode(); } } @@ -1195,6 +1194,14 @@ export class ExpressionTransformer { return !!fieldDef?.relation; } + private isBeforeEnumField(expr: Expression, context: ExpressionTransformerContext) { + if (!ExpressionUtils.isMember(expr) || !isBeforeInvocation(expr.receiver)) { + return false; + } + const fieldDef = QueryUtils.getField(this.schema, context.modelOrType, expr.members[0]!); + return !!fieldDef && QueryUtils.isEnum(this.schema, fieldDef.type); + } + private getFieldDefFromFieldRef(expr: Expression, context: ExpressionTransformerContext): FieldDef | undefined { return this.resolveFieldRef(expr, context)?.fieldDef; } diff --git a/tests/regression/test/issue-2727.test.ts b/tests/regression/test/issue-2727.test.ts new file mode 100644 index 000000000..cb5d4e488 --- /dev/null +++ b/tests/regression/test/issue-2727.test.ts @@ -0,0 +1,28 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue #2727', () => { + it('compares before() enum fields in post-update rules on PostgreSQL native enums', async () => { + const db = await createPolicyTestClient( + ` +enum State { + DRAFT + IN_PROGRESS + DONE +} + +model Post { + id Int @id + state State @default(DRAFT) + @@allow('all', true) + @@deny('post-update', before().state != state && before().state == DRAFT && state != IN_PROGRESS) +} + `, + { provider: 'postgresql', usePrismaPush: true }, + ); + + await db.post.create({ data: { id: 1 } }); + await expect(db.post.update({ where: { id: 1 }, data: { state: 'DONE' } })).toBeRejectedByPolicy(); + await db.post.update({ where: { id: 1 }, data: { state: 'IN_PROGRESS' } }); + }); +});