zenstack 0.1.42 → 1.0.0

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 (215) hide show
  1. package/.vscode/extensions.json +7 -0
  2. package/.vscode/launch.json +49 -0
  3. package/.vscode/settings.json +4 -0
  4. package/README.md +1 -0
  5. package/package.json +8 -90
  6. package/packages/internal/jest.config.ts +32 -0
  7. package/packages/internal/package.json +42 -0
  8. package/packages/internal/src/constants.ts +1 -0
  9. package/packages/internal/src/handler/data/guard-utils.ts +7 -0
  10. package/packages/internal/src/handler/data/handler.ts +415 -0
  11. package/packages/internal/src/handler/data/query-processor.ts +504 -0
  12. package/packages/internal/src/handler/index.ts +1 -0
  13. package/packages/internal/src/handler/types.ts +20 -0
  14. package/packages/internal/src/index.ts +3 -0
  15. package/packages/internal/src/request-handler.ts +27 -0
  16. package/packages/internal/src/request.ts +101 -0
  17. package/packages/internal/src/types.ts +40 -0
  18. package/packages/internal/tests/query-processor.test.ts +172 -0
  19. package/{out/cli/tsconfig.template.json → packages/internal/tsconfig.json} +7 -3
  20. package/packages/runtime/auth.d.ts +1 -0
  21. package/packages/runtime/auth.js +3 -0
  22. package/packages/runtime/hooks.d.ts +10 -0
  23. package/packages/runtime/hooks.js +3 -0
  24. package/packages/runtime/index.d.ts +3 -0
  25. package/packages/runtime/index.js +1 -0
  26. package/packages/runtime/package-lock.json +512 -0
  27. package/packages/runtime/package.json +16 -0
  28. package/packages/runtime/server.d.ts +1 -0
  29. package/packages/runtime/server.js +3 -0
  30. package/packages/runtime/types.d.ts +1 -0
  31. package/packages/runtime/types.js +3 -0
  32. package/packages/schema/.eslintrc.json +13 -0
  33. package/packages/schema/.vscodeignore +4 -0
  34. package/packages/schema/asset/logo-dark.png +0 -0
  35. package/packages/schema/asset/logo-light.png +0 -0
  36. package/{bin → packages/schema/bin}/cli +0 -0
  37. package/packages/schema/jest.config.ts +32 -0
  38. package/packages/schema/langium-config.json +14 -0
  39. package/packages/schema/langium-quickstart.md +41 -0
  40. package/packages/schema/language-configuration.json +30 -0
  41. package/packages/schema/package.json +96 -0
  42. package/packages/schema/src/cli/cli-util.ts +80 -0
  43. package/packages/schema/src/cli/index.ts +64 -0
  44. package/packages/schema/src/extension.ts +76 -0
  45. package/packages/schema/src/generator/constants.ts +5 -0
  46. package/packages/schema/src/generator/index.ts +92 -0
  47. package/{out/generator/next-auth/index.js → packages/schema/src/generator/next-auth/index.ts} +46 -58
  48. package/{out → packages/schema/src}/generator/package.template.json +0 -0
  49. package/packages/schema/src/generator/prisma/expression-writer.ts +352 -0
  50. package/packages/schema/src/generator/prisma/index.ts +32 -0
  51. package/packages/schema/src/generator/prisma/plain-expression-builder.ts +91 -0
  52. package/packages/schema/src/generator/prisma/prisma-builder.ts +366 -0
  53. package/packages/schema/src/generator/prisma/query-gard-generator.ts +208 -0
  54. package/packages/schema/src/generator/prisma/schema-generator.ts +300 -0
  55. package/packages/schema/src/generator/react-hooks/index.ts +181 -0
  56. package/packages/schema/src/generator/service/index.ts +107 -0
  57. package/{out → packages/schema/src}/generator/tsconfig.template.json +0 -0
  58. package/packages/schema/src/generator/types.ts +17 -0
  59. package/packages/schema/src/generator/utils.ts +9 -0
  60. package/packages/schema/src/language-server/generated/ast.ts +603 -0
  61. package/{out/language-server/generated/grammar.js → packages/schema/src/language-server/generated/grammar.ts} +5 -8
  62. package/packages/schema/src/language-server/generated/module.ts +24 -0
  63. package/packages/schema/src/language-server/main.ts +12 -0
  64. package/{out → packages/schema/src}/language-server/stdlib.zmodel +0 -0
  65. package/packages/schema/src/language-server/types.ts +9 -0
  66. package/packages/schema/src/language-server/zmodel-index.ts +33 -0
  67. package/packages/schema/src/language-server/zmodel-linker.ts +409 -0
  68. package/packages/schema/src/language-server/zmodel-module.ts +90 -0
  69. package/packages/schema/src/language-server/zmodel-scope.ts +21 -0
  70. package/packages/schema/src/language-server/zmodel-validator.ts +35 -0
  71. package/packages/schema/src/language-server/zmodel.langium +186 -0
  72. package/packages/schema/src/utils/exec-utils.ts +5 -0
  73. package/packages/schema/src/utils/indent-string.ts +6 -0
  74. package/packages/schema/syntaxes/zmodel.json +57 -0
  75. package/packages/schema/syntaxes/zmodel.tmLanguage.json +57 -0
  76. package/packages/schema/tests/generator/expression-writer.test.ts +676 -0
  77. package/packages/schema/tests/generator/prisma-builder.test.ts +138 -0
  78. package/packages/schema/tests/schema/parser.test.ts +423 -0
  79. package/packages/schema/tests/schema/sample-todo.test.ts +14 -0
  80. package/packages/schema/tests/utils.ts +38 -0
  81. package/packages/schema/tsconfig.json +23 -0
  82. package/pnpm-workspace.yaml +3 -0
  83. package/samples/todo/.env +2 -0
  84. package/samples/todo/.eslintrc.json +3 -0
  85. package/samples/todo/.vscode/launch.json +11 -0
  86. package/samples/todo/README.md +34 -0
  87. package/samples/todo/components/AuthGuard.tsx +17 -0
  88. package/samples/todo/components/Avatar.tsx +22 -0
  89. package/samples/todo/components/BreadCrumb.tsx +44 -0
  90. package/samples/todo/components/ManageMembers.tsx +134 -0
  91. package/samples/todo/components/NavBar.tsx +57 -0
  92. package/samples/todo/components/SpaceMembers.tsx +76 -0
  93. package/samples/todo/components/Spaces.tsx +28 -0
  94. package/samples/todo/components/TimeInfo.tsx +17 -0
  95. package/samples/todo/components/Todo.tsx +72 -0
  96. package/samples/todo/components/TodoList.tsx +77 -0
  97. package/samples/todo/lib/context.ts +31 -0
  98. package/samples/todo/next.config.js +10 -0
  99. package/samples/todo/package-lock.json +7527 -0
  100. package/samples/todo/package.json +45 -0
  101. package/samples/todo/pages/_app.tsx +50 -0
  102. package/samples/todo/pages/api/auth/[...nextauth].ts +83 -0
  103. package/samples/todo/pages/api/zenstack/[...path].ts +16 -0
  104. package/samples/todo/pages/create-space.tsx +114 -0
  105. package/samples/todo/pages/index.tsx +32 -0
  106. package/samples/todo/pages/space/[slug]/[listId]/index.tsx +88 -0
  107. package/samples/todo/pages/space/[slug]/index.tsx +169 -0
  108. package/samples/todo/postcss.config.js +6 -0
  109. package/samples/todo/public/avatar.jpg +0 -0
  110. package/samples/todo/public/favicon.ico +0 -0
  111. package/samples/todo/public/logo.png +0 -0
  112. package/samples/todo/public/vercel.svg +4 -0
  113. package/samples/todo/styles/globals.css +7 -0
  114. package/samples/todo/tailwind.config.js +11 -0
  115. package/samples/todo/tsconfig.json +28 -0
  116. package/samples/todo/types/next-auth.d.ts +14 -0
  117. package/samples/todo/types/next.d.ts +16 -0
  118. package/samples/todo/zenstack/migrations/20221014084317_init/migration.sql +153 -0
  119. package/samples/todo/zenstack/migrations/20221020094651_upate_cli/migration.sql +23 -0
  120. package/samples/todo/zenstack/migrations/migration_lock.toml +3 -0
  121. package/samples/todo/zenstack/schema.prisma +126 -0
  122. package/samples/todo/zenstack/schema.zmodel +161 -0
  123. package/tests/integration/jest.config.ts +16 -0
  124. package/tests/integration/package-lock.json +1081 -0
  125. package/tests/integration/package.json +27 -0
  126. package/tests/integration/tests/operation-coverate.test.ts +563 -0
  127. package/tests/integration/tests/operations.zmodel +69 -0
  128. package/tests/integration/tests/todo-e2e.test.ts +577 -0
  129. package/tests/integration/tests/todo.zmodel +123 -0
  130. package/tests/integration/tests/tsconfig.template.json +10 -0
  131. package/tests/integration/tests/utils.ts +133 -0
  132. package/tests/integration/tsconfig.json +10 -0
  133. package/out/cli/cli-util.js +0 -64
  134. package/out/cli/cli-util.js.map +0 -1
  135. package/out/cli/generator.js +0 -1
  136. package/out/cli/generator.js.map +0 -1
  137. package/out/cli/index.js +0 -46
  138. package/out/cli/index.js.map +0 -1
  139. package/out/cli/package.template.json +0 -10
  140. package/out/extension.js +0 -81
  141. package/out/extension.js.map +0 -1
  142. package/out/generator/constants.js +0 -9
  143. package/out/generator/constants.js.map +0 -1
  144. package/out/generator/data-server/index.js +0 -1
  145. package/out/generator/data-server/index.js.map +0 -1
  146. package/out/generator/index.js +0 -98
  147. package/out/generator/index.js.map +0 -1
  148. package/out/generator/next-auth/index.js.map +0 -1
  149. package/out/generator/prisma/expression-writer.js +0 -287
  150. package/out/generator/prisma/expression-writer.js.map +0 -1
  151. package/out/generator/prisma/index.js +0 -38
  152. package/out/generator/prisma/index.js.map +0 -1
  153. package/out/generator/prisma/plain-expression-builder.js +0 -69
  154. package/out/generator/prisma/plain-expression-builder.js.map +0 -1
  155. package/out/generator/prisma/prisma-builder.js +0 -307
  156. package/out/generator/prisma/prisma-builder.js.map +0 -1
  157. package/out/generator/prisma/query-gard-generator.js +0 -159
  158. package/out/generator/prisma/query-gard-generator.js.map +0 -1
  159. package/out/generator/prisma/schema-generator.js +0 -201
  160. package/out/generator/prisma/schema-generator.js.map +0 -1
  161. package/out/generator/query-guard/index.js +0 -2
  162. package/out/generator/query-guard/index.js.map +0 -1
  163. package/out/generator/react-hooks/index.js +0 -179
  164. package/out/generator/react-hooks/index.js.map +0 -1
  165. package/out/generator/server/data/data-generator.js +0 -376
  166. package/out/generator/server/data/data-generator.js.map +0 -1
  167. package/out/generator/server/data/expression-writer.js +0 -287
  168. package/out/generator/server/data/expression-writer.js.map +0 -1
  169. package/out/generator/server/data/plain-expression-builder.js +0 -69
  170. package/out/generator/server/data/plain-expression-builder.js.map +0 -1
  171. package/out/generator/server/data-generator.js +0 -82
  172. package/out/generator/server/data-generator.js.map +0 -1
  173. package/out/generator/server/expression-writer.js +0 -1
  174. package/out/generator/server/expression-writer.js.map +0 -1
  175. package/out/generator/server/function/function-generator.js +0 -50
  176. package/out/generator/server/function/function-generator.js.map +0 -1
  177. package/out/generator/server/function-generator.js +0 -13
  178. package/out/generator/server/function-generator.js.map +0 -1
  179. package/out/generator/server/index.js +0 -88
  180. package/out/generator/server/index.js.map +0 -1
  181. package/out/generator/server/js-expression-builder.js +0 -1
  182. package/out/generator/server/js-expression-builder.js.map +0 -1
  183. package/out/generator/server/plain-expression-builder.js +0 -1
  184. package/out/generator/server/plain-expression-builder.js.map +0 -1
  185. package/out/generator/server/server-code-generator.js +0 -3
  186. package/out/generator/server/server-code-generator.js.map +0 -1
  187. package/out/generator/server/server-code-writer.js +0 -1
  188. package/out/generator/server/server-code-writer.js.map +0 -1
  189. package/out/generator/service/index.js +0 -133
  190. package/out/generator/service/index.js.map +0 -1
  191. package/out/generator/types.js +0 -10
  192. package/out/generator/types.js.map +0 -1
  193. package/out/generator/utils.js +0 -10
  194. package/out/generator/utils.js.map +0 -1
  195. package/out/language-server/generated/ast.js +0 -386
  196. package/out/language-server/generated/ast.js.map +0 -1
  197. package/out/language-server/generated/grammar.js.map +0 -1
  198. package/out/language-server/generated/module.js +0 -23
  199. package/out/language-server/generated/module.js.map +0 -1
  200. package/out/language-server/main.js +0 -12
  201. package/out/language-server/main.js.map +0 -1
  202. package/out/language-server/types.js +0 -3
  203. package/out/language-server/types.js.map +0 -1
  204. package/out/language-server/zmodel-index.js +0 -38
  205. package/out/language-server/zmodel-index.js.map +0 -1
  206. package/out/language-server/zmodel-linker.js +0 -241
  207. package/out/language-server/zmodel-linker.js.map +0 -1
  208. package/out/language-server/zmodel-module.js +0 -51
  209. package/out/language-server/zmodel-module.js.map +0 -1
  210. package/out/language-server/zmodel-scope.js +0 -30
  211. package/out/language-server/zmodel-scope.js.map +0 -1
  212. package/out/language-server/zmodel-validator.js +0 -25
  213. package/out/language-server/zmodel-validator.js.map +0 -1
  214. package/out/utils/indent-string.js +0 -9
  215. package/out/utils/indent-string.js.map +0 -1
@@ -0,0 +1,366 @@
1
+ import indentString from '../../utils/indent-string';
2
+
3
+ export class PrismaModel {
4
+ private datasources: DataSource[] = [];
5
+ private generators: Generator[] = [];
6
+ private models: Model[] = [];
7
+ private enums: Enum[] = [];
8
+
9
+ addDataSource(
10
+ name: string,
11
+ provider: string,
12
+ url: DataSourceUrl,
13
+ shadowDatabaseUrl?: DataSourceUrl
14
+ ) {
15
+ const ds = new DataSource(name, provider, url, shadowDatabaseUrl);
16
+ this.datasources.push(ds);
17
+ return ds;
18
+ }
19
+
20
+ addGenerator(
21
+ name: string,
22
+ provider: string,
23
+ output: string,
24
+ previewFeatures?: string[]
25
+ ) {
26
+ const generator = new Generator(
27
+ name,
28
+ provider,
29
+ output,
30
+ previewFeatures
31
+ );
32
+ this.generators.push(generator);
33
+ return generator;
34
+ }
35
+
36
+ addModel(name: string) {
37
+ const model = new Model(name);
38
+ this.models.push(model);
39
+ return model;
40
+ }
41
+
42
+ addEnum(name: string, fields: string[]) {
43
+ const e = new Enum(name, fields);
44
+ this.enums.push(e);
45
+ return e;
46
+ }
47
+
48
+ toString() {
49
+ return [
50
+ ...this.datasources,
51
+ ...this.generators,
52
+ ...this.enums,
53
+ ...this.models,
54
+ ]
55
+ .map((d) => d.toString())
56
+ .join('\n\n');
57
+ }
58
+ }
59
+
60
+ export class DataSource {
61
+ constructor(
62
+ public name: string,
63
+ public provider: string,
64
+ public url: DataSourceUrl,
65
+ public shadowDatabaseUrl?: DataSourceUrl
66
+ ) {}
67
+
68
+ toString() {
69
+ return (
70
+ `datasource ${this.name} {\n` +
71
+ indentString(`provider="${this.provider}"\n`) +
72
+ indentString(`url=${this.url}\n`) +
73
+ (this.shadowDatabaseUrl
74
+ ? indentString(`shadowDatabaseurl=${this.shadowDatabaseUrl}\n`)
75
+ : '') +
76
+ `}`
77
+ );
78
+ }
79
+ }
80
+
81
+ export class DataSourceUrl {
82
+ constructor(public value: string, public isEnv: boolean) {}
83
+
84
+ toString() {
85
+ return this.isEnv ? `env("${this.value}")` : `"${this.value}"`;
86
+ }
87
+ }
88
+
89
+ export class Generator {
90
+ constructor(
91
+ public name: string,
92
+ public provider: string,
93
+ public output: string,
94
+ public previewFeatures?: string[]
95
+ ) {}
96
+
97
+ toString() {
98
+ return (
99
+ `generator ${this.name} {\n` +
100
+ indentString(`provider = "${this.provider}"\n`) +
101
+ indentString(`output = "${this.output}"\n`) +
102
+ (this.previewFeatures
103
+ ? indentString(
104
+ `previewFeatures = [${this.previewFeatures
105
+ ?.map((f) => '"' + f + '"')
106
+ .join(', ')}]\n`
107
+ )
108
+ : '') +
109
+ `}`
110
+ );
111
+ }
112
+ }
113
+
114
+ export class Model {
115
+ public fields: ModelField[] = [];
116
+ public attributes: ModelAttribute[] = [];
117
+ constructor(public name: string) {}
118
+
119
+ addField(
120
+ name: string,
121
+ type: ModelFieldType | string,
122
+ attributes: FieldAttribute[] = []
123
+ ) {
124
+ const field = new ModelField(name, type, attributes);
125
+ this.fields.push(field);
126
+ return field;
127
+ }
128
+
129
+ addAttribute(name: string, args: AttributeArg[] = []) {
130
+ const attr = new ModelAttribute(name, args);
131
+ this.attributes.push(attr);
132
+ return attr;
133
+ }
134
+
135
+ toString() {
136
+ return (
137
+ `model ${this.name} {\n` +
138
+ indentString(
139
+ [...this.fields, ...this.attributes]
140
+ .map((d) => d.toString())
141
+ .join('\n')
142
+ ) +
143
+ `\n}`
144
+ );
145
+ }
146
+ }
147
+
148
+ export type ScalarTypes =
149
+ | 'String'
150
+ | 'Boolean'
151
+ | 'Int'
152
+ | 'BigInt'
153
+ | 'Float'
154
+ | 'Decimal'
155
+ | 'DateTime'
156
+ | 'Json'
157
+ | 'Bytes'
158
+ | 'Unsupported';
159
+
160
+ export class ModelFieldType {
161
+ constructor(
162
+ public type: ScalarTypes | string,
163
+ public array?: boolean,
164
+ public optional?: boolean
165
+ ) {}
166
+
167
+ toString() {
168
+ return `${this.type}${this.array ? '[]' : ''}${
169
+ this.optional ? '?' : ''
170
+ }`;
171
+ }
172
+ }
173
+
174
+ export class ModelField {
175
+ constructor(
176
+ public name: string,
177
+ public type: ModelFieldType | string,
178
+ public attributes: FieldAttribute[] = []
179
+ ) {}
180
+
181
+ addAttribute(name: string, args: AttributeArg[] = []) {
182
+ const attr = new FieldAttribute(name, args);
183
+ this.attributes.push(attr);
184
+ return attr;
185
+ }
186
+
187
+ toString() {
188
+ return (
189
+ `${this.name} ${this.type}` +
190
+ (this.attributes.length > 0
191
+ ? ' ' + this.attributes.map((a) => a.toString()).join(' ')
192
+ : '')
193
+ );
194
+ }
195
+ }
196
+
197
+ export class FieldAttribute {
198
+ constructor(public name: string, public args: AttributeArg[] = []) {}
199
+
200
+ toString() {
201
+ return (
202
+ `@${this.name}(` +
203
+ this.args.map((a) => a.toString()).join(', ') +
204
+ `)`
205
+ );
206
+ }
207
+ }
208
+
209
+ export class ModelAttribute {
210
+ constructor(public name: string, public args: AttributeArg[] = []) {}
211
+
212
+ toString() {
213
+ return (
214
+ `@@${this.name}(` +
215
+ this.args.map((a) => a.toString()).join(', ') +
216
+ `)`
217
+ );
218
+ }
219
+ }
220
+
221
+ export class AttributeArg {
222
+ constructor(
223
+ public name: string | undefined,
224
+ public value: AttributeArgValue
225
+ ) {}
226
+
227
+ toString() {
228
+ return this.name
229
+ ? `${this.name}: ${this.value}`
230
+ : this.value.toString();
231
+ }
232
+ }
233
+
234
+ export class AttributeArgValue {
235
+ constructor(
236
+ public type:
237
+ | 'String'
238
+ | 'FieldReference'
239
+ | 'Number'
240
+ | 'Boolean'
241
+ | 'Array'
242
+ | 'FunctionCall',
243
+ public value:
244
+ | string
245
+ | number
246
+ | boolean
247
+ | FieldReference
248
+ | FunctionCall
249
+ | AttributeArgValue[]
250
+ ) {
251
+ switch (type) {
252
+ case 'String':
253
+ if (typeof value !== 'string')
254
+ throw new Error('Value must be string');
255
+ break;
256
+ case 'Number':
257
+ if (typeof value !== 'number')
258
+ throw new Error('Value must be number');
259
+ break;
260
+ case 'Boolean':
261
+ if (typeof value !== 'boolean')
262
+ throw new Error('Value must be boolean');
263
+ break;
264
+ case 'Array':
265
+ if (!Array.isArray(value))
266
+ throw new Error('Value must be array');
267
+ break;
268
+ case 'FieldReference':
269
+ if (
270
+ typeof value !== 'string' &&
271
+ !(value instanceof FieldReference)
272
+ )
273
+ throw new Error('Value must be string or FieldReference');
274
+ break;
275
+ case 'FunctionCall':
276
+ if (!(value instanceof FunctionCall))
277
+ throw new Error('Value must be FunctionCall');
278
+ break;
279
+ }
280
+ }
281
+
282
+ toString(): string {
283
+ switch (this.type) {
284
+ case 'String':
285
+ return `"${this.value}"`;
286
+ case 'Number':
287
+ return this.value.toString();
288
+ case 'FieldReference': {
289
+ if (typeof this.value === 'string') {
290
+ return this.value;
291
+ } else {
292
+ const fr = this.value as FieldReference;
293
+ let r = fr.field;
294
+ if (fr.args.length > 0) {
295
+ r +=
296
+ '(' +
297
+ fr.args.map((a) => a.toString()).join(',') +
298
+ ')';
299
+ }
300
+ return r;
301
+ }
302
+ }
303
+ case 'FunctionCall':
304
+ return this.value.toString();
305
+ case 'Boolean':
306
+ return this.value ? 'true' : 'false';
307
+ case 'Array':
308
+ return (
309
+ '[' +
310
+ (this.value as AttributeArgValue[])
311
+ .map((v) => v.toString())
312
+ .join(', ') +
313
+ ']'
314
+ );
315
+ default:
316
+ throw new Error(`Unknown attribute value type ${this.type}`);
317
+ }
318
+ }
319
+ }
320
+
321
+ export class FieldReference {
322
+ constructor(public field: string, public args: FieldReferenceArg[] = []) {}
323
+ }
324
+
325
+ export class FieldReferenceArg {
326
+ constructor(public name: 'sort', public value: 'Asc' | 'Desc') {}
327
+
328
+ toString() {
329
+ return `${this.name}: ${this.value}`;
330
+ }
331
+ }
332
+
333
+ export class FunctionCall {
334
+ constructor(public func: string, public args: FunctionCallArg[] = []) {}
335
+
336
+ toString() {
337
+ return (
338
+ `${this.func}` +
339
+ '(' +
340
+ this.args.map((a) => a.toString()).join(', ') +
341
+ ')'
342
+ );
343
+ }
344
+ }
345
+
346
+ export class FunctionCallArg {
347
+ constructor(public name: string | undefined, public value: any) {}
348
+
349
+ toString() {
350
+ return this.name ? `${this.name}: ${this.value}` : this.value;
351
+ }
352
+ }
353
+
354
+ export class Enum {
355
+ constructor(public name: string, public fields: EnumField[]) {}
356
+
357
+ toString() {
358
+ return (
359
+ `enum ${this.name} {\n` +
360
+ indentString(this.fields.join('\n')) +
361
+ '\n}'
362
+ );
363
+ }
364
+ }
365
+
366
+ type EnumField = String;
@@ -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/internal';
8
+ import path from 'path';
9
+ import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph';
10
+ import { GUARD_FIELD_NAME, INTERNAL_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.generatedCodeDir, 'src/query/guard.ts'),
21
+ undefined,
22
+ { overwrite: true }
23
+ );
24
+
25
+ sf.addImportDeclaration({
26
+ namedImports: [{ name: 'QueryContext' }],
27
+ moduleSpecifier: INTERNAL_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
+ }