Skip to content

Commit 96315a9

Browse files
authored
Merge pull request #249 from zenstackhq/dev
merge dev to main (v3.0.0-beta.4)
2 parents 28ab57b + 966018f commit 96315a9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1925
-410
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
</a>
2222
</div>
2323

24-
> V3 is currently in alpha phase and not ready for production use. Feedback and bug reports are greatly appreciated. Please visit this dedicated [discord channel](https://discord.com/channels/1035538056146595961/1352359627525718056) for chat and support.
24+
> V3 is currently in beta phase and not ready for production use. Feedback and bug reports are greatly appreciated. Please visit this dedicated [discord channel](https://discord.com/channels/1035538056146595961/1352359627525718056) for chat and support.
2525
2626
# What's ZenStack
2727

TODO.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,10 @@
8383
- [x] Error system
8484
- [x] Custom table name
8585
- [x] Custom field name
86+
- [ ] Global omit
8687
- [ ] DbNull vs JsonNull
8788
- [ ] Migrate to tsdown
89+
- [ ] @default validation
8890
- [ ] Benchmark
8991
- [x] Plugin
9092
- [x] Post-mutation hooks should be called after transaction is committed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-v3",
3-
"version": "3.0.0-beta.3",
3+
"version": "3.0.0-beta.4",
44
"description": "ZenStack",
55
"packageManager": "[email protected]",
66
"scripts": {

packages/cli/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 CLI",
55
"description": "FullStack database toolkit with built-in access control and automatic API generation.",
6-
"version": "3.0.0-beta.3",
6+
"version": "3.0.0-beta.4",
77
"type": "module",
88
"author": {
99
"name": "ZenStack Team"

packages/common-helpers/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/common-helpers",
3-
"version": "3.0.0-beta.3",
3+
"version": "3.0.0-beta.4",
44
"description": "ZenStack Common Helpers",
55
"type": "module",
66
"scripts": {

packages/create-zenstack/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "create-zenstack",
3-
"version": "3.0.0-beta.3",
3+
"version": "3.0.0-beta.4",
44
"description": "Create a new ZenStack project",
55
"type": "module",
66
"scripts": {

packages/dialects/sql.js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/kysely-sql-js",
3-
"version": "3.0.0-beta.3",
3+
"version": "3.0.0-beta.4",
44
"description": "Kysely dialect for sql.js",
55
"type": "module",
66
"scripts": {

packages/eslint-config/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/eslint-config",
3-
"version": "3.0.0-beta.3",
3+
"version": "3.0.0-beta.4",
44
"type": "module",
55
"private": true,
66
"license": "MIT"

packages/language/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/language",
33
"description": "ZenStack ZModel language specification",
4-
"version": "3.0.0-beta.3",
4+
"version": "3.0.0-beta.4",
55
"license": "MIT",
66
"author": "ZenStack Team",
77
"files": [
@@ -59,13 +59,14 @@
5959
},
6060
"devDependencies": {
6161
"@types/pluralize": "^0.0.33",
62+
"@types/tmp": "catalog:",
63+
"@zenstackhq/common-helpers": "workspace:*",
6264
"@zenstackhq/eslint-config": "workspace:*",
6365
"@zenstackhq/typescript-config": "workspace:*",
64-
"@zenstackhq/common-helpers": "workspace:*",
6566
"@zenstackhq/vitest-config": "workspace:*",
67+
"glob": "^11.0.2",
6668
"langium-cli": "catalog:",
67-
"tmp": "catalog:",
68-
"@types/tmp": "catalog:"
69+
"tmp": "catalog:"
6970
},
7071
"volta": {
7172
"node": "18.19.1",

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

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
import { AstUtils, type ValidationAcceptor } from 'langium';
22
import pluralize from 'pluralize';
3+
import type { BinaryExpr, DataModel, Expression } from '../ast';
34
import {
45
ArrayExpr,
56
Attribute,
67
AttributeArg,
78
AttributeParam,
8-
DataModelAttribute,
99
DataField,
1010
DataFieldAttribute,
11+
DataModelAttribute,
1112
InternalAttribute,
1213
ReferenceExpr,
1314
isArrayExpr,
1415
isAttribute,
15-
isDataModel,
1616
isDataField,
17+
isDataModel,
1718
isEnum,
1819
isReferenceExpr,
1920
isTypeDef,
2021
} from '../generated/ast';
2122
import {
2223
getAllAttributes,
2324
getStringLiteral,
24-
hasAttribute,
25+
isAuthOrAuthMemberAccess,
26+
isCollectionPredicate,
2527
isDataFieldReference,
2628
isDelegateModel,
2729
isFutureExpr,
@@ -31,7 +33,6 @@ import {
3133
typeAssignable,
3234
} from '../utils';
3335
import type { AstValidator } from './common';
34-
import type { DataModel } from '../ast';
3536

3637
// a registry of function handlers marked with @check
3738
const attributeCheckers = new Map<string, PropertyDescriptor>();
@@ -153,6 +154,7 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
153154
}
154155
}
155156

157+
// TODO: design a way to let plugin register validation
156158
@check('@@allow')
157159
@check('@@deny')
158160
// @ts-expect-error
@@ -166,10 +168,75 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
166168
}
167169
this.validatePolicyKinds(kind, ['create', 'read', 'update', 'delete', 'all'], attr, accept);
168170

169-
// @encrypted fields cannot be used in policy rules
170-
this.rejectEncryptedFields(attr, accept);
171+
if ((kind === 'create' || kind === 'all') && attr.args[1]?.value) {
172+
// "create" rules cannot access non-owned relations because the entity does not exist yet, so
173+
// there can't possibly be a fk that points to it
174+
this.rejectNonOwnedRelationInExpression(attr.args[1].value, accept);
175+
}
171176
}
172177

178+
private rejectNonOwnedRelationInExpression(expr: Expression, accept: ValidationAcceptor) {
179+
const contextModel = AstUtils.getContainerOfType(expr, isDataModel);
180+
if (!contextModel) {
181+
return;
182+
}
183+
184+
if (
185+
AstUtils.streamAst(expr).some((node) => {
186+
if (!isDataFieldReference(node)) {
187+
// not a field reference, skip
188+
return false;
189+
}
190+
191+
// referenced field is not a member of the context model, skip
192+
if (node.target.ref?.$container !== contextModel) {
193+
return false;
194+
}
195+
196+
const field = node.target.ref as DataField;
197+
if (!isRelationshipField(field)) {
198+
// not a relation, skip
199+
return false;
200+
}
201+
202+
if (isAuthOrAuthMemberAccess(node)) {
203+
// field reference is from auth() or access from auth(), not a relation query
204+
return false;
205+
}
206+
207+
// check if the the node is a reference inside a collection predicate scope by auth access,
208+
// e.g., `auth().foo?[x > 0]`
209+
210+
// make sure to skip the current level if the node is already an LHS of a collection predicate,
211+
// otherwise we're just circling back to itself when visiting the parent
212+
const startNode =
213+
isCollectionPredicate(node.$container) && (node.$container as BinaryExpr).left === node
214+
? node.$container
215+
: node;
216+
const collectionPredicate = AstUtils.getContainerOfType(startNode.$container, isCollectionPredicate);
217+
if (collectionPredicate && isAuthOrAuthMemberAccess(collectionPredicate.left)) {
218+
return false;
219+
}
220+
221+
const relationAttr = field.attributes.find((attr) => attr.decl.ref?.name === '@relation');
222+
if (!relationAttr) {
223+
// no "@relation", not owner side of the relation, match
224+
return true;
225+
}
226+
227+
if (!relationAttr.args.some((arg) => arg.name === 'fields')) {
228+
// no "fields" argument, can't be owner side of the relation, match
229+
return true;
230+
}
231+
232+
return false;
233+
})
234+
) {
235+
accept('error', `non-owned relation fields are not allowed in "create" rules`, { node: expr });
236+
}
237+
}
238+
239+
// TODO: design a way to let plugin register validation
173240
@check('@allow')
174241
@check('@deny')
175242
// @ts-expect-error
@@ -199,9 +266,6 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
199266
);
200267
}
201268
}
202-
203-
// @encrypted fields cannot be used in policy rules
204-
this.rejectEncryptedFields(attr, accept);
205269
}
206270

207271
@check('@@validate')
@@ -261,14 +325,6 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
261325
}
262326
}
263327

264-
private rejectEncryptedFields(attr: AttributeApplication, accept: ValidationAcceptor) {
265-
AstUtils.streamAllContents(attr).forEach((node) => {
266-
if (isDataFieldReference(node) && hasAttribute(node.target.ref as DataField, '@encrypted')) {
267-
accept('error', `Encrypted fields cannot be used in policy rules`, { node });
268-
}
269-
});
270-
}
271-
272328
private validatePolicyKinds(
273329
kind: string,
274330
candidates: string[],

0 commit comments

Comments
 (0)