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.
- package/out/cli/index.js +4 -51
- package/out/cli/index.js.map +1 -1
- package/out/cli/package.template.json +10 -0
- package/out/cli/tsconfig.template.json +17 -0
- package/out/generator/constants.js +6 -0
- package/out/generator/constants.js.map +1 -0
- package/out/generator/index.js +76 -0
- package/out/generator/index.js.map +1 -0
- package/out/generator/next-auth/index.js +3 -3
- package/out/generator/package.template.json +9 -0
- package/out/generator/prisma/expression-writer.js +287 -0
- package/out/generator/prisma/expression-writer.js.map +1 -0
- package/out/generator/prisma/index.js +8 -182
- package/out/generator/prisma/index.js.map +1 -1
- package/out/generator/prisma/plain-expression-builder.js +69 -0
- package/out/generator/prisma/plain-expression-builder.js.map +1 -0
- package/out/generator/prisma/prisma-builder.js +1 -1
- package/out/generator/prisma/prisma-builder.js.map +1 -1
- package/out/generator/prisma/query-gard-generator.js +159 -0
- package/out/generator/prisma/query-gard-generator.js.map +1 -0
- package/out/generator/prisma/schema-generator.js +202 -0
- package/out/generator/prisma/schema-generator.js.map +1 -0
- package/out/generator/query-guard/index.js +2 -0
- package/out/generator/query-guard/index.js.map +1 -0
- package/out/generator/react-hooks/index.js +1 -1
- package/out/generator/react-hooks/index.js.map +1 -1
- package/out/generator/server/data/expression-writer.js +42 -36
- package/out/generator/server/data/expression-writer.js.map +1 -1
- package/out/generator/server/data/plain-expression-builder.js +18 -2
- package/out/generator/server/data/plain-expression-builder.js.map +1 -1
- package/out/generator/service/index.js +51 -1
- package/out/generator/service/index.js.map +1 -1
- package/out/generator/tsconfig.template.json +17 -0
- package/out/utils/indent-string.js +3 -19
- package/out/utils/indent-string.js.map +1 -1
- package/package.json +7 -4
- package/src/cli/index.ts +5 -33
- package/src/generator/constants.ts +2 -0
- package/src/generator/index.ts +59 -0
- package/src/generator/next-auth/index.ts +3 -3
- package/src/generator/package.template.json +9 -0
- package/src/generator/{server/data → prisma}/expression-writer.ts +65 -63
- package/src/generator/prisma/index.ts +10 -309
- package/src/generator/{server/data → prisma}/plain-expression-builder.ts +22 -3
- package/src/generator/prisma/prisma-builder.ts +1 -1
- package/src/generator/prisma/query-gard-generator.ts +208 -0
- package/src/generator/prisma/schema-generator.ts +295 -0
- package/src/generator/react-hooks/index.ts +2 -4
- package/src/generator/service/index.ts +54 -1
- package/src/generator/tsconfig.template.json +17 -0
- package/src/utils/indent-string.ts +3 -38
- package/src/generator/server/data/data-generator.ts +0 -483
- package/src/generator/server/function/function-generator.ts +0 -32
- package/src/generator/server/index.ts +0 -57
- 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
|
-
|
|
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
|
-
|
|
52
|
-
const
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
77
|
-
await
|
|
78
|
-
console.log(colors.blue(` ✔️ Prisma schema generated`));
|
|
15
|
+
// generate prisma query guard
|
|
16
|
+
await new QueryGuardGenerator(context).generate();
|
|
79
17
|
|
|
80
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|
|
@@ -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
|
+
}
|