Skip to content

Commit fd06893

Browse files
committed
feat(policy): post-update policies
1 parent 3b13b72 commit fd06893

File tree

22 files changed

+539
-78
lines changed

22 files changed

+539
-78
lines changed

TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
- [ ] Short-circuit pre-create check for scalar-field only policies
102102
- [x] Inject "on conflict do update"
103103
- [x] `check` function
104+
- [ ] Accessing tables not in the schema
104105
- [x] Migration
105106
- [ ] Databases
106107
- [x] SQLite

packages/language/res/stdlib.zmodel

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ attribute @@@deprecated(_ message: String)
666666
* @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations.
667667
* @param condition: a boolean expression that controls if the operation should be allowed.
668668
*/
669-
attribute @@allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean)
669+
attribute @@allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'post-update'","'delete'", "'all'"]), _ condition: Boolean)
670670

671671
/**
672672
* Defines an access policy that allows the annotated field to be read or updated.
@@ -684,7 +684,7 @@ attribute @allow(_ operation: String @@@completionHint(["'create'", "'read'", "'
684684
* @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations.
685685
* @param condition: a boolean expression that controls if the operation should be denied.
686686
*/
687-
attribute @@deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean)
687+
attribute @@deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'post-update'","'delete'", "'all'"]), _ condition: Boolean)
688688

689689
/**
690690
* Defines an access policy that denies the annotated field to be read or updated.
@@ -705,8 +705,8 @@ function check(field: Any, operation: String?): Boolean {
705705
} @@@expressionContext([AccessPolicy])
706706

707707
/**
708-
* Gets entities value before an update. Only valid when used in a "update" policy rule.
708+
* Gets entity's value before an update. Only valid when used in a "update" policy rule.
709709
*/
710-
function future(): Any {
710+
function before(): Any {
711711
} @@@expressionContext([AccessPolicy])
712712

packages/language/src/utils.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,6 @@ export function isRelationshipField(field: DataField) {
145145
return isDataModel(field.type.reference?.ref);
146146
}
147147

148-
export function isFutureExpr(node: AstNode) {
149-
return isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref);
150-
}
151-
152148
export function isDelegateModel(node: AstNode) {
153149
return isDataModel(node) && hasAttribute(node, '@@delegate');
154150
}
@@ -450,8 +446,8 @@ export function getAuthDecl(decls: (DataModel | TypeDef)[]) {
450446
return authModel;
451447
}
452448

453-
export function isFutureInvocation(node: AstNode) {
454-
return isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref);
449+
export function isBeforeInvocation(node: AstNode) {
450+
return isInvocationExpr(node) && node.function.ref?.name === 'before' && isFromStdlib(node.function.ref);
455451
}
456452

457453
export function isCollectionPredicate(node: AstNode): node is BinaryExpr {

packages/language/src/validators/attribute-application-validator.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ import {
2323
getAllAttributes,
2424
getStringLiteral,
2525
isAuthOrAuthMemberAccess,
26+
isBeforeInvocation,
2627
isCollectionPredicate,
2728
isDataFieldReference,
2829
isDelegateModel,
29-
isFutureExpr,
3030
isRelationshipField,
3131
mapBuiltinTypeToExpressionType,
3232
resolved,
@@ -166,7 +166,7 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
166166
});
167167
return;
168168
}
169-
this.validatePolicyKinds(kind, ['create', 'read', 'update', 'delete', 'all'], attr, accept);
169+
this.validatePolicyKinds(kind, ['create', 'read', 'update', 'post-update', 'delete', 'all'], attr, accept);
170170

171171
if ((kind === 'create' || kind === 'all') && attr.args[1]?.value) {
172172
// "create" rules cannot access non-owned relations because the entity does not exist yet, so
@@ -251,8 +251,8 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
251251
const kindItems = this.validatePolicyKinds(kind, ['read', 'update', 'all'], attr, accept);
252252

253253
const expr = attr.args[1]?.value;
254-
if (expr && AstUtils.streamAst(expr).some((node) => isFutureExpr(node))) {
255-
accept('error', `"future()" is not allowed in field-level policy rules`, { node: expr });
254+
if (expr && AstUtils.streamAst(expr).some((node) => isBeforeInvocation(node))) {
255+
accept('error', `"before()" is not allowed in field-level policy rules`, { node: expr });
256256
}
257257

258258
// 'update' rules are not allowed for relation fields

packages/language/src/validators/expression-validator.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import {
1111
isNullExpr,
1212
isReferenceExpr,
1313
isThisExpr,
14+
MemberAccessExpr,
1415
type ExpressionType,
1516
} from '../generated/ast';
1617

1718
import {
1819
findUpAst,
1920
isAuthInvocation,
2021
isAuthOrAuthMemberAccess,
22+
isBeforeInvocation,
2123
isDataFieldReference,
2224
isEnumFieldReference,
2325
typeAssignable,
@@ -59,12 +61,21 @@ export default class ExpressionValidator implements AstValidator<Expression> {
5961

6062
// extra validations by expression type
6163
switch (expr.$type) {
64+
case 'MemberAccessExpr':
65+
this.validateMemberAccessExpr(expr, accept);
66+
break;
6267
case 'BinaryExpr':
6368
this.validateBinaryExpr(expr, accept);
6469
break;
6570
}
6671
}
6772

73+
private validateMemberAccessExpr(expr: MemberAccessExpr, accept: ValidationAcceptor) {
74+
if (isBeforeInvocation(expr.operand) && isDataModel(expr.$resolvedType?.decl)) {
75+
accept('error', 'relation fields cannot be accessed from `before()`', { node: expr });
76+
}
77+
}
78+
6879
private validateBinaryExpr(expr: BinaryExpr, accept: ValidationAcceptor) {
6980
switch (expr.operator) {
7081
case 'in': {

packages/language/src/zmodel-linker.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ import {
5757
getAuthDecl,
5858
getContainingDataModel,
5959
isAuthInvocation,
60-
isFutureExpr,
60+
isBeforeInvocation,
6161
isMemberContainer,
6262
mapBuiltinTypeToExpressionType,
6363
} from './utils';
@@ -292,8 +292,8 @@ export class ZModelLinker extends DefaultLinker {
292292
if (authDecl) {
293293
node.$resolvedType = { decl: authDecl, nullable: true };
294294
}
295-
} else if (isFutureExpr(node)) {
296-
// future() function is resolved to current model
295+
} else if (isBeforeInvocation(node)) {
296+
// before() function is resolved to current model
297297
node.$resolvedType = { decl: getContainingDataModel(node) };
298298
} else {
299299
this.resolveToDeclaredType(node, funcDecl.returnType);

packages/language/src/zmodel-scope.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {
3737
getRecursiveBases,
3838
isAuthInvocation,
3939
isCollectionPredicate,
40-
isFutureInvocation,
40+
isBeforeInvocation,
4141
resolveImportUri,
4242
} from './utils';
4343

@@ -170,8 +170,8 @@ export class ZModelScopeProvider extends DefaultScopeProvider {
170170
return this.createScopeForAuth(node, globalScope);
171171
}
172172

173-
if (isFutureInvocation(operand)) {
174-
// resolve `future()` to the containing model
173+
if (isBeforeInvocation(operand)) {
174+
// resolve `before()` to the containing model
175175
return this.createScopeForContainingModel(node, globalScope);
176176
}
177177
return EMPTY_SCOPE;

packages/runtime/src/client/contract.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,21 @@ export interface ClientConstructor {
213213
*/
214214
export type CRUD = 'create' | 'read' | 'update' | 'delete';
215215

216+
/**
217+
* Extended CRUD operations including 'post-update'.
218+
*/
219+
export type CRUD_EXT = CRUD | 'post-update';
220+
216221
/**
217222
* CRUD operations.
218223
*/
219224
export const CRUD = ['create', 'read', 'update', 'delete'] as const;
220225

226+
/**
227+
* Extended CRUD operations including 'post-update'.
228+
*/
229+
export const CRUD_EXT = [...CRUD, 'post-update'] as const;
230+
221231
//#region Model operations
222232

223233
export type AllModelOperations<Schema extends SchemaDef, Model extends GetModels<Schema>> = {

packages/runtime/src/client/crud/operations/base.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,8 +1296,9 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
12961296
return { count: Number(result.numAffectedRows) } as Result;
12971297
} else {
12981298
const idFields = requireIdFields(this.schema, model);
1299-
const result = await query.returning(idFields as any).execute();
1300-
return result as Result;
1299+
const finalQuery = query.returning(idFields as any);
1300+
const result = await this.executeQuery(kysely, finalQuery, 'update');
1301+
return result.rows as Result;
13011302
}
13021303
}
13031304

packages/runtime/src/client/options.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Dialect, Expression, ExpressionBuilder, KyselyConfig } from 'kysely';
22
import type { GetModel, GetModels, ProcedureDef, SchemaDef } from '../schema';
33
import type { PrependParameter } from '../utils/type-utils';
4-
import type { ClientContract, CRUD, ProcedureFunc } from './contract';
4+
import type { ClientContract, CRUD_EXT, ProcedureFunc } from './contract';
55
import type { BaseCrudDialect } from './crud/dialects/base-dialect';
66
import type { RuntimePlugin } from './plugin';
77
import type { ToKyselySchema } from './query-builder';
@@ -30,7 +30,7 @@ export type ZModelFunctionContext<Schema extends SchemaDef> = {
3030
/**
3131
* The CRUD operation being performed
3232
*/
33-
operation: CRUD;
33+
operation: CRUD_EXT;
3434
};
3535

3636
export type ZModelFunction<Schema extends SchemaDef> = (

0 commit comments

Comments
 (0)