zenstack 0.1.0 → 0.1.1

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.
Files changed (55) hide show
  1. package/out/cli/index.js +4 -51
  2. package/out/cli/index.js.map +1 -1
  3. package/out/cli/package.template.json +10 -0
  4. package/out/cli/tsconfig.template.json +17 -0
  5. package/out/generator/constants.js +6 -0
  6. package/out/generator/constants.js.map +1 -0
  7. package/out/generator/index.js +76 -0
  8. package/out/generator/index.js.map +1 -0
  9. package/out/generator/next-auth/index.js +3 -3
  10. package/out/generator/package.template.json +9 -0
  11. package/out/generator/prisma/expression-writer.js +287 -0
  12. package/out/generator/prisma/expression-writer.js.map +1 -0
  13. package/out/generator/prisma/index.js +8 -182
  14. package/out/generator/prisma/index.js.map +1 -1
  15. package/out/generator/prisma/plain-expression-builder.js +69 -0
  16. package/out/generator/prisma/plain-expression-builder.js.map +1 -0
  17. package/out/generator/prisma/prisma-builder.js +1 -1
  18. package/out/generator/prisma/prisma-builder.js.map +1 -1
  19. package/out/generator/prisma/query-gard-generator.js +159 -0
  20. package/out/generator/prisma/query-gard-generator.js.map +1 -0
  21. package/out/generator/prisma/schema-generator.js +202 -0
  22. package/out/generator/prisma/schema-generator.js.map +1 -0
  23. package/out/generator/query-guard/index.js +2 -0
  24. package/out/generator/query-guard/index.js.map +1 -0
  25. package/out/generator/react-hooks/index.js +1 -1
  26. package/out/generator/react-hooks/index.js.map +1 -1
  27. package/out/generator/server/data/expression-writer.js +42 -36
  28. package/out/generator/server/data/expression-writer.js.map +1 -1
  29. package/out/generator/server/data/plain-expression-builder.js +18 -2
  30. package/out/generator/server/data/plain-expression-builder.js.map +1 -1
  31. package/out/generator/service/index.js +51 -1
  32. package/out/generator/service/index.js.map +1 -1
  33. package/out/generator/tsconfig.template.json +17 -0
  34. package/out/utils/indent-string.js +3 -19
  35. package/out/utils/indent-string.js.map +1 -1
  36. package/package.json +6 -3
  37. package/src/cli/index.ts +5 -33
  38. package/src/generator/constants.ts +2 -0
  39. package/src/generator/index.ts +59 -0
  40. package/src/generator/next-auth/index.ts +3 -3
  41. package/src/generator/package.template.json +9 -0
  42. package/src/generator/{server/data → prisma}/expression-writer.ts +65 -63
  43. package/src/generator/prisma/index.ts +10 -309
  44. package/src/generator/{server/data → prisma}/plain-expression-builder.ts +22 -3
  45. package/src/generator/prisma/prisma-builder.ts +1 -1
  46. package/src/generator/prisma/query-gard-generator.ts +208 -0
  47. package/src/generator/prisma/schema-generator.ts +295 -0
  48. package/src/generator/react-hooks/index.ts +2 -4
  49. package/src/generator/service/index.ts +54 -1
  50. package/src/generator/tsconfig.template.json +17 -0
  51. package/src/utils/indent-string.ts +3 -38
  52. package/src/generator/server/data/data-generator.ts +0 -483
  53. package/src/generator/server/function/function-generator.ts +0 -32
  54. package/src/generator/server/index.ts +0 -57
  55. package/src/generator/server/server-code-generator.ts +0 -6
@@ -1,323 +1,24 @@
1
- import { writeFile } from 'fs/promises';
2
- import { AstNode } from 'langium';
3
- import path from 'path';
4
1
  import colors from 'colors';
5
- import {
6
- AttributeArg,
7
- DataModel,
8
- DataModelAttribute,
9
- DataModelField,
10
- DataModelFieldAttribute,
11
- DataSource,
12
- Enum,
13
- Expression,
14
- InvocationExpr,
15
- isArrayExpr,
16
- isInvocationExpr,
17
- isLiteralExpr,
18
- isReferenceExpr,
19
- LiteralExpr,
20
- } from '@lang/generated/ast';
21
- import { Context, Generator, GeneratorError } from '../types';
22
- import {
23
- AttributeArg as PrismaAttributeArg,
24
- AttributeArgValue as PrismaAttributeArgValue,
25
- DataSourceUrl as PrismaDataSourceUrl,
26
- FieldAttribute as PrismaFieldAttribute,
27
- ModelAttribute as PrismaModelAttribute,
28
- Model as PrismaDataModel,
29
- FieldReference as PrismaFieldReference,
30
- FieldReferenceArg as PrismaFieldReferenceArg,
31
- FunctionCall as PrismaFunctionCall,
32
- FunctionCallArg as PrismaFunctionCallArg,
33
- PrismaModel,
34
- ModelFieldType,
35
- } from './prisma-builder';
2
+ import { Context, Generator } from '../types';
36
3
  import { execSync } from 'child_process';
37
-
38
- const supportedProviders = ['postgresql', 'mysql', 'sqlite', 'sqlserver'];
39
- const supportedAttrbutes = [
40
- 'id',
41
- 'index',
42
- 'relation',
43
- 'default',
44
- 'createdAt',
45
- 'updatedAt',
46
- 'unique',
47
- ];
4
+ import PrismaSchemaGenerator from './schema-generator';
5
+ import QueryGuardGenerator from './query-gard-generator';
48
6
 
49
7
  export default class PrismaGenerator implements Generator {
50
8
  async generate(context: Context) {
51
- const { schema } = context;
52
- const prisma = new PrismaModel();
53
-
54
- for (const decl of schema.declarations) {
55
- switch (decl.$type) {
56
- case DataSource:
57
- this.generateDataSource(
58
- context,
59
- prisma,
60
- decl as DataSource
61
- );
62
- break;
63
-
64
- case Enum:
65
- this.generateEnum(context, prisma, decl as Enum);
66
- break;
9
+ // generate prisma schema
10
+ const schemaFile = await new PrismaSchemaGenerator(context).generate();
67
11
 
68
- case DataModel:
69
- this.generateModel(context, prisma, decl as DataModel);
70
- break;
71
- }
72
- }
73
-
74
- this.generateGenerator(context, prisma);
12
+ // run prisma generate and install @prisma/client
13
+ await this.generatePrismaClient(schemaFile);
75
14
 
76
- const outFile = path.join(context.outDir, 'schema.prisma');
77
- await writeFile(outFile, prisma.toString());
78
- console.log(colors.blue(` ✔️ Prisma schema generated`));
15
+ // generate prisma query guard
16
+ await new QueryGuardGenerator(context).generate();
79
17
 
80
- // run prisma generate and install @prisma/client
81
- await this.generatePrismaClient(outFile);
18
+ console.log(colors.blue(` ✔️ Prisma schema and query code generated`));
82
19
  }
83
20
 
84
21
  async generatePrismaClient(schemaFile: string) {
85
- try {
86
- execSync('npx prisma');
87
- } catch (err) {
88
- execSync(`npm i prisma @prisma/client`);
89
- console.log(colors.blue(' ✔️ Prisma package installed'));
90
- }
91
-
92
22
  execSync(`npx prisma generate --schema "${schemaFile}"`);
93
- console.log(colors.blue(' ✔️ Prisma client generated'));
94
- }
95
-
96
- private isStringLiteral(node: AstNode): node is LiteralExpr {
97
- return isLiteralExpr(node) && typeof node.value === 'string';
98
- }
99
-
100
- private generateDataSource(
101
- context: Context,
102
- prisma: PrismaModel,
103
- dataSource: DataSource
104
- ) {
105
- let provider: string | undefined = undefined;
106
- let url: PrismaDataSourceUrl | undefined = undefined;
107
- let shadowDatabaseUrl: PrismaDataSourceUrl | undefined = undefined;
108
-
109
- for (const f of dataSource.fields) {
110
- switch (f.name) {
111
- case 'provider': {
112
- if (this.isStringLiteral(f.value)) {
113
- provider = f.value.value as string;
114
- } else {
115
- throw new GeneratorError(
116
- 'Datasource provider must be set to a string'
117
- );
118
- }
119
- if (!supportedProviders.includes(provider)) {
120
- throw new GeneratorError(
121
- `Provider ${provider} is not supported. Supported providers: ${supportedProviders.join(
122
- ', '
123
- )}`
124
- );
125
- }
126
- break;
127
- }
128
-
129
- case 'url': {
130
- const r = this.extractDataSourceUrl(f.value);
131
- if (!r) {
132
- throw new GeneratorError(
133
- 'Invalid value for datasource url'
134
- );
135
- }
136
- url = r;
137
- break;
138
- }
139
-
140
- case 'shadowDatabaseUrl': {
141
- const r = this.extractDataSourceUrl(f.value);
142
- if (!r) {
143
- throw new GeneratorError(
144
- 'Invalid value for datasource url'
145
- );
146
- }
147
- shadowDatabaseUrl = r;
148
- break;
149
- }
150
- }
151
- }
152
-
153
- if (!provider) {
154
- throw new GeneratorError('Datasource is missing "provider" field');
155
- }
156
- if (!url) {
157
- throw new GeneratorError('Datasource is missing "url" field');
158
- }
159
-
160
- prisma.addDataSource(dataSource.name, provider, url, shadowDatabaseUrl);
161
- }
162
-
163
- private extractDataSourceUrl(fieldValue: LiteralExpr | InvocationExpr) {
164
- if (this.isStringLiteral(fieldValue)) {
165
- return new PrismaDataSourceUrl(fieldValue.value as string, false);
166
- } else if (
167
- isInvocationExpr(fieldValue) &&
168
- fieldValue.function.ref?.name === 'env' &&
169
- fieldValue.args.length === 1 &&
170
- this.isStringLiteral(fieldValue.args[0].value)
171
- ) {
172
- return new PrismaDataSourceUrl(
173
- fieldValue.args[0].value.value as string,
174
- true
175
- );
176
- } else {
177
- return null;
178
- }
179
- }
180
-
181
- private generateGenerator(context: Context, prisma: PrismaModel) {
182
- prisma.addGenerator(
183
- 'client',
184
- 'prisma-client-js',
185
- path.join(context.outDir, '.prisma'),
186
- ['fieldReference']
187
- );
188
- }
189
-
190
- private generateModel(
191
- context: Context,
192
- prisma: PrismaModel,
193
- decl: DataModel
194
- ) {
195
- const model = prisma.addModel(decl.name);
196
- for (const field of decl.fields) {
197
- this.generateModelField(model, field);
198
- }
199
-
200
- // add an "zenstack_guard" field for dealing with pure auth() related conditions
201
- model.addField('zenstack_guard', 'Boolean', [
202
- new PrismaFieldAttribute('default', [
203
- new PrismaAttributeArg(
204
- undefined,
205
- new PrismaAttributeArgValue('Boolean', true)
206
- ),
207
- ]),
208
- ]);
209
-
210
- for (const attr of decl.attributes.filter((attr) =>
211
- supportedAttrbutes.includes(attr.decl.ref?.name!)
212
- )) {
213
- this.generateModelAttribute(model, attr);
214
- }
215
- }
216
-
217
- private generateModelField(model: PrismaDataModel, field: DataModelField) {
218
- const type = new ModelFieldType(
219
- (field.type.type || field.type.reference?.ref?.name)!,
220
- field.type.array,
221
- field.type.optional
222
- );
223
-
224
- const attributes = field.attributes
225
- .filter((attr) => supportedAttrbutes.includes(attr.decl.ref?.name!))
226
- .map((attr) => this.makeFieldAttribute(attr));
227
- model.addField(field.name, type, attributes);
228
- }
229
-
230
- private makeFieldAttribute(attr: DataModelFieldAttribute) {
231
- return new PrismaFieldAttribute(
232
- attr.decl.ref?.name!,
233
- attr.args.map((arg) => this.makeAttributeArg(arg))
234
- );
235
- }
236
-
237
- makeAttributeArg(arg: AttributeArg): PrismaAttributeArg {
238
- return new PrismaAttributeArg(
239
- arg.name,
240
- this.makeAttributeArgValue(arg.value)
241
- );
242
- }
243
-
244
- makeAttributeArgValue(node: Expression): PrismaAttributeArgValue {
245
- if (isLiteralExpr(node)) {
246
- switch (typeof node.value) {
247
- case 'string':
248
- return new PrismaAttributeArgValue('String', node.value);
249
- case 'number':
250
- return new PrismaAttributeArgValue('Number', node.value);
251
- case 'boolean':
252
- return new PrismaAttributeArgValue('Boolean', node.value);
253
- default:
254
- throw new GeneratorError(
255
- `Unexpected literal type: ${typeof node.value}`
256
- );
257
- }
258
- } else if (isArrayExpr(node)) {
259
- return new PrismaAttributeArgValue(
260
- 'Array',
261
- new Array(
262
- ...node.items.map((item) =>
263
- this.makeAttributeArgValue(item)
264
- )
265
- )
266
- );
267
- } else if (isReferenceExpr(node)) {
268
- return new PrismaAttributeArgValue(
269
- 'FieldReference',
270
- new PrismaFieldReference(
271
- node.target.ref?.name!,
272
- node.args.map(
273
- (arg) =>
274
- new PrismaFieldReferenceArg(arg.name, arg.value)
275
- )
276
- )
277
- );
278
- } else if (isInvocationExpr(node)) {
279
- // invocation
280
- return new PrismaAttributeArgValue(
281
- 'FunctionCall',
282
- this.makeFunctionCall(node)
283
- );
284
- } else {
285
- throw new GeneratorError(
286
- `Unsupported attribute argument expression type: ${node.$type}`
287
- );
288
- }
289
- }
290
-
291
- makeFunctionCall(node: InvocationExpr): PrismaFunctionCall {
292
- return new PrismaFunctionCall(
293
- node.function.ref?.name!,
294
- node.args.map((arg) => {
295
- if (!isLiteralExpr(arg.value)) {
296
- throw new GeneratorError(
297
- 'Function call argument must be literal'
298
- );
299
- }
300
- return new PrismaFunctionCallArg(arg.name, arg.value.value);
301
- })
302
- );
303
- }
304
-
305
- private generateModelAttribute(
306
- model: PrismaDataModel,
307
- attr: DataModelAttribute
308
- ) {
309
- model.attributes.push(
310
- new PrismaModelAttribute(
311
- attr.decl.ref?.name!,
312
- attr.args.map((arg) => this.makeAttributeArg(arg))
313
- )
314
- );
315
- }
316
-
317
- private generateEnum(context: Context, prisma: PrismaModel, decl: Enum) {
318
- prisma.addEnum(
319
- decl.name,
320
- decl.fields.map((f) => f.name)
321
- );
322
23
  }
323
24
  }
@@ -1,12 +1,15 @@
1
- import { GeneratorError } from '../../types';
1
+ import { GeneratorError } from '../types';
2
2
  import {
3
3
  ArrayExpr,
4
4
  Expression,
5
5
  InvocationExpr,
6
+ isEnumField,
7
+ isThisExpr,
6
8
  LiteralExpr,
7
9
  MemberAccessExpr,
8
10
  NullExpr,
9
11
  ReferenceExpr,
12
+ ThisExpr,
10
13
  } from '@lang/generated/ast';
11
14
 
12
15
  export default class PlainExpressionBuilder {
@@ -21,6 +24,9 @@ export default class PlainExpressionBuilder {
21
24
  case NullExpr:
22
25
  return this.null();
23
26
 
27
+ case ThisExpr:
28
+ return this.this(expr as ThisExpr);
29
+
24
30
  case ReferenceExpr:
25
31
  return this.reference(expr as ReferenceExpr);
26
32
 
@@ -37,8 +43,17 @@ export default class PlainExpressionBuilder {
37
43
  }
38
44
  }
39
45
 
46
+ private this(expr: ThisExpr) {
47
+ // "this" is mapped to id comparison
48
+ return 'id';
49
+ }
50
+
40
51
  private memberAccess(expr: MemberAccessExpr) {
41
- return `${this.build(expr.operand)}?.${expr.member.ref!.name}`;
52
+ if (isThisExpr(expr.operand)) {
53
+ return expr.member.ref!.name;
54
+ } else {
55
+ return `${this.build(expr.operand)}?.${expr.member.ref!.name}`;
56
+ }
42
57
  }
43
58
 
44
59
  private invocation(expr: InvocationExpr) {
@@ -51,7 +66,11 @@ export default class PlainExpressionBuilder {
51
66
  }
52
67
 
53
68
  private reference(expr: ReferenceExpr) {
54
- return expr.target.ref!.name;
69
+ if (isEnumField(expr.target.ref)) {
70
+ return `${expr.target.ref.$container.name}.${expr.target.ref.name}`;
71
+ } else {
72
+ return expr.target.ref!.name;
73
+ }
55
74
  }
56
75
 
57
76
  private null() {
@@ -103,7 +103,7 @@ export class Generator {
103
103
  ? indentString(
104
104
  `previewFeatures = [${this.previewFeatures
105
105
  ?.map((f) => '"' + f + '"')
106
- .join(',')}]\n`
106
+ .join(', ')}]\n`
107
107
  )
108
108
  : '') +
109
109
  `}`
@@ -0,0 +1,208 @@
1
+ import {
2
+ DataModel,
3
+ isDataModel,
4
+ isEnum,
5
+ isLiteralExpr,
6
+ } from '@lang/generated/ast';
7
+ import { PolicyKind, PolicyOperationKind } from '@zenstackhq/runtime';
8
+ import path from 'path';
9
+ import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph';
10
+ import { GUARD_FIELD_NAME, RUNTIME_PACKAGE } from '../constants';
11
+ import { Context } from '../types';
12
+ import ExpressionWriter from './expression-writer';
13
+
14
+ export default class QueryGuardGenerator {
15
+ constructor(private readonly context: Context) {}
16
+
17
+ async generate() {
18
+ const project = new Project();
19
+ const sf = project.createSourceFile(
20
+ path.join(this.context.outDir, 'query/guard.ts'),
21
+ undefined,
22
+ { overwrite: true }
23
+ );
24
+
25
+ sf.addImportDeclaration({
26
+ namedImports: [{ name: 'QueryContext' }],
27
+ moduleSpecifier: RUNTIME_PACKAGE,
28
+ isTypeOnly: true,
29
+ });
30
+
31
+ // import enums
32
+ for (const e of this.context.schema.declarations.filter((d) =>
33
+ isEnum(d)
34
+ )) {
35
+ sf.addImportDeclaration({
36
+ namedImports: [{ name: e.name }],
37
+ moduleSpecifier: '../.prisma',
38
+ });
39
+ }
40
+
41
+ const models = this.context.schema.declarations.filter((d) =>
42
+ isDataModel(d)
43
+ ) as DataModel[];
44
+
45
+ this.generateFieldMapping(models, sf);
46
+
47
+ models.forEach((model) => this.generateQueryGuardForModel(model, sf));
48
+
49
+ sf.formatText({});
50
+ await project.save();
51
+ }
52
+
53
+ private generateFieldMapping(models: DataModel[], sourceFile: SourceFile) {
54
+ const mapping = Object.fromEntries(
55
+ models.map((m) => [
56
+ m.name,
57
+ Object.fromEntries(
58
+ m.fields
59
+ .filter((f) => isDataModel(f.type.reference?.ref))
60
+ .map((f) => [
61
+ f.name,
62
+ {
63
+ type: f.type.reference!.ref!.name,
64
+ isArray: f.type.array,
65
+ },
66
+ ])
67
+ ),
68
+ ])
69
+ );
70
+
71
+ sourceFile.addVariableStatement({
72
+ isExported: true,
73
+ declarationKind: VariableDeclarationKind.Const,
74
+ declarations: [
75
+ {
76
+ name: '_fieldMapping',
77
+ initializer: JSON.stringify(mapping),
78
+ },
79
+ ],
80
+ });
81
+ }
82
+
83
+ private getPolicyExpressions(
84
+ model: DataModel,
85
+ kind: PolicyKind,
86
+ operation: PolicyOperationKind
87
+ ) {
88
+ const attrs = model.attributes.filter(
89
+ (attr) => attr.decl.ref?.name === kind
90
+ );
91
+ return attrs
92
+ .filter((attr) => {
93
+ if (
94
+ !isLiteralExpr(attr.args[0].value) ||
95
+ typeof attr.args[0].value.value !== 'string'
96
+ ) {
97
+ return false;
98
+ }
99
+ const ops = attr.args[0].value.value
100
+ .split(',')
101
+ .map((s) => s.trim());
102
+ return ops.includes(operation) || ops.includes('all');
103
+ })
104
+ .map((attr) => attr.args[1].value);
105
+ }
106
+
107
+ private async generateQueryGuardForModel(
108
+ model: DataModel,
109
+ sourceFile: SourceFile
110
+ ) {
111
+ for (const kind of ['create', 'update', 'read', 'delete']) {
112
+ const func = sourceFile
113
+ .addFunction({
114
+ name: model.name + '_' + kind,
115
+ returnType: 'any',
116
+ parameters: [
117
+ {
118
+ name: 'context',
119
+ type: 'QueryContext',
120
+ },
121
+ ],
122
+ isExported: true,
123
+ })
124
+ .addBody();
125
+
126
+ func.addStatements('const { user } = context;');
127
+
128
+ // r = <guard object>;
129
+ func.addVariableStatement({
130
+ declarationKind: VariableDeclarationKind.Const,
131
+ declarations: [
132
+ {
133
+ name: 'r',
134
+ initializer: (writer) => {
135
+ const exprWriter = new ExpressionWriter(writer);
136
+ const denies = this.getPolicyExpressions(
137
+ model,
138
+ 'deny',
139
+ kind as PolicyOperationKind
140
+ );
141
+ const allows = this.getPolicyExpressions(
142
+ model,
143
+ 'allow',
144
+ kind as PolicyOperationKind
145
+ );
146
+
147
+ const writeDenies = () => {
148
+ writer.conditionalWrite(
149
+ denies.length > 1,
150
+ '{ AND: ['
151
+ );
152
+ denies.forEach((expr, i) => {
153
+ writer.block(() => {
154
+ writer.write('NOT: ');
155
+ exprWriter.write(expr);
156
+ });
157
+ writer.conditionalWrite(
158
+ i !== denies.length - 1,
159
+ ','
160
+ );
161
+ });
162
+ writer.conditionalWrite(
163
+ denies.length > 1,
164
+ ']}'
165
+ );
166
+ };
167
+
168
+ const writeAllows = () => {
169
+ writer.conditionalWrite(
170
+ allows.length > 1,
171
+ '{ OR: ['
172
+ );
173
+ allows.forEach((expr, i) => {
174
+ exprWriter.write(expr);
175
+ writer.conditionalWrite(
176
+ i !== allows.length - 1,
177
+ ','
178
+ );
179
+ });
180
+ writer.conditionalWrite(
181
+ allows.length > 1,
182
+ ']}'
183
+ );
184
+ };
185
+
186
+ if (allows.length > 0 && denies.length > 0) {
187
+ writer.writeLine('{ AND: [');
188
+ writeDenies();
189
+ writer.writeLine(',');
190
+ writeAllows();
191
+ writer.writeLine(']}');
192
+ } else if (denies.length > 0) {
193
+ writeDenies();
194
+ } else if (allows.length > 0) {
195
+ writeAllows();
196
+ } else {
197
+ // disallow any operation
198
+ writer.write(`{ ${GUARD_FIELD_NAME}: false }`);
199
+ }
200
+ },
201
+ },
202
+ ],
203
+ });
204
+
205
+ func.addStatements('return r;');
206
+ }
207
+ }
208
+ }