Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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);
}
}
}
7 changes: 6 additions & 1 deletion 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 All @@ -102,7 +107,7 @@ export class ExpressionEvaluator {

const left = this.evaluate(expr.left, context);
if (!left) {
return false;
return null;
}

invariant(Array.isArray(left), 'expected array');
Expand Down
Loading