zenstack 0.1.0 → 0.1.2

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 +7 -4
  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,483 +0,0 @@
1
- import { Context, GeneratorError } from '../../types';
2
- import {
3
- CodeBlockWriter,
4
- OptionalKind,
5
- ParameterDeclarationStructure,
6
- Project,
7
- SourceFile,
8
- VariableDeclarationKind,
9
- } from 'ts-morph';
10
- import {
11
- DataModel,
12
- Expression,
13
- isInvocationExpr,
14
- isLiteralExpr,
15
- } from '@lang/generated/ast';
16
- import * as path from 'path';
17
- import { camelCase, paramCase } from 'change-case';
18
- import { extractDataModelsWithAllowRules } from '../../utils';
19
- import { ServerCodeGenerator } from '../server-code-generator';
20
- import ExpressionWriter from './expression-writer';
21
- import { streamAllContents } from 'langium';
22
- import colors from 'colors';
23
-
24
- type ServerOperation = 'get' | 'create' | 'find' | 'update' | 'del';
25
- type PolicyAction = 'create' | 'read' | 'update' | 'delete';
26
-
27
- export default class DataServerGenerator implements ServerCodeGenerator {
28
- generate(project: Project, context: Context): void {
29
- const models = extractDataModelsWithAllowRules(context.schema);
30
- this.generateIndex(models, project, context);
31
- this.generateUtils(project, context);
32
- models.forEach((model) =>
33
- this.generateForModel(model, project, context)
34
- );
35
-
36
- console.log(colors.blue(' ✔️ Server-side CRUD generated'));
37
- }
38
-
39
- //#region Index & Utils
40
-
41
- private generateIndex(
42
- models: DataModel[],
43
- project: Project,
44
- context: Context
45
- ) {
46
- const content = `
47
- import type { NextApiRequest, NextApiResponse } from 'next';
48
- import { RequestHandlerOptions } from '..';
49
- ${models.map((model) => this.writeModelImport(model)).join('\n')}
50
-
51
- export default async function (
52
- req: NextApiRequest,
53
- res: NextApiResponse,
54
- path: string[],
55
- options: RequestHandlerOptions
56
- ) {
57
- const [type, ...rest] = path;
58
- switch (type) {
59
- ${models
60
- .map((model) => this.writeModelEntrance(model))
61
- .join('\n')}
62
- default:
63
- res.status(404).json({ error: 'Unknown type: ' + type });
64
- }
65
- }
66
- `;
67
- const sf = project.createSourceFile(
68
- path.join(context.outDir, 'server/data/index.ts'),
69
- content,
70
- { overwrite: true }
71
- );
72
- sf.formatText();
73
- }
74
-
75
- private generateUtils(project: Project, context: Context) {
76
- const content = `
77
- import type { NextApiRequest, NextApiResponse } from 'next';
78
- import { RequestHandlerOptions } from '..';
79
-
80
- export async function getUser(
81
- req: NextApiRequest,
82
- res: NextApiResponse,
83
- options: RequestHandlerOptions
84
- ) {
85
- return await options.getServerUser(req, res);
86
- }
87
-
88
- export function unauthorized(res: NextApiResponse) {
89
- res.status(403).json({ message: 'Unauthorized' });
90
- }
91
-
92
- export function notFound(res: NextApiResponse) {
93
- res.status(404).json({ message: 'Entity not found' });
94
- }
95
- `;
96
- const sf = project.createSourceFile(
97
- path.join(context.outDir, 'server/data/_utils.ts'),
98
- content,
99
- { overwrite: true }
100
- );
101
- sf.formatText();
102
- }
103
-
104
- private writeModelImport(model: DataModel) {
105
- return `import ${camelCase(model.name)}Handler from './${paramCase(
106
- model.name
107
- )}';`;
108
- }
109
-
110
- private writeModelEntrance(model: DataModel) {
111
- return `
112
- case '${camelCase(model.name)}':
113
- return ${camelCase(model.name)}Handler(req, res, rest, options);
114
- `;
115
- }
116
-
117
- //#endregion
118
-
119
- //#region Per-Model
120
-
121
- private generateForModel(
122
- model: DataModel,
123
- project: Project,
124
- context: Context
125
- ) {
126
- const content = `
127
- import type { NextApiRequest, NextApiResponse } from 'next';
128
- import type { Prisma as P } from '@zenstack/.prisma';
129
- import { RequestHandlerOptions } from '..';
130
- import service from '@zenstack/service';
131
- import { getUser, notFound } from './_utils';
132
-
133
- export default async function (
134
- req: NextApiRequest,
135
- res: NextApiResponse,
136
- path: string[],
137
- options: RequestHandlerOptions
138
- ) {
139
- switch (req.method) {
140
- case 'GET':
141
- if (path.length > 0) {
142
- return get(req, res, path[0], options);
143
- } else {
144
- return find(req, res, options);
145
- }
146
-
147
- case 'POST':
148
- return create(req, res, options);
149
-
150
- case 'PUT':
151
- return update(req, res, path[0], options);
152
-
153
- case 'DELETE':
154
- return del(req, res, path[0], options);
155
-
156
- default:
157
- throw new Error('Unsupported HTTP method: ' + req.method);
158
- }
159
- }
160
- `;
161
- const sf = project.createSourceFile(
162
- path.join(
163
- context.outDir,
164
- `server/data/${paramCase(model.name)}.ts`
165
- ),
166
- content,
167
- { overwrite: true }
168
- );
169
-
170
- this.generateFind(sf, model);
171
- this.generateGet(sf, model);
172
- this.generateCreate(sf, model);
173
- this.generateUpdate(sf, model);
174
- this.generateDel(sf, model);
175
-
176
- sf.formatText();
177
- sf.saveSync();
178
- }
179
-
180
- private generateServeFunction(
181
- sourceFile: SourceFile,
182
- model: DataModel,
183
- operation: ServerOperation
184
- ) {
185
- const parameters: OptionalKind<ParameterDeclarationStructure>[] = [];
186
-
187
- parameters.push({
188
- name: 'req',
189
- type: 'NextApiRequest',
190
- });
191
-
192
- parameters.push({
193
- name: 'res',
194
- type: 'NextApiResponse',
195
- });
196
-
197
- if (
198
- operation === 'get' ||
199
- operation === 'update' ||
200
- operation === 'del'
201
- ) {
202
- // an extra "id" parameter
203
- parameters.push({
204
- name: 'id',
205
- type: 'string',
206
- });
207
- }
208
-
209
- parameters.push({
210
- name: 'options',
211
- type: 'RequestHandlerOptions',
212
- });
213
-
214
- const func = sourceFile
215
- .addFunction({
216
- name: operation,
217
- isAsync: true,
218
- parameters,
219
- })
220
- .addBody();
221
-
222
- if (this.modelUsesAuth(model)) {
223
- func.addStatements([
224
- `const user = await getUser(req, res, options);`,
225
- ]);
226
- }
227
- return func;
228
- }
229
-
230
- private modelUsesAuth(model: DataModel) {
231
- return !!streamAllContents(model).find(
232
- (node) =>
233
- isInvocationExpr(node) && node.function.ref?.name === 'auth'
234
- );
235
- }
236
-
237
- //#region Find & Get
238
-
239
- private generateFind(sourceFile: SourceFile, model: DataModel) {
240
- const func = this.generateServeFunction(sourceFile, model, 'find');
241
-
242
- func.addStatements([
243
- `const query: P.${model.name}FindManyArgs = req.query.q? (JSON.parse(req.query.q as string)): {};`,
244
- ]);
245
-
246
- func.addVariableStatement({
247
- declarationKind: VariableDeclarationKind.Const,
248
- declarations: [
249
- {
250
- name: 'args',
251
- type: `P.${model.name}FindManyArgs`,
252
- initializer: (writer) => {
253
- writer.block(() => {
254
- writer.writeLine('...query,');
255
- writer.write('where:');
256
- writer.block(() => {
257
- writer.write('AND: [');
258
- writer.write('{ ...query.where },');
259
- this.writeFindArgs(writer, model, 'read');
260
- writer.write(']');
261
- });
262
- });
263
- },
264
- },
265
- ],
266
- });
267
-
268
- func.addStatements([
269
- `res.status(200).send(await service.db.${camelCase(
270
- model.name
271
- )}.findMany(args));`,
272
- ]);
273
- }
274
-
275
- private generateGet(sourceFile: SourceFile, model: DataModel) {
276
- const func = this.generateServeFunction(sourceFile, model, 'get');
277
-
278
- func.addStatements([
279
- `const query: P.${model.name}FindFirstArgs = req.query.q? (JSON.parse(req.query.q as string)): {};`,
280
- ]);
281
-
282
- func.addVariableStatement({
283
- declarationKind: VariableDeclarationKind.Const,
284
- declarations: [
285
- {
286
- name: 'args',
287
- type: `P.${model.name}FindFirstArgs`,
288
- initializer: (writer) => {
289
- writer.block(() => {
290
- writer.writeLine('...query,');
291
- writer.write('where:');
292
- writer.block(() => {
293
- writer.write('AND: [');
294
- writer.write('{ id },');
295
- this.writeFindArgs(writer, model, 'read');
296
- writer.write(']');
297
- });
298
- });
299
- },
300
- },
301
- ],
302
- });
303
-
304
- func.addStatements([
305
- `
306
- const r = await service.db.${camelCase(model.name)}.findFirst(args);
307
- if (!r) {
308
- notFound(res);
309
- } else {
310
- res.status(200).send(r);
311
- }
312
- `,
313
- ]);
314
- }
315
-
316
- private writeFindArgs(
317
- writer: CodeBlockWriter,
318
- model: DataModel,
319
- action: PolicyAction
320
- ) {
321
- writer.block(() => {
322
- writer.writeLine('AND: [');
323
- this.writeDenyRules(writer, model, action);
324
- this.writeAllowRules(writer, model, action);
325
- writer.writeLine(']');
326
- });
327
- }
328
-
329
- //#endregion
330
-
331
- //#region Create
332
-
333
- private generateCreate(sourceFile: SourceFile, model: DataModel) {
334
- const func = this.generateServeFunction(sourceFile, model, 'create');
335
-
336
- func.addVariableStatement({
337
- declarationKind: VariableDeclarationKind.Const,
338
- declarations: [
339
- {
340
- name: 'args',
341
- type: `P.${model.name}CreateArgs`,
342
- initializer: 'req.body',
343
- },
344
- ],
345
- });
346
-
347
- // TODO: policy
348
-
349
- func.addStatements([
350
- `
351
- const r = await service.db.${camelCase(model.name)}.create(args);
352
- res.status(200).send(r);
353
- `,
354
- ]);
355
- }
356
-
357
- //#endregion
358
-
359
- //#region Update
360
-
361
- private generateUpdate(sourceFile: SourceFile, model: DataModel) {
362
- const func = this.generateServeFunction(sourceFile, model, 'update');
363
-
364
- func.addVariableStatement({
365
- declarationKind: VariableDeclarationKind.Const,
366
- declarations: [
367
- {
368
- name: 'body',
369
- type: `P.${model.name}UpdateArgs`,
370
- initializer: 'req.body',
371
- },
372
- ],
373
- });
374
-
375
- // TODO: policy
376
-
377
- func.addStatements([
378
- `
379
- const r = await service.db.${camelCase(model.name)}.update({
380
- ...body,
381
- where: { id }
382
- });
383
- res.status(200).send(r);
384
- `,
385
- ]);
386
- }
387
-
388
- //#endregion
389
-
390
- //#region Delete
391
-
392
- private generateDel(sourceFile: SourceFile, model: DataModel) {
393
- const func = this.generateServeFunction(sourceFile, model, 'del');
394
-
395
- func.addStatements([
396
- `const args: P.${model.name}DeleteArgs = req.query.q? (JSON.parse(req.query.q as string)): {};`,
397
- ]);
398
-
399
- // TODO: policy
400
-
401
- func.addStatements([
402
- `
403
- const r = await service.db.${camelCase(model.name)}.delete({
404
- ...args,
405
- where: { id }
406
- });
407
- res.status(200).send(r);
408
- `,
409
- ]);
410
- }
411
-
412
- //#endregion
413
-
414
- //#endregion
415
-
416
- //#region Policy
417
-
418
- private ruleSpecCovers(ruleSpec: Expression, action: string) {
419
- if (!isLiteralExpr(ruleSpec) || typeof ruleSpec.value !== 'string') {
420
- throw new GeneratorError(`Rule spec must be a string literal`);
421
- }
422
-
423
- const specs = ruleSpec.value.split(',').map((s) => s.trim());
424
- return specs.includes('all') || specs.includes(action);
425
- }
426
-
427
- private writeDenyRules(
428
- writer: CodeBlockWriter,
429
- model: DataModel,
430
- action: PolicyAction
431
- ) {
432
- const attrs = model.attributes.filter(
433
- (attr) =>
434
- attr.args.length > 0 &&
435
- attr.decl.ref?.name === 'deny' &&
436
- attr.args.length > 1 &&
437
- this.ruleSpecCovers(attr.args[0].value, action)
438
- );
439
- attrs.forEach((attr) =>
440
- this.writeDenyRule(writer, model, attr.args[1].value)
441
- );
442
- }
443
-
444
- private writeDenyRule(
445
- writer: CodeBlockWriter,
446
- model: DataModel,
447
- rule: Expression
448
- ) {
449
- writer.block(() => {
450
- writer.writeLine('NOT: ');
451
- new ExpressionWriter(writer).write(rule);
452
- });
453
- writer.write(',');
454
- }
455
-
456
- private writeAllowRules(
457
- writer: CodeBlockWriter,
458
- model: DataModel,
459
- action: PolicyAction
460
- ) {
461
- const attrs = model.attributes.filter(
462
- (attr) =>
463
- attr.args.length > 0 &&
464
- attr.decl.ref?.name === 'allow' &&
465
- attr.args.length > 1 &&
466
- this.ruleSpecCovers(attr.args[0].value, action)
467
- );
468
- attrs.forEach((attr) =>
469
- this.writeAllowRule(writer, model, attr.args[1].value)
470
- );
471
- }
472
-
473
- private writeAllowRule(
474
- writer: CodeBlockWriter,
475
- model: DataModel,
476
- rule: Expression
477
- ) {
478
- new ExpressionWriter(writer).write(rule);
479
- writer.write(',');
480
- }
481
-
482
- //#endregion
483
- }
@@ -1,32 +0,0 @@
1
- import { Context } from '../../types';
2
- import { Project } from 'ts-morph';
3
- import * as path from 'path';
4
- import { ServerCodeGenerator } from '../server-code-generator';
5
-
6
- export default class FunctionServerGenerator implements ServerCodeGenerator {
7
- generate(project: Project, context: Context) {
8
- this.generateIndex(project, context);
9
- }
10
-
11
- private generateIndex(project: Project, context: Context) {
12
- const content = `
13
- import type { NextApiRequest, NextApiResponse } from 'next';
14
- import { RequestHandlerOptions } from '..';
15
-
16
- export default async function (
17
- req: NextApiRequest,
18
- res: NextApiResponse,
19
- path: string[],
20
- options: RequestHandlerOptions
21
- ) {
22
- throw new Error('Not implemented');
23
- }
24
- `;
25
- const sf = project.createSourceFile(
26
- path.join(context.outDir, 'server/function/index.ts'),
27
- content,
28
- { overwrite: true }
29
- );
30
- sf.formatText();
31
- }
32
- }
@@ -1,57 +0,0 @@
1
- import { Project } from 'ts-morph';
2
- import { Context, Generator } from '../types';
3
- import * as path from 'path';
4
- import DataServerGenerator from './data/data-generator';
5
- import FunctionServerGenerator from './function/function-generator';
6
-
7
- export default class ServerGenerator implements Generator {
8
- async generate(context: Context) {
9
- const project = new Project();
10
-
11
- this.generateIndex(project, context);
12
-
13
- new DataServerGenerator().generate(project, context);
14
- new FunctionServerGenerator().generate(project, context);
15
-
16
- await project.save();
17
- }
18
-
19
- generateIndex(project: Project, context: Context) {
20
- const content = `
21
- import type { NextApiRequest, NextApiResponse } from 'next';
22
- import dataHandler from './data';
23
- import functionHandler from './function';
24
-
25
- export type AuthUser = { id: string } & Record<string, any>;
26
-
27
- export type RequestHandlerOptions = {
28
- getServerUser: (
29
- req: NextApiRequest,
30
- res: NextApiResponse
31
- ) => Promise<AuthUser | undefined>;
32
- };
33
-
34
- export function RequestHandler(options: RequestHandlerOptions) {
35
- return async (req: NextApiRequest, res: NextApiResponse) => {
36
- const [route, ...rest] = req.query.path as string[];
37
- switch (route) {
38
- case 'data':
39
- return dataHandler(req, res, rest, options);
40
-
41
- case 'function':
42
- return functionHandler(req, res, rest, options);
43
-
44
- default:
45
- res.status(404).json({ error: 'Unknown route: ' + route });
46
- }
47
- };
48
- }
49
- `;
50
- const sf = project.createSourceFile(
51
- path.join(context.outDir, 'server/index.ts'),
52
- content,
53
- { overwrite: true }
54
- );
55
- sf.formatText();
56
- }
57
- }
@@ -1,6 +0,0 @@
1
- import { Context } from '../types';
2
- import { Project } from 'ts-morph';
3
-
4
- export interface ServerCodeGenerator {
5
- generate(project: Project, context: Context): void;
6
- }