zenstack 0.5.0 → 0.6.0-pre.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.
- package/{LICENSE.md → LICENSE} +0 -0
- package/bin/cli +1 -1
- package/package.json +18 -13
- package/bin/post-install.js +0 -0
- package/bundle/asset/logo-256-bg.png +0 -0
- package/bundle/asset/logo-dark-256.png +0 -0
- package/bundle/asset/logo-light-256.png +0 -0
- package/bundle/cli/index.js +0 -6952
- package/bundle/extension.js +0 -39
- package/bundle/language-server/main.js +0 -6208
- package/bundle/res/package.template.json +0 -9
- package/bundle/res/prism-zmodel.js +0 -22
- package/bundle/res/stdlib.zmodel +0 -218
- package/bundle/res/tsconfig.template.json +0 -17
- package/src/cli/cli-error.ts +0 -4
- package/src/cli/cli-util.ts +0 -214
- package/src/cli/index.ts +0 -246
- package/src/extension.ts +0 -76
- package/src/generator/ast-utils.ts +0 -18
- package/src/generator/constants.ts +0 -6
- package/src/generator/field-constraint/index.ts +0 -304
- package/src/generator/index.ts +0 -86
- package/src/generator/prisma/expression-writer.ts +0 -360
- package/src/generator/prisma/index.ts +0 -44
- package/src/generator/prisma/prisma-builder.ts +0 -370
- package/src/generator/prisma/query-guard-generator.ts +0 -249
- package/src/generator/prisma/schema-generator.ts +0 -313
- package/src/generator/prisma/typescript-expression-transformer.ts +0 -108
- package/src/generator/react-hooks/index.ts +0 -273
- package/src/generator/service/index.ts +0 -113
- package/src/generator/tsc/index.ts +0 -59
- package/src/generator/types.ts +0 -20
- package/src/global.d.ts +0 -3
- package/src/language-server/constants.ts +0 -29
- package/src/language-server/generated/ast.ts +0 -643
- package/src/language-server/generated/grammar.ts +0 -2492
- package/src/language-server/generated/module.ts +0 -24
- package/src/language-server/langium-ext.d.ts +0 -22
- package/src/language-server/main.ts +0 -13
- package/src/language-server/types.ts +0 -25
- package/src/language-server/utils.ts +0 -21
- package/src/language-server/validator/attribute-validator.ts +0 -11
- package/src/language-server/validator/datamodel-validator.ts +0 -426
- package/src/language-server/validator/datasource-validator.ts +0 -102
- package/src/language-server/validator/enum-validator.ts +0 -14
- package/src/language-server/validator/expression-validator.ts +0 -48
- package/src/language-server/validator/schema-validator.ts +0 -31
- package/src/language-server/validator/utils.ts +0 -158
- package/src/language-server/validator/zmodel-validator.ts +0 -91
- package/src/language-server/zmodel-linker.ts +0 -453
- package/src/language-server/zmodel-module.ts +0 -131
- package/src/language-server/zmodel-scope.ts +0 -45
- package/src/language-server/zmodel-workspace-manager.ts +0 -23
- package/src/language-server/zmodel.langium +0 -207
- package/src/res/package.template.json +0 -9
- package/src/res/prism-zmodel.js +0 -22
- package/src/res/stdlib.zmodel +0 -218
- package/src/res/tsconfig.template.json +0 -17
- package/src/telemetry.ts +0 -119
- package/src/utils/exec-utils.ts +0 -8
- package/src/utils/indent-string.ts +0 -9
- package/src/utils/pkg-utils.ts +0 -63
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Attribute,
|
|
3
|
-
AttributeArg,
|
|
4
|
-
DataModel,
|
|
5
|
-
DataModelAttribute,
|
|
6
|
-
DataModelField,
|
|
7
|
-
DataModelFieldAttribute,
|
|
8
|
-
DataSource,
|
|
9
|
-
Enum,
|
|
10
|
-
Expression,
|
|
11
|
-
InvocationExpr,
|
|
12
|
-
isArrayExpr,
|
|
13
|
-
isInvocationExpr,
|
|
14
|
-
isLiteralExpr,
|
|
15
|
-
isReferenceExpr,
|
|
16
|
-
LiteralExpr,
|
|
17
|
-
} from '@lang/generated/ast';
|
|
18
|
-
import { writeFile } from 'fs/promises';
|
|
19
|
-
import { AstNode } from 'langium';
|
|
20
|
-
import path from 'path';
|
|
21
|
-
import { GUARD_FIELD_NAME, TRANSACTION_FIELD_NAME } from '../constants';
|
|
22
|
-
import { Context, GeneratorError } from '../types';
|
|
23
|
-
import { resolved } from '../ast-utils';
|
|
24
|
-
import {
|
|
25
|
-
AttributeArg as PrismaAttributeArg,
|
|
26
|
-
AttributeArgValue as PrismaAttributeArgValue,
|
|
27
|
-
DataSourceUrl as PrismaDataSourceUrl,
|
|
28
|
-
FieldAttribute as PrismaFieldAttribute,
|
|
29
|
-
ModelAttribute as PrismaModelAttribute,
|
|
30
|
-
Model as PrismaDataModel,
|
|
31
|
-
FieldReference as PrismaFieldReference,
|
|
32
|
-
FieldReferenceArg as PrismaFieldReferenceArg,
|
|
33
|
-
FunctionCall as PrismaFunctionCall,
|
|
34
|
-
FunctionCallArg as PrismaFunctionCallArg,
|
|
35
|
-
PrismaModel,
|
|
36
|
-
ModelFieldType,
|
|
37
|
-
} from './prisma-builder';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Generates Prisma schema file
|
|
41
|
-
*/
|
|
42
|
-
export default class PrismaSchemaGenerator {
|
|
43
|
-
constructor(private readonly context: Context) {}
|
|
44
|
-
|
|
45
|
-
async generate(): Promise<string> {
|
|
46
|
-
const { schema } = this.context;
|
|
47
|
-
const prisma = new PrismaModel();
|
|
48
|
-
|
|
49
|
-
for (const decl of schema.declarations) {
|
|
50
|
-
switch (decl.$type) {
|
|
51
|
-
case DataSource:
|
|
52
|
-
this.generateDataSource(prisma, decl as DataSource);
|
|
53
|
-
break;
|
|
54
|
-
|
|
55
|
-
case Enum:
|
|
56
|
-
this.generateEnum(prisma, decl as Enum);
|
|
57
|
-
break;
|
|
58
|
-
|
|
59
|
-
case DataModel:
|
|
60
|
-
this.generateModel(prisma, decl as DataModel);
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
this.generateGenerator(prisma);
|
|
66
|
-
|
|
67
|
-
const outFile = path.join(this.context.outDir, 'schema.prisma');
|
|
68
|
-
await writeFile(outFile, prisma.toString());
|
|
69
|
-
return outFile;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private generateDataSource(prisma: PrismaModel, dataSource: DataSource) {
|
|
73
|
-
let provider: string | undefined = undefined;
|
|
74
|
-
let url: PrismaDataSourceUrl | undefined = undefined;
|
|
75
|
-
let shadowDatabaseUrl: PrismaDataSourceUrl | undefined = undefined;
|
|
76
|
-
|
|
77
|
-
for (const f of dataSource.fields) {
|
|
78
|
-
switch (f.name) {
|
|
79
|
-
case 'provider': {
|
|
80
|
-
if (this.isStringLiteral(f.value)) {
|
|
81
|
-
provider = f.value.value as string;
|
|
82
|
-
} else {
|
|
83
|
-
throw new GeneratorError(
|
|
84
|
-
'Datasource provider must be set to a string'
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
case 'url': {
|
|
91
|
-
const r = this.extractDataSourceUrl(f.value);
|
|
92
|
-
if (!r) {
|
|
93
|
-
throw new GeneratorError(
|
|
94
|
-
'Invalid value for datasource url'
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
url = r;
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
case 'shadowDatabaseUrl': {
|
|
102
|
-
const r = this.extractDataSourceUrl(f.value);
|
|
103
|
-
if (!r) {
|
|
104
|
-
throw new GeneratorError(
|
|
105
|
-
'Invalid value for datasource url'
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
shadowDatabaseUrl = r;
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (!provider) {
|
|
115
|
-
throw new GeneratorError('Datasource is missing "provider" field');
|
|
116
|
-
}
|
|
117
|
-
if (!url) {
|
|
118
|
-
throw new GeneratorError('Datasource is missing "url" field');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
prisma.addDataSource(dataSource.name, provider, url, shadowDatabaseUrl);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
private extractDataSourceUrl(fieldValue: LiteralExpr | InvocationExpr) {
|
|
125
|
-
if (this.isStringLiteral(fieldValue)) {
|
|
126
|
-
return new PrismaDataSourceUrl(fieldValue.value as string, false);
|
|
127
|
-
} else if (
|
|
128
|
-
isInvocationExpr(fieldValue) &&
|
|
129
|
-
fieldValue.function.ref?.name === 'env' &&
|
|
130
|
-
fieldValue.args.length === 1 &&
|
|
131
|
-
this.isStringLiteral(fieldValue.args[0].value)
|
|
132
|
-
) {
|
|
133
|
-
return new PrismaDataSourceUrl(
|
|
134
|
-
fieldValue.args[0].value.value as string,
|
|
135
|
-
true
|
|
136
|
-
);
|
|
137
|
-
} else {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
private generateGenerator(prisma: PrismaModel) {
|
|
143
|
-
prisma.addGenerator(
|
|
144
|
-
'client',
|
|
145
|
-
'prisma-client-js',
|
|
146
|
-
path.join('..', this.context.generatedCodeDir, '.prisma'),
|
|
147
|
-
['fieldReference']
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
private generateModel(prisma: PrismaModel, decl: DataModel) {
|
|
152
|
-
const model = prisma.addModel(decl.name);
|
|
153
|
-
for (const field of decl.fields) {
|
|
154
|
-
this.generateModelField(model, field);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// add an "zenstack_guard" field for dealing with pure auth() related conditions
|
|
158
|
-
model.addField(GUARD_FIELD_NAME, 'Boolean', [
|
|
159
|
-
new PrismaFieldAttribute('@default', [
|
|
160
|
-
new PrismaAttributeArg(
|
|
161
|
-
undefined,
|
|
162
|
-
new PrismaAttributeArgValue('Boolean', true)
|
|
163
|
-
),
|
|
164
|
-
]),
|
|
165
|
-
]);
|
|
166
|
-
|
|
167
|
-
// add an "zenstack_transaction" field for tracking records created/updated with nested writes
|
|
168
|
-
model.addField(TRANSACTION_FIELD_NAME, 'String?');
|
|
169
|
-
|
|
170
|
-
// create an index for "zenstack_transaction" field
|
|
171
|
-
model.addAttribute('@@index', [
|
|
172
|
-
new PrismaAttributeArg(
|
|
173
|
-
undefined,
|
|
174
|
-
new PrismaAttributeArgValue('Array', [
|
|
175
|
-
new PrismaAttributeArgValue(
|
|
176
|
-
'FieldReference',
|
|
177
|
-
TRANSACTION_FIELD_NAME
|
|
178
|
-
),
|
|
179
|
-
])
|
|
180
|
-
),
|
|
181
|
-
]);
|
|
182
|
-
|
|
183
|
-
for (const attr of decl.attributes.filter(
|
|
184
|
-
(attr) => attr.decl.ref && this.isPrismaAttribute(attr.decl.ref)
|
|
185
|
-
)) {
|
|
186
|
-
this.generateModelAttribute(model, attr);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private isPrismaAttribute(attr: Attribute) {
|
|
191
|
-
return !!attr.attributes.find((a) => a.decl.ref?.name === '@@@prisma');
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
private generateModelField(model: PrismaDataModel, field: DataModelField) {
|
|
195
|
-
const fieldType = field.type.type || field.type.reference?.ref?.name;
|
|
196
|
-
if (!fieldType) {
|
|
197
|
-
throw new GeneratorError(
|
|
198
|
-
`Field type is not resolved: ${field.$container.name}.${field.name}`
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const type = new ModelFieldType(
|
|
203
|
-
fieldType,
|
|
204
|
-
field.type.array,
|
|
205
|
-
field.type.optional
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
const attributes = field.attributes
|
|
209
|
-
.filter(
|
|
210
|
-
(attr) => attr.decl.ref && this.isPrismaAttribute(attr.decl.ref)
|
|
211
|
-
)
|
|
212
|
-
.map((attr) => this.makeFieldAttribute(attr));
|
|
213
|
-
model.addField(field.name, type, attributes);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private makeFieldAttribute(attr: DataModelFieldAttribute) {
|
|
217
|
-
return new PrismaFieldAttribute(
|
|
218
|
-
resolved(attr.decl).name,
|
|
219
|
-
attr.args.map((arg) => this.makeAttributeArg(arg))
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
makeAttributeArg(arg: AttributeArg): PrismaAttributeArg {
|
|
224
|
-
return new PrismaAttributeArg(
|
|
225
|
-
arg.name,
|
|
226
|
-
this.makeAttributeArgValue(arg.value)
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
makeAttributeArgValue(node: Expression): PrismaAttributeArgValue {
|
|
231
|
-
if (isLiteralExpr(node)) {
|
|
232
|
-
switch (typeof node.value) {
|
|
233
|
-
case 'string':
|
|
234
|
-
return new PrismaAttributeArgValue('String', node.value);
|
|
235
|
-
case 'number':
|
|
236
|
-
return new PrismaAttributeArgValue('Number', node.value);
|
|
237
|
-
case 'boolean':
|
|
238
|
-
return new PrismaAttributeArgValue('Boolean', node.value);
|
|
239
|
-
default:
|
|
240
|
-
throw new GeneratorError(
|
|
241
|
-
`Unexpected literal type: ${typeof node.value}`
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
} else if (isArrayExpr(node)) {
|
|
245
|
-
return new PrismaAttributeArgValue(
|
|
246
|
-
'Array',
|
|
247
|
-
new Array(
|
|
248
|
-
...node.items.map((item) =>
|
|
249
|
-
this.makeAttributeArgValue(item)
|
|
250
|
-
)
|
|
251
|
-
)
|
|
252
|
-
);
|
|
253
|
-
} else if (isReferenceExpr(node)) {
|
|
254
|
-
return new PrismaAttributeArgValue(
|
|
255
|
-
'FieldReference',
|
|
256
|
-
new PrismaFieldReference(
|
|
257
|
-
resolved(node.target).name,
|
|
258
|
-
node.args.map(
|
|
259
|
-
(arg) =>
|
|
260
|
-
new PrismaFieldReferenceArg(arg.name, arg.value)
|
|
261
|
-
)
|
|
262
|
-
)
|
|
263
|
-
);
|
|
264
|
-
} else if (isInvocationExpr(node)) {
|
|
265
|
-
// invocation
|
|
266
|
-
return new PrismaAttributeArgValue(
|
|
267
|
-
'FunctionCall',
|
|
268
|
-
this.makeFunctionCall(node)
|
|
269
|
-
);
|
|
270
|
-
} else {
|
|
271
|
-
throw new GeneratorError(
|
|
272
|
-
`Unsupported attribute argument expression type: ${node.$type}`
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
makeFunctionCall(node: InvocationExpr): PrismaFunctionCall {
|
|
278
|
-
return new PrismaFunctionCall(
|
|
279
|
-
resolved(node.function).name,
|
|
280
|
-
node.args.map((arg) => {
|
|
281
|
-
if (!isLiteralExpr(arg.value)) {
|
|
282
|
-
throw new GeneratorError(
|
|
283
|
-
'Function call argument must be literal'
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
return new PrismaFunctionCallArg(arg.name, arg.value.value);
|
|
287
|
-
})
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private generateModelAttribute(
|
|
292
|
-
model: PrismaDataModel,
|
|
293
|
-
attr: DataModelAttribute
|
|
294
|
-
) {
|
|
295
|
-
model.attributes.push(
|
|
296
|
-
new PrismaModelAttribute(
|
|
297
|
-
resolved(attr.decl).name,
|
|
298
|
-
attr.args.map((arg) => this.makeAttributeArg(arg))
|
|
299
|
-
)
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
private generateEnum(prisma: PrismaModel, decl: Enum) {
|
|
304
|
-
prisma.addEnum(
|
|
305
|
-
decl.name,
|
|
306
|
-
decl.fields.map((f) => f.name)
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
private isStringLiteral(node: AstNode): node is LiteralExpr {
|
|
311
|
-
return isLiteralExpr(node) && typeof node.value === 'string';
|
|
312
|
-
}
|
|
313
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { GeneratorError } from '../types';
|
|
2
|
-
import {
|
|
3
|
-
ArrayExpr,
|
|
4
|
-
Expression,
|
|
5
|
-
InvocationExpr,
|
|
6
|
-
isEnumField,
|
|
7
|
-
isThisExpr,
|
|
8
|
-
LiteralExpr,
|
|
9
|
-
MemberAccessExpr,
|
|
10
|
-
NullExpr,
|
|
11
|
-
ReferenceExpr,
|
|
12
|
-
ThisExpr,
|
|
13
|
-
} from '@lang/generated/ast';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Transforms ZModel expression to plain TypeScript expression.
|
|
17
|
-
*/
|
|
18
|
-
export default class TypeScriptExpressionTransformer {
|
|
19
|
-
/**
|
|
20
|
-
*
|
|
21
|
-
* @param expr
|
|
22
|
-
* @returns
|
|
23
|
-
*/
|
|
24
|
-
transform(expr: Expression): string {
|
|
25
|
-
switch (expr.$type) {
|
|
26
|
-
case LiteralExpr:
|
|
27
|
-
return this.literal(expr as LiteralExpr);
|
|
28
|
-
|
|
29
|
-
case ArrayExpr:
|
|
30
|
-
return this.array(expr as ArrayExpr);
|
|
31
|
-
|
|
32
|
-
case NullExpr:
|
|
33
|
-
return this.null();
|
|
34
|
-
|
|
35
|
-
case ThisExpr:
|
|
36
|
-
return this.this(expr as ThisExpr);
|
|
37
|
-
|
|
38
|
-
case ReferenceExpr:
|
|
39
|
-
return this.reference(expr as ReferenceExpr);
|
|
40
|
-
|
|
41
|
-
case InvocationExpr:
|
|
42
|
-
return this.invocation(expr as InvocationExpr);
|
|
43
|
-
|
|
44
|
-
case MemberAccessExpr:
|
|
45
|
-
return this.memberAccess(expr as MemberAccessExpr);
|
|
46
|
-
|
|
47
|
-
default:
|
|
48
|
-
throw new GeneratorError(
|
|
49
|
-
`Unsupported expression type: ${expr.$type}`
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
55
|
-
private this(expr: ThisExpr) {
|
|
56
|
-
// "this" is mapped to id comparison
|
|
57
|
-
return 'id';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
private memberAccess(expr: MemberAccessExpr) {
|
|
61
|
-
if (!expr.member.ref) {
|
|
62
|
-
throw new GeneratorError(`Unresolved MemberAccessExpr`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (isThisExpr(expr.operand)) {
|
|
66
|
-
return expr.member.ref.name;
|
|
67
|
-
} else {
|
|
68
|
-
return `${this.transform(expr.operand)}?.${expr.member.ref.name}`;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private invocation(expr: InvocationExpr) {
|
|
73
|
-
if (expr.function.ref?.name !== 'auth') {
|
|
74
|
-
throw new GeneratorError(
|
|
75
|
-
`Function invocation is not supported: ${expr.function.ref?.name}`
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
return 'user';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private reference(expr: ReferenceExpr) {
|
|
82
|
-
if (!expr.target.ref) {
|
|
83
|
-
throw new GeneratorError(`Unresolved ReferenceExpr`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (isEnumField(expr.target.ref)) {
|
|
87
|
-
return `${expr.target.ref.$container.name}.${expr.target.ref.name}`;
|
|
88
|
-
} else {
|
|
89
|
-
return expr.target.ref.name;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private null() {
|
|
94
|
-
return 'null';
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private array(expr: ArrayExpr) {
|
|
98
|
-
return `[${expr.items.map((item) => this.transform(item)).join(', ')}]`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private literal(expr: LiteralExpr) {
|
|
102
|
-
if (typeof expr.value === 'string') {
|
|
103
|
-
return `'${expr.value}'`;
|
|
104
|
-
} else {
|
|
105
|
-
return expr.value.toString();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { DataModel, isDataModel } from '@lang/generated/ast';
|
|
2
|
-
import { paramCase } from 'change-case';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { Project } from 'ts-morph';
|
|
5
|
-
import { API_ROUTE_NAME, RUNTIME_PACKAGE } from '../constants';
|
|
6
|
-
import { Context, Generator } from '../types';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Generate react data query hooks code
|
|
10
|
-
*/
|
|
11
|
-
export default class ReactHooksGenerator implements Generator {
|
|
12
|
-
get name() {
|
|
13
|
-
return 'react-hooks';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
get startMessage() {
|
|
17
|
-
return 'Generating React hooks...';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
get successMessage(): string {
|
|
21
|
-
return 'Successfully generated React hooks';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async generate(context: Context) {
|
|
25
|
-
const project = new Project();
|
|
26
|
-
const models: DataModel[] = [];
|
|
27
|
-
const warnings: string[] = [];
|
|
28
|
-
|
|
29
|
-
for (const model of context.schema.declarations.filter(
|
|
30
|
-
(d): d is DataModel => isDataModel(d)
|
|
31
|
-
)) {
|
|
32
|
-
const hasAllowRule = model.attributes.find(
|
|
33
|
-
(attr) => attr.decl.ref?.name === '@@allow'
|
|
34
|
-
);
|
|
35
|
-
if (!hasAllowRule) {
|
|
36
|
-
warnings.push(
|
|
37
|
-
`Not generating hooks for "${model.name}" because it doesn't have any @@allow rule`
|
|
38
|
-
);
|
|
39
|
-
} else {
|
|
40
|
-
models.push(model);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
this.generateIndex(project, context, models);
|
|
45
|
-
|
|
46
|
-
models.forEach((d) => this.generateModelHooks(project, context, d));
|
|
47
|
-
|
|
48
|
-
await project.save();
|
|
49
|
-
return warnings;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private getValidator(model: DataModel, mode: 'create' | 'update') {
|
|
53
|
-
return `${model.name}_${mode}_validator`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private generateModelHooks(
|
|
57
|
-
project: Project,
|
|
58
|
-
context: Context,
|
|
59
|
-
model: DataModel
|
|
60
|
-
) {
|
|
61
|
-
const fileName = paramCase(model.name);
|
|
62
|
-
const sf = project.createSourceFile(
|
|
63
|
-
path.join(context.generatedCodeDir, `src/hooks/${fileName}.ts`),
|
|
64
|
-
undefined,
|
|
65
|
-
{ overwrite: true }
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
sf.addImportDeclaration({
|
|
69
|
-
namedImports: [{ name: 'Prisma', alias: 'P' }, model.name],
|
|
70
|
-
isTypeOnly: true,
|
|
71
|
-
moduleSpecifier: '../../.prisma',
|
|
72
|
-
});
|
|
73
|
-
sf.addStatements([
|
|
74
|
-
`import * as request from '${RUNTIME_PACKAGE}/lib/request';`,
|
|
75
|
-
`import { ServerErrorCode, RequestOptions } from '${RUNTIME_PACKAGE}/lib/types';`,
|
|
76
|
-
`import { validate } from '${RUNTIME_PACKAGE}/lib/validation';`,
|
|
77
|
-
`import { type SWRResponse } from 'swr';`,
|
|
78
|
-
`import { ${this.getValidator(
|
|
79
|
-
model,
|
|
80
|
-
'create'
|
|
81
|
-
)}, ${this.getValidator(
|
|
82
|
-
model,
|
|
83
|
-
'update'
|
|
84
|
-
)} } from '../field-constraint';`,
|
|
85
|
-
]);
|
|
86
|
-
|
|
87
|
-
sf.addStatements(
|
|
88
|
-
`const endpoint = '/api/${API_ROUTE_NAME}/data/${model.name}';`
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const useFuncBody = sf
|
|
92
|
-
.addFunction({
|
|
93
|
-
name: `use${model.name}`,
|
|
94
|
-
isExported: true,
|
|
95
|
-
})
|
|
96
|
-
.addBody();
|
|
97
|
-
|
|
98
|
-
useFuncBody.addStatements(['const mutate = request.getMutate();']);
|
|
99
|
-
|
|
100
|
-
// create
|
|
101
|
-
useFuncBody
|
|
102
|
-
.addFunction({
|
|
103
|
-
name: 'create',
|
|
104
|
-
isAsync: true,
|
|
105
|
-
typeParameters: [`T extends P.${model.name}CreateArgs`],
|
|
106
|
-
parameters: [
|
|
107
|
-
{ name: 'args', type: `P.${model.name}CreateArgs` },
|
|
108
|
-
],
|
|
109
|
-
})
|
|
110
|
-
.addBody()
|
|
111
|
-
.addStatements([
|
|
112
|
-
`
|
|
113
|
-
// validate field-level constraints
|
|
114
|
-
validate(${this.getValidator(model, 'create')}, args.data);
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
return await request.post<P.${
|
|
118
|
-
model.name
|
|
119
|
-
}CreateArgs, P.CheckSelect<T, ${model.name}, P.${
|
|
120
|
-
model.name
|
|
121
|
-
}GetPayload<T>>>(endpoint, args, mutate);
|
|
122
|
-
} catch (err: any) {
|
|
123
|
-
if (err.info?.code === ServerErrorCode.READ_BACK_AFTER_WRITE_DENIED) {
|
|
124
|
-
return undefined;
|
|
125
|
-
} else {
|
|
126
|
-
throw err;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
`,
|
|
130
|
-
]);
|
|
131
|
-
|
|
132
|
-
// find
|
|
133
|
-
useFuncBody
|
|
134
|
-
.addFunction({
|
|
135
|
-
name: 'find',
|
|
136
|
-
typeParameters: [`T extends P.${model.name}FindManyArgs`],
|
|
137
|
-
returnType: `SWRResponse<P.CheckSelect<T, ${model.name}[], P.${model.name}GetPayload<T>[]>, any>`,
|
|
138
|
-
parameters: [
|
|
139
|
-
{
|
|
140
|
-
name: 'args?',
|
|
141
|
-
type: `P.SelectSubset<T, P.${model.name}FindManyArgs>`,
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
name: 'options?',
|
|
145
|
-
type: `RequestOptions<P.CheckSelect<T, Array<${model.name}>, Array<P.${model.name}GetPayload<T>>>>`,
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
})
|
|
149
|
-
.addBody()
|
|
150
|
-
.addStatements([
|
|
151
|
-
`return request.get<P.CheckSelect<T, Array<${model.name}>, Array<P.${model.name}GetPayload<T>>>>(endpoint, args, options);`,
|
|
152
|
-
]);
|
|
153
|
-
|
|
154
|
-
// get
|
|
155
|
-
useFuncBody
|
|
156
|
-
.addFunction({
|
|
157
|
-
name: 'get',
|
|
158
|
-
typeParameters: [`T extends P.${model.name}FindFirstArgs`],
|
|
159
|
-
returnType: `SWRResponse<P.CheckSelect<T, ${model.name}, P.${model.name}GetPayload<T>>, any>`,
|
|
160
|
-
parameters: [
|
|
161
|
-
{
|
|
162
|
-
name: 'id',
|
|
163
|
-
type: 'String | undefined',
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
name: 'args?',
|
|
167
|
-
type: `P.SelectSubset<T, P.Subset<P.${model.name}FindFirstArgs, 'select' | 'include'>>`,
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
name: 'options?',
|
|
171
|
-
type: `RequestOptions<P.CheckSelect<T, ${model.name}, P.${model.name}GetPayload<T>>>`,
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
})
|
|
175
|
-
.addBody()
|
|
176
|
-
.addStatements([
|
|
177
|
-
`return request.get<P.CheckSelect<T, ${model.name}, P.${model.name}GetPayload<T>>>(id ? \`\${endpoint}/\${id}\`: null, args, options);`,
|
|
178
|
-
]);
|
|
179
|
-
|
|
180
|
-
// update
|
|
181
|
-
useFuncBody
|
|
182
|
-
.addFunction({
|
|
183
|
-
name: 'update',
|
|
184
|
-
isAsync: true,
|
|
185
|
-
typeParameters: [
|
|
186
|
-
`T extends Omit<P.${model.name}UpdateArgs, 'where'>`,
|
|
187
|
-
],
|
|
188
|
-
parameters: [
|
|
189
|
-
{ name: 'id', type: 'String' },
|
|
190
|
-
{
|
|
191
|
-
name: 'args',
|
|
192
|
-
type: `Omit<P.${model.name}UpdateArgs, 'where'>`,
|
|
193
|
-
},
|
|
194
|
-
],
|
|
195
|
-
})
|
|
196
|
-
.addBody()
|
|
197
|
-
.addStatements([
|
|
198
|
-
`
|
|
199
|
-
// validate field-level constraints
|
|
200
|
-
validate(${this.getValidator(model, 'update')}, args.data);
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
return await request.put<Omit<P.${
|
|
204
|
-
model.name
|
|
205
|
-
}UpdateArgs, 'where'>, P.CheckSelect<T, ${model.name}, P.${
|
|
206
|
-
model.name
|
|
207
|
-
}GetPayload<T>>>(\`\${endpoint}/\${id}\`, args, mutate);
|
|
208
|
-
} catch (err: any) {
|
|
209
|
-
if (err.info?.code === ServerErrorCode.READ_BACK_AFTER_WRITE_DENIED) {
|
|
210
|
-
return undefined;
|
|
211
|
-
} else {
|
|
212
|
-
throw err;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
`,
|
|
216
|
-
]);
|
|
217
|
-
|
|
218
|
-
// del
|
|
219
|
-
useFuncBody
|
|
220
|
-
.addFunction({
|
|
221
|
-
name: 'del',
|
|
222
|
-
isAsync: true,
|
|
223
|
-
typeParameters: [
|
|
224
|
-
`T extends Omit<P.${model.name}DeleteArgs, 'where'>`,
|
|
225
|
-
],
|
|
226
|
-
parameters: [
|
|
227
|
-
{ name: 'id', type: 'String' },
|
|
228
|
-
{
|
|
229
|
-
name: 'args?',
|
|
230
|
-
type: `Omit<P.${model.name}DeleteArgs, 'where'>`,
|
|
231
|
-
},
|
|
232
|
-
],
|
|
233
|
-
})
|
|
234
|
-
.addBody()
|
|
235
|
-
.addStatements([
|
|
236
|
-
`
|
|
237
|
-
try {
|
|
238
|
-
return await request.del<P.CheckSelect<T, ${model.name}, P.${model.name}GetPayload<T>>>(\`\${endpoint}/\${id}\`, args, mutate);
|
|
239
|
-
} catch (err: any) {
|
|
240
|
-
if (err.info?.code === ServerErrorCode.READ_BACK_AFTER_WRITE_DENIED) {
|
|
241
|
-
return undefined;
|
|
242
|
-
} else {
|
|
243
|
-
throw err;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
`,
|
|
247
|
-
]);
|
|
248
|
-
|
|
249
|
-
useFuncBody.addStatements([
|
|
250
|
-
'return { create, find, get, update, del };',
|
|
251
|
-
]);
|
|
252
|
-
|
|
253
|
-
sf.formatText();
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private generateIndex(
|
|
257
|
-
project: Project,
|
|
258
|
-
context: Context,
|
|
259
|
-
models: DataModel[]
|
|
260
|
-
) {
|
|
261
|
-
const sf = project.createSourceFile(
|
|
262
|
-
path.join(context.generatedCodeDir, 'src/hooks/index.ts'),
|
|
263
|
-
undefined,
|
|
264
|
-
{ overwrite: true }
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
sf.addStatements(
|
|
268
|
-
models.map((d) => `export * from './${paramCase(d.name)}';`)
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
sf.formatText();
|
|
272
|
-
}
|
|
273
|
-
}
|