Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
- [ ] CLI
- [x] generate
- [x] migrate
- [ ] db
- [x] db
- [x] push
- [ ] seed
- [x] seed
- [x] info
- [x] init
- [x] validate
- [ ] format
- [x] format
- [ ] repl
- [x] plugin mechanism
- [x] built-in plugins
Expand Down
11 changes: 11 additions & 0 deletions packages/language/src/validators/function-invocation-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,17 @@ export default class FunctionInvocationValidator implements AstValidator<Express
return true;
}

@func('auth')
private _checkAuth(expr: InvocationExpr, accept: ValidationAcceptor) {
if (!expr.$resolvedType) {
accept(
'error',
'cannot resolve `auth()` - make sure you have a model or type with `@auth` attribute or named "User"',
{ node: expr },
);
}
}

@func('length')
private _checkLength(expr: InvocationExpr, accept: ValidationAcceptor) {
const msg = 'argument must be a string or list field';
Expand Down
9 changes: 4 additions & 5 deletions packages/language/src/zmodel-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
getAuthDecl,
getRecursiveBases,
isAuthInvocation,
isAuthOrAuthMemberAccess,
isBeforeInvocation,
isCollectionPredicate,
resolveImportUri,
Expand Down Expand Up @@ -138,8 +139,7 @@ export class ZModelScopeProvider extends DefaultScopeProvider {
// typedef's fields are only added to the scope if the access starts with `auth().`
// or the member access resides inside a typedef
const allowTypeDefScope =
// isAuthOrAuthMemberAccess(node.operand) ||
!!AstUtils.getContainerOfType(node, isTypeDef);
isAuthOrAuthMemberAccess(node.operand) || !!AstUtils.getContainerOfType(node, isTypeDef);

return match(node.operand)
.when(isReferenceExpr, (operand) => {
Expand Down Expand Up @@ -184,10 +184,9 @@ export class ZModelScopeProvider extends DefaultScopeProvider {
const globalScope = this.getGlobalScope(referenceType, context);
const collection = collectionPredicate.left;

// TODO: generalize it
// TODO: full support of typedef member access
// // typedef's fields are only added to the scope if the access starts with `auth().`
// const allowTypeDefScope = isAuthOrAuthMemberAccess(collection);
const allowTypeDefScope = false;
const allowTypeDefScope = isAuthOrAuthMemberAccess(collection);

return match(collection)
.when(isReferenceExpr, (expr) => {
Expand Down
72 changes: 55 additions & 17 deletions packages/orm/src/utils/schema-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import type {
UnaryExpression,
} from '../schema';

export type VisitResult = void | { abort: true };

export class ExpressionVisitor {
visit(expr: Expression): void {
match(expr)
visit(expr: Expression): VisitResult {
return match(expr)
.with({ kind: 'literal' }, (e) => this.visitLiteral(e))
.with({ kind: 'array' }, (e) => this.visitArray(e))
.with({ kind: 'field' }, (e) => this.visitField(e))
Expand All @@ -27,32 +29,68 @@ export class ExpressionVisitor {
.exhaustive();
}

protected visitLiteral(_e: LiteralExpression) {}
protected visitLiteral(_e: LiteralExpression): VisitResult {}

protected visitArray(e: ArrayExpression) {
e.items.forEach((item) => this.visit(item));
protected visitArray(e: ArrayExpression): VisitResult {
for (const item of e.items) {
const result = this.visit(item);
if (result?.abort) {
return result;
}
}
}

protected visitField(_e: FieldExpression) {}
protected visitField(_e: FieldExpression): VisitResult {}

protected visitMember(e: MemberExpression) {
this.visit(e.receiver);
protected visitMember(e: MemberExpression): VisitResult {
return this.visit(e.receiver);
}

protected visitBinary(e: BinaryExpression) {
this.visit(e.left);
this.visit(e.right);
protected visitBinary(e: BinaryExpression): VisitResult {
const l = this.visit(e.left);
if (l?.abort) {
return l;
} else {
return this.visit(e.right);
}
}

protected visitUnary(e: UnaryExpression) {
this.visit(e.operand);
protected visitUnary(e: UnaryExpression): VisitResult {
return this.visit(e.operand);
}

protected visitCall(e: CallExpression) {
e.args?.forEach((arg) => this.visit(arg));
protected visitCall(e: CallExpression): VisitResult {
for (const arg of e.args ?? []) {
const r = this.visit(arg);
if (r?.abort) {
return r;
}
}
}

protected visitThis(_e: ThisExpression) {}
protected visitThis(_e: ThisExpression): VisitResult {}

protected visitNull(_e: NullExpression): VisitResult {}
}

protected visitNull(_e: NullExpression) {}
export class MatchingExpressionVisitor extends ExpressionVisitor {
private found = false;

constructor(private predicate: (expr: Expression) => boolean) {
super();
}

find(expr: Expression) {
this.visit(expr);
return this.found;
}

override visit(expr: Expression) {
if (this.predicate(expr)) {
this.found = true;
return { abort: true } as const;
} else {
return super.visit(expr);
}
}
}
5 changes: 5 additions & 0 deletions packages/plugins/policy/src/expression-evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export class ExpressionEvaluator {
const left = this.evaluate(expr.left, context);
const right = this.evaluate(expr.right, context);

if (!['==', '!='].includes(expr.op) && (left === null || right === null)) {
// non-equality comparison with null always yields null (follow SQL logic)
return null;
}

return match(expr.op)
.with('==', () => left === right)
.with('!=', () => left !== right)
Expand Down
Loading
Loading