zenstack 0.3.18 → 0.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -246
- package/bundle/cli/index.js +180 -167
- package/bundle/language-server/main.js +67 -67
- package/package.json +5 -5
- package/src/generator/next-auth/index.ts +0 -5
- package/src/generator/react-hooks/index.ts +18 -3
- package/src/generator/service/index.ts +40 -4
- package/src/language-server/utils.ts +21 -0
- package/src/language-server/validator/datamodel-validator.ts +32 -0
- package/src/language-server/validator/expression-validator.ts +48 -0
- package/src/language-server/validator/zmodel-validator.ts +7 -0
- package/src/language-server/zmodel-linker.ts +14 -3
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publisher": "zenstack",
|
|
4
4
|
"displayName": "ZenStack Language Tools",
|
|
5
5
|
"description": "A toolkit for modeling data and access policies in full-stack development with Next.js and Typescript",
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.20",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "ZenStack Team"
|
|
9
9
|
},
|
|
@@ -65,7 +65,6 @@
|
|
|
65
65
|
},
|
|
66
66
|
"main": "./bundle/extension.js",
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@zenstackhq/runtime": "0.3.18",
|
|
69
68
|
"async-exit-hook": "^2.0.1",
|
|
70
69
|
"change-case": "^4.1.2",
|
|
71
70
|
"chevrotain": "^9.1.0",
|
|
@@ -76,7 +75,7 @@
|
|
|
76
75
|
"mixpanel": "^0.17.0",
|
|
77
76
|
"node-machine-id": "^1.1.12",
|
|
78
77
|
"pluralize": "^8.0.0",
|
|
79
|
-
"prisma": "
|
|
78
|
+
"prisma": "~4.5.0",
|
|
80
79
|
"promisify": "^0.0.3",
|
|
81
80
|
"sleep-promise": "^9.1.0",
|
|
82
81
|
"ts-morph": "^16.0.0",
|
|
@@ -85,10 +84,11 @@
|
|
|
85
84
|
"vscode-languageclient": "^8.0.2",
|
|
86
85
|
"vscode-languageserver": "^8.0.2",
|
|
87
86
|
"vscode-languageserver-textdocument": "^1.0.7",
|
|
88
|
-
"vscode-uri": "^3.0.6"
|
|
87
|
+
"vscode-uri": "^3.0.6",
|
|
88
|
+
"@zenstackhq/runtime": "0.3.20"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
|
-
"@prisma/internals": "
|
|
91
|
+
"@prisma/internals": "~4.5.0",
|
|
92
92
|
"@types/async-exit-hook": "^2.0.0",
|
|
93
93
|
"@types/jest": "^29.2.0",
|
|
94
94
|
"@types/node": "^14.18.32",
|
|
@@ -27,11 +27,6 @@ export default class NextAuthGenerator implements Generator {
|
|
|
27
27
|
try {
|
|
28
28
|
execSync('npm ls next-auth');
|
|
29
29
|
} catch (err) {
|
|
30
|
-
console.warn(
|
|
31
|
-
colors.yellow(
|
|
32
|
-
'Next-auth module is not installed, skipping generating adapter.'
|
|
33
|
-
)
|
|
34
|
-
);
|
|
35
30
|
return;
|
|
36
31
|
}
|
|
37
32
|
|
|
@@ -2,9 +2,8 @@ import { Context, Generator } from '../types';
|
|
|
2
2
|
import { Project } from 'ts-morph';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { paramCase } from 'change-case';
|
|
5
|
-
import { DataModel } from '@lang/generated/ast';
|
|
5
|
+
import { DataModel, isDataModel } from '@lang/generated/ast';
|
|
6
6
|
import colors from 'colors';
|
|
7
|
-
import { extractDataModelsWithAllowRules } from '../ast-utils';
|
|
8
7
|
import { API_ROUTE_NAME, RUNTIME_PACKAGE } from '../constants';
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -17,8 +16,24 @@ export default class ReactHooksGenerator implements Generator {
|
|
|
17
16
|
|
|
18
17
|
async generate(context: Context): Promise<void> {
|
|
19
18
|
const project = new Project();
|
|
19
|
+
const models: DataModel[] = [];
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
for (const model of context.schema.declarations.filter(
|
|
22
|
+
(d): d is DataModel => isDataModel(d)
|
|
23
|
+
)) {
|
|
24
|
+
const hasAllowRule = model.attributes.find(
|
|
25
|
+
(attr) => attr.decl.ref?.name === '@@allow'
|
|
26
|
+
);
|
|
27
|
+
if (!hasAllowRule) {
|
|
28
|
+
console.warn(
|
|
29
|
+
colors.yellow(
|
|
30
|
+
`Not generating hooks for "${model.name}" because it doesn't have any @@allow rule`
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
} else {
|
|
34
|
+
models.push(model);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
22
37
|
|
|
23
38
|
this.generateIndex(project, context, models);
|
|
24
39
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import * as path from 'path';
|
|
1
|
+
import { DataModel, isDataModel } from '@lang/generated/ast';
|
|
2
|
+
import { camelCase } from 'change-case';
|
|
4
3
|
import colors from 'colors';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { Project } from 'ts-morph';
|
|
5
6
|
import { RUNTIME_PACKAGE } from '../constants';
|
|
7
|
+
import { Context, Generator } from '../types';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Generates ZenStack service code
|
|
@@ -20,9 +22,18 @@ export default class ServiceGenerator implements Generator {
|
|
|
20
22
|
{ overwrite: true }
|
|
21
23
|
);
|
|
22
24
|
|
|
25
|
+
const models = context.schema.declarations.filter((d): d is DataModel =>
|
|
26
|
+
isDataModel(d)
|
|
27
|
+
);
|
|
28
|
+
|
|
23
29
|
sf.addStatements([
|
|
24
|
-
`import { PrismaClient } from "../.prisma";`,
|
|
30
|
+
`import { Prisma as P, PrismaClient } from "../.prisma";`,
|
|
25
31
|
`import { DefaultService } from "${RUNTIME_PACKAGE}/lib/service";`,
|
|
32
|
+
`import { CRUD } from "${RUNTIME_PACKAGE}/lib/handler/data/crud";`,
|
|
33
|
+
`import type { QueryContext } from "${RUNTIME_PACKAGE}/lib/types";`,
|
|
34
|
+
`import type { ${models
|
|
35
|
+
.map((m) => m.name)
|
|
36
|
+
.join(', ')} } from "../.prisma";`,
|
|
26
37
|
]);
|
|
27
38
|
|
|
28
39
|
const cls = sf.addClass({
|
|
@@ -31,6 +42,11 @@ export default class ServiceGenerator implements Generator {
|
|
|
31
42
|
extends: 'DefaultService<PrismaClient>',
|
|
32
43
|
});
|
|
33
44
|
|
|
45
|
+
cls.addProperty({
|
|
46
|
+
name: 'private crud',
|
|
47
|
+
initializer: `new CRUD<PrismaClient>(this)`,
|
|
48
|
+
});
|
|
49
|
+
|
|
34
50
|
cls.addMethod({
|
|
35
51
|
name: 'initializePrisma',
|
|
36
52
|
}).setBodyText(`
|
|
@@ -53,6 +69,26 @@ export default class ServiceGenerator implements Generator {
|
|
|
53
69
|
return import('./field-constraint');
|
|
54
70
|
`);
|
|
55
71
|
|
|
72
|
+
// server-side CRUD operations per model
|
|
73
|
+
for (const model of models) {
|
|
74
|
+
cls.addGetAccessor({
|
|
75
|
+
name: camelCase(model.name),
|
|
76
|
+
}).setBodyText(`
|
|
77
|
+
return {
|
|
78
|
+
get: <T extends P.${model.name}FindFirstArgs>(context: QueryContext, id: string, args?: P.SelectSubset<T, P.Subset<P.${model.name}FindFirstArgs, 'select' | 'include'>>) =>
|
|
79
|
+
this.crud.get('${model.name}', id, args, context) as Promise<P.CheckSelect<T, ${model.name}, P.${model.name}GetPayload<T>>>,
|
|
80
|
+
find: <T extends P.${model.name}FindManyArgs>(context: QueryContext, args?: P.SelectSubset<T, P.${model.name}FindManyArgs>) =>
|
|
81
|
+
this.crud.find('${model.name}', args, context) as Promise<P.CheckSelect<T, Array<${model.name}>, Array<P.${model.name}GetPayload<T>>>>,
|
|
82
|
+
create: <T extends P.${model.name}CreateArgs>(context: QueryContext, args: P.${model.name}CreateArgs) =>
|
|
83
|
+
this.crud.create('${model.name}', args, context) as Promise<P.CheckSelect<T, ${model.name}, P.${model.name}GetPayload<T>>>,
|
|
84
|
+
update: <T extends Omit<P.${model.name}UpdateArgs, 'where'>>(context: QueryContext, id: string, args: Omit<P.${model.name}UpdateArgs, 'where'>) =>
|
|
85
|
+
this.crud.update('${model.name}', id, args, context) as Promise<P.CheckSelect<T, ${model.name}, P.${model.name}GetPayload<T>>>,
|
|
86
|
+
del: <T extends Omit<P.${model.name}DeleteArgs, 'where'>>(context: QueryContext, id: string, args?: Omit<P.${model.name}DeleteArgs, 'where'>) =>
|
|
87
|
+
this.crud.del('${model.name}', id, args, context) as Promise<P.CheckSelect<T, ${model.name}, P.${model.name}GetPayload<T>>>,
|
|
88
|
+
}
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
|
|
56
92
|
// Recommended by Prisma for Next.js
|
|
57
93
|
// https://www.prisma.io/docs/guides/database/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices#problem
|
|
58
94
|
sf.addStatements([
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AstNode } from 'langium';
|
|
2
|
+
import { STD_LIB_MODULE_NAME } from './constants';
|
|
3
|
+
import { isModel, Model } from './generated/ast';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gets the toplevel Model containing the given node.
|
|
7
|
+
*/
|
|
8
|
+
export function getContainingModel(node: AstNode | undefined): Model | null {
|
|
9
|
+
if (!node) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return isModel(node) ? node : getContainingModel(node.$container);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns if the given node is declared in stdlib.
|
|
17
|
+
*/
|
|
18
|
+
export function isFromStdlib(node: AstNode) {
|
|
19
|
+
const model = getContainingModel(node);
|
|
20
|
+
return model && model.$document?.uri.path.endsWith(STD_LIB_MODULE_NAME);
|
|
21
|
+
}
|
|
@@ -390,5 +390,37 @@ export default class DataModelValidator implements AstValidator<DataModel> {
|
|
|
390
390
|
});
|
|
391
391
|
return;
|
|
392
392
|
}
|
|
393
|
+
|
|
394
|
+
if (relationOwner !== field && !relationOwner.type.array) {
|
|
395
|
+
// one-to-one relation requires defining side's reference field to be @unique
|
|
396
|
+
// e.g.:
|
|
397
|
+
// model User {
|
|
398
|
+
// id String @id @default(cuid())
|
|
399
|
+
// data UserData?
|
|
400
|
+
// }
|
|
401
|
+
// model UserData {
|
|
402
|
+
// id String @id @default(cuid())
|
|
403
|
+
// user User @relation(fields: [userId], references: [id])
|
|
404
|
+
// userId String
|
|
405
|
+
// }
|
|
406
|
+
//
|
|
407
|
+
// UserData.userId field needs to be @unique
|
|
408
|
+
|
|
409
|
+
thisRelation.fields?.forEach((ref) => {
|
|
410
|
+
const refField = ref.target.ref as DataModelField;
|
|
411
|
+
if (
|
|
412
|
+
refField &&
|
|
413
|
+
!refField.attributes.find(
|
|
414
|
+
(a) => a.decl.ref?.name === '@unique'
|
|
415
|
+
)
|
|
416
|
+
) {
|
|
417
|
+
accept(
|
|
418
|
+
'error',
|
|
419
|
+
`Field "${refField.name}" is part of a one-to-one relation and must be marked as @unique`,
|
|
420
|
+
{ node: refField }
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
393
425
|
}
|
|
394
426
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Expression,
|
|
3
|
+
isBinaryExpr,
|
|
4
|
+
isInvocationExpr,
|
|
5
|
+
} from '@lang/generated/ast';
|
|
6
|
+
import { AstValidator } from '@lang/types';
|
|
7
|
+
import { isFromStdlib } from '@lang/utils';
|
|
8
|
+
import { ValidationAcceptor } from 'langium';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validates expressions.
|
|
12
|
+
*/
|
|
13
|
+
export default class ExpressionValidator implements AstValidator<Expression> {
|
|
14
|
+
validate(expr: Expression, accept: ValidationAcceptor): void {
|
|
15
|
+
if (!expr.$resolvedType) {
|
|
16
|
+
if (this.isAuthInvocation(expr)) {
|
|
17
|
+
// check was done at link time
|
|
18
|
+
accept(
|
|
19
|
+
'error',
|
|
20
|
+
'auth() cannot be resolved because no "User" model is defined',
|
|
21
|
+
{ node: expr }
|
|
22
|
+
);
|
|
23
|
+
} else if (this.isCollectionPredicate(expr)) {
|
|
24
|
+
accept(
|
|
25
|
+
'error',
|
|
26
|
+
'collection predicate can only be used on an array of model type',
|
|
27
|
+
{ node: expr }
|
|
28
|
+
);
|
|
29
|
+
} else {
|
|
30
|
+
accept('error', 'expression cannot be resolved', {
|
|
31
|
+
node: expr,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private isCollectionPredicate(expr: Expression) {
|
|
38
|
+
return isBinaryExpr(expr) && ['?', '!', '^'].includes(expr.operator);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private isAuthInvocation(expr: Expression) {
|
|
42
|
+
return (
|
|
43
|
+
isInvocationExpr(expr) &&
|
|
44
|
+
expr.function.ref?.name === 'auth' &&
|
|
45
|
+
isFromStdlib(expr.function.ref)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
DataModel,
|
|
11
11
|
DataSource,
|
|
12
12
|
Enum,
|
|
13
|
+
Expression,
|
|
13
14
|
Model,
|
|
14
15
|
ZModelAstType,
|
|
15
16
|
} from '../generated/ast';
|
|
@@ -19,6 +20,7 @@ import DataSourceValidator from './datasource-validator';
|
|
|
19
20
|
import DataModelValidator from './datamodel-validator';
|
|
20
21
|
import AttributeValidator from './attribute-validator';
|
|
21
22
|
import EnumValidator from './enum-validator';
|
|
23
|
+
import ExpressionValidator from './expression-validator';
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Registry for validation checks.
|
|
@@ -33,6 +35,7 @@ export class ZModelValidationRegistry extends ValidationRegistry {
|
|
|
33
35
|
DataModel: validator.checkDataModel,
|
|
34
36
|
Enum: validator.checkEnum,
|
|
35
37
|
Attribute: validator.checkAttribute,
|
|
38
|
+
Expression: validator.checkExpression,
|
|
36
39
|
};
|
|
37
40
|
this.register(checks, validator);
|
|
38
41
|
}
|
|
@@ -81,4 +84,8 @@ export class ZModelValidator {
|
|
|
81
84
|
this.shouldCheck(node) &&
|
|
82
85
|
new AttributeValidator().validate(node, accept);
|
|
83
86
|
}
|
|
87
|
+
|
|
88
|
+
checkExpression(node: Expression, accept: ValidationAcceptor): void {
|
|
89
|
+
new ExpressionValidator().validate(node, accept);
|
|
90
|
+
}
|
|
84
91
|
}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
UnaryExpr,
|
|
36
36
|
} from './generated/ast';
|
|
37
37
|
import { ResolvedShape } from './types';
|
|
38
|
+
import { getContainingModel, isFromStdlib } from './utils';
|
|
38
39
|
import { mapBuiltinTypeToExpressionType } from './validator/utils';
|
|
39
40
|
|
|
40
41
|
interface DefaultReference extends Reference {
|
|
@@ -291,7 +292,18 @@ export class ZModelLinker extends DefaultLinker {
|
|
|
291
292
|
if (node.function.ref) {
|
|
292
293
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
293
294
|
const funcDecl = node.function.ref as Function;
|
|
294
|
-
|
|
295
|
+
if (funcDecl.name === 'auth' && isFromStdlib(funcDecl)) {
|
|
296
|
+
// auth() function is resolved to User model in the current document
|
|
297
|
+
const model = getContainingModel(node);
|
|
298
|
+
const userModel = model?.declarations.find(
|
|
299
|
+
(d) => isDataModel(d) && d.name === 'User'
|
|
300
|
+
);
|
|
301
|
+
if (userModel) {
|
|
302
|
+
node.$resolvedType = { decl: userModel };
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
this.resolveToDeclaredType(node, funcDecl.returnType);
|
|
306
|
+
}
|
|
295
307
|
}
|
|
296
308
|
}
|
|
297
309
|
|
|
@@ -355,8 +367,7 @@ export class ZModelLinker extends DefaultLinker {
|
|
|
355
367
|
this.resolve(node.right, document, extraScopes);
|
|
356
368
|
this.resolveToBuiltinTypeOrDecl(node, 'Boolean');
|
|
357
369
|
} else {
|
|
358
|
-
//
|
|
359
|
-
console.warn(`Unresolved collection predicate`);
|
|
370
|
+
// error is reported in validation pass
|
|
360
371
|
}
|
|
361
372
|
}
|
|
362
373
|
|