Skip to content

Commit d921a7c

Browse files
committed
fix: post-update rules incorrectly reject update (#826)
1 parent 2c345e1 commit d921a7c

File tree

20 files changed

+349
-53
lines changed

20 files changed

+349
-53
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-monorepo",
3-
"version": "1.2.1",
3+
"version": "1.2.2",
44
"description": "",
55
"scripts": {
66
"build": "pnpm -r build",

packages/language/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/language",
3-
"version": "1.2.1",
3+
"version": "1.2.2",
44
"displayName": "ZenStack modeling language compiler",
55
"description": "ZenStack modeling language compiler",
66
"homepage": "https://zenstack.dev",

packages/plugins/openapi/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/openapi",
33
"displayName": "ZenStack Plugin and Runtime for OpenAPI",
4-
"version": "1.2.1",
4+
"version": "1.2.2",
55
"description": "ZenStack plugin and runtime supporting OpenAPI",
66
"main": "index.js",
77
"repository": {

packages/plugins/swr/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/swr",
33
"displayName": "ZenStack plugin for generating SWR hooks",
4-
"version": "1.2.1",
4+
"version": "1.2.2",
55
"description": "ZenStack plugin for generating SWR hooks",
66
"main": "index.js",
77
"repository": {

packages/plugins/tanstack-query/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/tanstack-query",
33
"displayName": "ZenStack plugin for generating tanstack-query hooks",
4-
"version": "1.2.1",
4+
"version": "1.2.2",
55
"description": "ZenStack plugin for generating tanstack-query hooks",
66
"main": "index.js",
77
"exports": {

packages/plugins/trpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/trpc",
33
"displayName": "ZenStack plugin for tRPC",
4-
"version": "1.2.1",
4+
"version": "1.2.2",
55
"description": "ZenStack plugin for tRPC",
66
"main": "index.js",
77
"repository": {

packages/runtime/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/runtime",
33
"displayName": "ZenStack Runtime Library",
4-
"version": "1.2.1",
4+
"version": "1.2.2",
55
"description": "Runtime of ZenStack for both client-side and server-side environments.",
66
"repository": {
77
"type": "git",

packages/schema/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publisher": "zenstack",
44
"displayName": "ZenStack Language Tools",
55
"description": "A toolkit for building secure CRUD apps with Next.js + Typescript",
6-
"version": "1.2.1",
6+
"version": "1.2.2",
77
"author": {
88
"name": "ZenStack Team"
99
},

packages/schema/src/plugins/access-policy/expression-writer.ts

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -223,15 +223,28 @@ export class ExpressionWriter {
223223
}
224224

225225
private writeCollectionPredicate(expr: BinaryExpr, operator: string) {
226-
this.block(() => {
227-
this.writeFieldCondition(
228-
expr.left,
229-
() => {
230-
this.write(expr.right);
231-
},
232-
operator === '?' ? 'some' : operator === '!' ? 'every' : 'none'
233-
);
234-
});
226+
// check if the operand should be compiled to a relation query
227+
// or a plain expression
228+
const compileToRelationQuery =
229+
(this.isPostGuard && this.isFutureMemberAccess(expr.left)) ||
230+
(!this.isPostGuard && !this.isFutureMemberAccess(expr.left));
231+
232+
if (compileToRelationQuery) {
233+
this.block(() => {
234+
this.writeFieldCondition(
235+
expr.left,
236+
() => {
237+
// inner scope of collection expression is always compiled as non-post-guard
238+
const innerWriter = new ExpressionWriter(this.writer, false);
239+
innerWriter.write(expr.right);
240+
},
241+
operator === '?' ? 'some' : operator === '!' ? 'every' : 'none'
242+
);
243+
});
244+
} else {
245+
const plain = this.plainExprBuilder.transform(expr);
246+
this.writer.write(`${plain} ? ${TRUE} : ${FALSE}`);
247+
}
235248
}
236249

237250
private isFieldAccess(expr: Expression): boolean {
@@ -275,6 +288,19 @@ export class ExpressionWriter {
275288
}
276289
}
277290

291+
private writeIdFieldsCheck(model: DataModel, value: Expression) {
292+
const idFields = this.requireIdFields(model);
293+
idFields.forEach((idField, idx) => {
294+
// eg: id: user.id
295+
this.writer.write(`${idField.name}:`);
296+
this.plain(value);
297+
this.writer.write(`.${idField.name}`);
298+
if (idx !== idFields.length - 1) {
299+
this.writer.write(',');
300+
}
301+
});
302+
}
303+
278304
private writeComparison(expr: BinaryExpr, operator: ComparisonOperator) {
279305
const leftIsFieldAccess = this.isFieldAccess(expr.left);
280306
const rightIsFieldAccess = this.isFieldAccess(expr.right);
@@ -298,7 +324,7 @@ export class ExpressionWriter {
298324
operator = this.negateOperator(operator);
299325
}
300326

301-
if (isMemberAccessExpr(fieldAccess) && isFutureExpr(fieldAccess.operand)) {
327+
if (this.isFutureMemberAccess(fieldAccess)) {
302328
// future().field should be treated as the "field" directly, so we
303329
// strip 'future().' and synthesize a reference expr
304330
fieldAccess = {
@@ -338,8 +364,6 @@ export class ExpressionWriter {
338364
// right now this branch only serves comparison with `auth`, like
339365
// @@allow('all', owner == auth())
340366

341-
const idFields = this.requireIdFields(dataModel);
342-
343367
if (operator !== '==' && operator !== '!=') {
344368
throw new PluginError(name, 'Only == and != operators are allowed');
345369
}
@@ -354,25 +378,13 @@ export class ExpressionWriter {
354378
}
355379

356380
this.block(() => {
357-
idFields.forEach((idField, idx) => {
358-
const writeIdsCheck = () => {
359-
// id: user.id
360-
this.writer.write(`${idField.name}:`);
361-
this.plain(operand);
362-
this.writer.write(`.${idField.name}`);
363-
if (idx !== idFields.length - 1) {
364-
this.writer.write(',');
365-
}
366-
};
367-
368-
if (isThisExpr(fieldAccess) && operator === '!=') {
369-
// wrap a not
370-
this.writer.writeLine('NOT:');
371-
this.block(() => writeIdsCheck());
372-
} else {
373-
writeIdsCheck();
374-
}
375-
});
381+
if (isThisExpr(fieldAccess) && operator === '!=') {
382+
// negate
383+
this.writer.writeLine('isNot:');
384+
this.block(() => this.writeIdFieldsCheck(dataModel, operand));
385+
} else {
386+
this.writeIdFieldsCheck(dataModel, operand);
387+
}
376388
});
377389
} else {
378390
if (this.equivalentRefs(fieldAccess, operand)) {
@@ -386,7 +398,13 @@ export class ExpressionWriter {
386398
// we should generate a field reference (comparing fields in the same model)
387399
this.writeFieldReference(operand);
388400
} else {
389-
this.plain(operand);
401+
if (dataModel && this.isModelTyped(operand)) {
402+
// the comparison is between model types, generate id fields comparison block
403+
this.block(() => this.writeIdFieldsCheck(dataModel, operand));
404+
} else {
405+
// scalar value, just generate the plain expression
406+
this.plain(operand);
407+
}
390408
}
391409
});
392410
}
@@ -400,6 +418,10 @@ export class ExpressionWriter {
400418
);
401419
}
402420

421+
private isFutureMemberAccess(expr: Expression): expr is MemberAccessExpr {
422+
return isMemberAccessExpr(expr) && isFutureExpr(expr.operand);
423+
}
424+
403425
private requireIdFields(dataModel: DataModel) {
404426
const idFields = getIdFields(dataModel);
405427
if (!idFields || idFields.length === 0) {

packages/schema/src/plugins/access-policy/policy-guard-generator.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,16 @@ export default class PolicyGenerator {
207207
}
208208

209209
private processUpdatePolicies(expressions: Expression[], postUpdate: boolean) {
210-
return expressions
211-
.map((expr) => this.visitPolicyExpression(expr, postUpdate))
212-
.filter((e): e is Expression => !!e);
210+
const hasFutureReference = expressions.some((expr) => this.hasFutureReference(expr));
211+
if (postUpdate) {
212+
// when compiling post-update rules, if any rule contains `future()` reference,
213+
// we include all as post-update rules
214+
return hasFutureReference ? expressions : [];
215+
} else {
216+
// when compiling pre-update rules, if any rule contains `future()` reference,
217+
// we completely skip pre-update check and defer them to post-update
218+
return hasFutureReference ? [] : expressions;
219+
}
213220
}
214221

215222
private visitPolicyExpression(expr: Expression, postUpdate: boolean): Expression | undefined {
@@ -543,6 +550,9 @@ export default class PolicyGenerator {
543550
} else {
544551
return [];
545552
}
553+
} else if (isInvocationExpr(expr)) {
554+
// recurse into function arguments
555+
return expr.args.flatMap((arg) => collectReferencePaths(arg.value));
546556
} else {
547557
// recurse
548558
const children = streamContents(expr)

0 commit comments

Comments
 (0)