Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@
- [ ] Short-circuit pre-create check for scalar-field only policies
- [x] Inject "on conflict do update"
- [x] `check` function
- [ ] Custom functions
- [ ] Accessing tables not in the schema
- [x] Migration
- [ ] Databases
- [x] SQLite
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zenstack-v3",
"version": "3.0.0-beta.7",
"version": "3.0.0-beta.8",
"description": "ZenStack",
"packageManager": "[email protected]",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"publisher": "zenstack",
"displayName": "ZenStack CLI",
"description": "FullStack database toolkit with built-in access control and automatic API generation.",
"version": "3.0.0-beta.7",
"version": "3.0.0-beta.8",
"type": "module",
"author": {
"name": "ZenStack Team"
Expand Down
2 changes: 1 addition & 1 deletion packages/common-helpers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/common-helpers",
"version": "3.0.0-beta.7",
"version": "3.0.0-beta.8",
"description": "ZenStack Common Helpers",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/create-zenstack/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-zenstack",
"version": "3.0.0-beta.7",
"version": "3.0.0-beta.8",
"description": "Create a new ZenStack project",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/dialects/sql.js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/kysely-sql-js",
"version": "3.0.0-beta.7",
"version": "3.0.0-beta.8",
"description": "Kysely dialect for sql.js",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/eslint-config",
"version": "3.0.0-beta.7",
"version": "3.0.0-beta.8",
"type": "module",
"private": true,
"license": "MIT"
Expand Down
2 changes: 1 addition & 1 deletion packages/ide/vscode/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zenstack-v3",
"publisher": "zenstack",
"version": "3.0.8",
"version": "3.0.9",
"displayName": "ZenStack V3 Language Tools",
"description": "VSCode extension for ZenStack (v3) ZModel language",
"private": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/language",
"description": "ZenStack ZModel language specification",
"version": "3.0.0-beta.7",
"version": "3.0.0-beta.8",
"license": "MIT",
"author": "ZenStack Team",
"files": [
Expand Down
8 changes: 4 additions & 4 deletions packages/language/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ attribute @@@deprecated(_ message: String)
* @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations.
* @param condition: a boolean expression that controls if the operation should be allowed.
*/
attribute @@allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean)
attribute @@allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'post-update'","'delete'", "'all'"]), _ condition: Boolean)

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

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

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

6 changes: 5 additions & 1 deletion packages/language/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ export async function loadDocument(

// build the document together with standard library, plugin modules, and imported documents
await services.shared.workspace.DocumentBuilder.build([stdLib, ...pluginDocs, document, ...importedDocuments], {
validation: true,
validation: {
stopAfterLexingErrors: true,
stopAfterParsingErrors: true,
stopAfterLinkingErrors: true,
},
});

const diagnostics = langiumDocuments.all
Expand Down
8 changes: 2 additions & 6 deletions packages/language/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,6 @@ export function isRelationshipField(field: DataField) {
return isDataModel(field.type.reference?.ref);
}

export function isFutureExpr(node: AstNode) {
return isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref);
}

export function isDelegateModel(node: AstNode) {
return isDataModel(node) && hasAttribute(node, '@@delegate');
}
Expand Down Expand Up @@ -450,8 +446,8 @@ export function getAuthDecl(decls: (DataModel | TypeDef)[]) {
return authModel;
}

export function isFutureInvocation(node: AstNode) {
return isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref);
export function isBeforeInvocation(node: AstNode) {
return isInvocationExpr(node) && node.function.ref?.name === 'before' && isFromStdlib(node.function.ref);
}

export function isCollectionPredicate(node: AstNode): node is BinaryExpr {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ import {
getAllAttributes,
getStringLiteral,
isAuthOrAuthMemberAccess,
isBeforeInvocation,
isCollectionPredicate,
isDataFieldReference,
isDelegateModel,
isFutureExpr,
isRelationshipField,
mapBuiltinTypeToExpressionType,
resolved,
Expand Down Expand Up @@ -166,13 +166,20 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
});
return;
}
this.validatePolicyKinds(kind, ['create', 'read', 'update', 'delete', 'all'], attr, accept);
this.validatePolicyKinds(kind, ['create', 'read', 'update', 'post-update', 'delete', 'all'], attr, accept);

if ((kind === 'create' || kind === 'all') && attr.args[1]?.value) {
// "create" rules cannot access non-owned relations because the entity does not exist yet, so
// there can't possibly be a fk that points to it
this.rejectNonOwnedRelationInExpression(attr.args[1].value, accept);
}

if (kind !== 'post-update' && attr.args[1]?.value) {
const beforeCall = AstUtils.streamAst(attr.args[1]?.value).find(isBeforeInvocation);
if (beforeCall) {
accept('error', `"before()" is only allowed in "post-update" policy rules`, { node: beforeCall });
}
}
}

private rejectNonOwnedRelationInExpression(expr: Expression, accept: ValidationAcceptor) {
Expand Down Expand Up @@ -251,8 +258,8 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
const kindItems = this.validatePolicyKinds(kind, ['read', 'update', 'all'], attr, accept);

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

// 'update' rules are not allowed for relation fields
Expand Down
11 changes: 11 additions & 0 deletions packages/language/src/validators/expression-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import {
isNullExpr,
isReferenceExpr,
isThisExpr,
MemberAccessExpr,
type ExpressionType,
} from '../generated/ast';

import {
findUpAst,
isAuthInvocation,
isAuthOrAuthMemberAccess,
isBeforeInvocation,
isDataFieldReference,
isEnumFieldReference,
typeAssignable,
Expand Down Expand Up @@ -59,12 +61,21 @@ export default class ExpressionValidator implements AstValidator<Expression> {

// extra validations by expression type
switch (expr.$type) {
case 'MemberAccessExpr':
this.validateMemberAccessExpr(expr, accept);
break;
case 'BinaryExpr':
this.validateBinaryExpr(expr, accept);
break;
}
}

private validateMemberAccessExpr(expr: MemberAccessExpr, accept: ValidationAcceptor) {
if (isBeforeInvocation(expr.operand) && isDataModel(expr.$resolvedType?.decl)) {
accept('error', 'relation fields cannot be accessed from `before()`', { node: expr });
}
}

private validateBinaryExpr(expr: BinaryExpr, accept: ValidationAcceptor) {
switch (expr.operator) {
case 'in': {
Expand Down
40 changes: 22 additions & 18 deletions packages/language/src/zmodel-document-builder.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import { DefaultDocumentBuilder, type BuildOptions, type LangiumDocument } from 'langium';
import { DefaultDocumentBuilder, type LangiumSharedCoreServices } from 'langium';

export class ZModelDocumentBuilder extends DefaultDocumentBuilder {
override buildDocuments(documents: LangiumDocument[], options: BuildOptions, cancelToken: any): Promise<void> {
return super.buildDocuments(
documents,
{
...options,
validation:
// force overriding validation options
options.validation === false || options.validation === undefined
? options.validation
: {
stopAfterLexingErrors: true,
stopAfterParsingErrors: true,
stopAfterLinkingErrors: true,
},
},
cancelToken,
);
constructor(services: LangiumSharedCoreServices) {
super(services);

// override update build options to skip validation when there are
// errors in the previous stages
let validationOptions = this.updateBuildOptions.validation;
const stopFlags = {
stopAfterLinkingErrors: true,
stopAfterLexingErrors: true,
stopAfterParsingErrors: true,
};
if (validationOptions === true) {
validationOptions = stopFlags;
} else if (typeof validationOptions === 'object') {
validationOptions = { ...validationOptions, ...stopFlags };
}

this.updateBuildOptions = {
...this.updateBuildOptions,
validation: validationOptions,
};
}
}
48 changes: 21 additions & 27 deletions packages/language/src/zmodel-linker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
type LangiumDocument,
type LinkingError,
type Reference,
type ReferenceInfo,
interruptAndCheck,
isReference,
} from 'langium';
import { match } from 'ts-pattern';
import {
Expand Down Expand Up @@ -57,7 +57,7 @@ import {
getAuthDecl,
getContainingDataModel,
isAuthInvocation,
isFutureExpr,
isBeforeInvocation,
isMemberContainer,
mapBuiltinTypeToExpressionType,
} from './utils';
Expand Down Expand Up @@ -94,31 +94,29 @@ export class ZModelLinker extends DefaultLinker {
document.state = DocumentState.Linked;
}

private linkReference(
container: AstNode,
property: string,
document: LangiumDocument,
extraScopes: ScopeProvider[],
) {
if (this.resolveFromScopeProviders(container, property, document, extraScopes)) {
private linkReference(refInfo: ReferenceInfo, document: LangiumDocument, extraScopes: ScopeProvider[]) {
const defaultRef = refInfo.reference as DefaultReference;
if (defaultRef._ref) {
// already linked
return;
}

const reference: DefaultReference = (container as any)[property];
this.doLink({ reference, container, property }, document);
if (this.resolveFromScopeProviders(refInfo.reference, document, extraScopes)) {
// resolved from additional scope provider
return;
}
// default linking
this.doLink(refInfo, document);
}

//#endregion

//#region Expression type resolving

private resolveFromScopeProviders(
node: AstNode,
property: string,
reference: DefaultReference,
document: LangiumDocument,
providers: ScopeProvider[],
) {
const reference: DefaultReference = (node as any)[property];
for (const provider of providers) {
const target = provider(reference.$refText);
if (target) {
Expand Down Expand Up @@ -276,7 +274,7 @@ export class ZModelLinker extends DefaultLinker {
}

private resolveInvocation(node: InvocationExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) {
this.linkReference(node, 'function', document, extraScopes);
this.linkReference({ reference: node.function, container: node, property: 'function' }, document, extraScopes);
node.args.forEach((arg) => this.resolve(arg, document, extraScopes));
if (node.function.ref) {
const funcDecl = node.function.ref as FunctionDecl;
Expand All @@ -294,8 +292,8 @@ export class ZModelLinker extends DefaultLinker {
if (authDecl) {
node.$resolvedType = { decl: authDecl, nullable: true };
}
} else if (isFutureExpr(node)) {
// future() function is resolved to current model
} else if (isBeforeInvocation(node)) {
// before() function is resolved to current model
node.$resolvedType = { decl: getContainingDataModel(node) };
} else {
this.resolveToDeclaredType(node, funcDecl.returnType);
Expand Down Expand Up @@ -401,7 +399,7 @@ export class ZModelLinker extends DefaultLinker {
if (isArrayExpr(node.value)) {
node.value.items.forEach((item) => {
if (isReferenceExpr(item)) {
const resolved = this.resolveFromScopeProviders(item, 'target', document, [scopeProvider]);
const resolved = this.resolveFromScopeProviders(item.target, document, [scopeProvider]);
if (resolved) {
this.resolveToDeclaredType(item, (resolved as DataField).type);
} else {
Expand All @@ -414,7 +412,7 @@ export class ZModelLinker extends DefaultLinker {
this.resolveToBuiltinTypeOrDecl(node.value, node.value.items[0].$resolvedType.decl, true);
}
} else if (isReferenceExpr(node.value)) {
const resolved = this.resolveFromScopeProviders(node.value, 'target', document, [scopeProvider]);
const resolved = this.resolveFromScopeProviders(node.value.target, document, [scopeProvider]);
if (resolved) {
this.resolveToDeclaredType(node.value, (resolved as DataField).type);
} else {
Expand Down Expand Up @@ -495,13 +493,9 @@ export class ZModelLinker extends DefaultLinker {
}

private resolveDefault(node: AstNode, document: LangiumDocument<AstNode>, extraScopes: ScopeProvider[]) {
for (const [property, value] of Object.entries(node)) {
if (!property.startsWith('$')) {
if (isReference(value)) {
this.linkReference(node, property, document, extraScopes);
}
}
}
AstUtils.streamReferences(node).forEach((ref) => {
this.linkReference(ref, document, extraScopes);
});
for (const child of AstUtils.streamContents(node)) {
this.resolve(child, document, extraScopes);
}
Expand Down
Loading