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/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.18",
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": "^4.5.0",
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": "^4.5.0",
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 models = extractDataModelsWithAllowRules(context.schema);
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 { Context, Generator } from '../types';
2
- import { Project } from 'ts-morph';
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
- this.resolveToDeclaredType(node, funcDecl.returnType);
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
- // TODO: handle this during validation
359
- console.warn(`Unresolved collection predicate`);
370
+ // error is reported in validation pass
360
371
  }
361
372
  }
362
373