cloesce 0.0.5-unstable.0 → 0.0.5-unstable.10
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/dist/ast.d.ts +96 -106
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +12 -12
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +330 -368
- package/dist/common.d.ts +23 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/common.js +78 -0
- package/dist/extractor/err.d.ts +25 -26
- package/dist/extractor/err.d.ts.map +1 -1
- package/dist/extractor/err.js +95 -129
- package/dist/extractor/extract.d.ts +24 -61
- package/dist/extractor/extract.d.ts.map +1 -1
- package/dist/extractor/extract.js +1006 -836
- package/dist/generator.wasm +0 -0
- package/dist/orm.wasm +0 -0
- package/dist/router/crud.d.ts +2 -3
- package/dist/router/crud.d.ts.map +1 -1
- package/dist/router/crud.js +58 -48
- package/dist/router/orm.d.ts +66 -0
- package/dist/router/orm.d.ts.map +1 -0
- package/dist/router/orm.js +447 -0
- package/dist/router/router.d.ts +93 -139
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +389 -432
- package/dist/router/validator.d.ts +7 -12
- package/dist/router/validator.d.ts.map +1 -1
- package/dist/router/validator.js +190 -159
- package/dist/router/wasm.d.ts +26 -67
- package/dist/router/wasm.d.ts.map +1 -1
- package/dist/router/wasm.js +52 -103
- package/dist/ui/backend.d.ts +103 -382
- package/dist/ui/backend.d.ts.map +1 -1
- package/dist/ui/backend.js +143 -430
- package/package.json +4 -10
- package/dist/ui/client.d.ts +0 -7
- package/dist/ui/client.d.ts.map +0 -1
- package/dist/ui/client.js +0 -2
- package/dist/ui/common.d.ts +0 -120
- package/dist/ui/common.d.ts.map +0 -1
- package/dist/ui/common.js +0 -210
|
@@ -1,882 +1,1052 @@
|
|
|
1
|
-
import { Node as MorphNode, SyntaxKind, Scope } from "ts-morph";
|
|
2
|
-
import { HttpVerb, defaultMediaType } from "../ast.js";
|
|
1
|
+
import { Node as MorphNode, SyntaxKind, Scope, } from "ts-morph";
|
|
2
|
+
import { HttpVerb, defaultMediaType, } from "../ast.js";
|
|
3
3
|
import { TypeFormatFlags } from "typescript";
|
|
4
4
|
import { ExtractorError, ExtractorErrorCode } from "./err.js";
|
|
5
|
-
import { Either } from "../
|
|
6
|
-
var
|
|
7
|
-
(function (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
import { Either } from "../common.js";
|
|
6
|
+
var PropertyDecoratorKind;
|
|
7
|
+
(function (PropertyDecoratorKind) {
|
|
8
|
+
PropertyDecoratorKind["PrimaryKey"] = "PrimaryKey";
|
|
9
|
+
PropertyDecoratorKind["ForeignKey"] = "ForeignKey";
|
|
10
|
+
PropertyDecoratorKind["OneToOne"] = "OneToOne";
|
|
11
|
+
PropertyDecoratorKind["OneToMany"] = "OneToMany";
|
|
12
|
+
PropertyDecoratorKind["ManyToMany"] = "ManyToMany";
|
|
13
|
+
PropertyDecoratorKind["KeyParam"] = "KeyParam";
|
|
14
|
+
PropertyDecoratorKind["KV"] = "KV";
|
|
15
|
+
PropertyDecoratorKind["R2"] = "R2";
|
|
16
|
+
})(PropertyDecoratorKind || (PropertyDecoratorKind = {}));
|
|
15
17
|
var ClassDecoratorKind;
|
|
16
18
|
(function (ClassDecoratorKind) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
ClassDecoratorKind["Service"] = "Service";
|
|
21
|
-
ClassDecoratorKind["CRUD"] = "CRUD";
|
|
19
|
+
ClassDecoratorKind["Model"] = "Model";
|
|
20
|
+
ClassDecoratorKind["WranglerEnv"] = "WranglerEnv";
|
|
21
|
+
ClassDecoratorKind["Service"] = "Service";
|
|
22
22
|
})(ClassDecoratorKind || (ClassDecoratorKind = {}));
|
|
23
23
|
var ParameterDecoratorKind;
|
|
24
24
|
(function (ParameterDecoratorKind) {
|
|
25
|
-
|
|
25
|
+
ParameterDecoratorKind["Inject"] = "Inject";
|
|
26
26
|
})(ParameterDecoratorKind || (ParameterDecoratorKind = {}));
|
|
27
27
|
export class CidlExtractor {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
result.value.addContext((prev) => `${classDecl.getName()}.${prev}`);
|
|
86
|
-
return result;
|
|
87
|
-
}
|
|
88
|
-
poos[result.unwrap().name] = result.unwrap();
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (hasDecorator(classDecl, ClassDecoratorKind.WranglerEnv)) {
|
|
92
|
-
// Error: invalid attribute modifier
|
|
93
|
-
for (const prop of classDecl.getProperties()) {
|
|
94
|
-
const modifierRes = checkAttributeModifier(prop);
|
|
95
|
-
if (modifierRes.isLeft()) {
|
|
96
|
-
return modifierRes;
|
|
28
|
+
modelDecls;
|
|
29
|
+
extractedPoos;
|
|
30
|
+
constructor(modelDecls, extractedPoos = new Map()) {
|
|
31
|
+
this.modelDecls = modelDecls;
|
|
32
|
+
this.extractedPoos = extractedPoos;
|
|
33
|
+
}
|
|
34
|
+
static extract(projectName, project) {
|
|
35
|
+
const modelDecls = new Map();
|
|
36
|
+
const serviceDecls = new Map();
|
|
37
|
+
const wranglerEnvs = [];
|
|
38
|
+
let main_source = null;
|
|
39
|
+
// TODO: Concurrently across several threads?
|
|
40
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
41
|
+
// Extract main source
|
|
42
|
+
const mainRes = CidlExtractor.main(sourceFile);
|
|
43
|
+
if (mainRes.isLeft()) {
|
|
44
|
+
return mainRes;
|
|
45
|
+
}
|
|
46
|
+
const main = mainRes.unwrap();
|
|
47
|
+
if (main) {
|
|
48
|
+
main_source = main;
|
|
49
|
+
}
|
|
50
|
+
for (const classDecl of sourceFile.getClasses()) {
|
|
51
|
+
const notExportedErr = err(ExtractorErrorCode.MissingExport, (e) => {
|
|
52
|
+
e.context = classDecl.getName();
|
|
53
|
+
e.snippet = classDecl.getText();
|
|
54
|
+
});
|
|
55
|
+
for (const decorator of classDecl.getDecorators()) {
|
|
56
|
+
const decoratorName = decorator.getName();
|
|
57
|
+
switch (decoratorName) {
|
|
58
|
+
case ClassDecoratorKind.Model: {
|
|
59
|
+
if (!classDecl.isExported())
|
|
60
|
+
return notExportedErr;
|
|
61
|
+
modelDecls.set(classDecl.getName(), [classDecl, decorator]);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case ClassDecoratorKind.Service: {
|
|
65
|
+
if (!classDecl.isExported())
|
|
66
|
+
return notExportedErr;
|
|
67
|
+
serviceDecls.set(classDecl.getName(), classDecl);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case ClassDecoratorKind.WranglerEnv: {
|
|
71
|
+
const res = CidlExtractor.env(classDecl, sourceFile);
|
|
72
|
+
if (res.isLeft()) {
|
|
73
|
+
res.value.addContext((prev) => `${classDecl.getName()}.${prev}`);
|
|
74
|
+
return res;
|
|
75
|
+
}
|
|
76
|
+
const wranglerEnv = res.unwrap();
|
|
77
|
+
wranglerEnvs.push(wranglerEnv);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
default: {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
97
85
|
}
|
|
98
|
-
}
|
|
99
|
-
const result = CidlExtractor.env(classDecl, sourceFile);
|
|
100
|
-
if (result.isLeft()) {
|
|
101
|
-
return result;
|
|
102
|
-
}
|
|
103
|
-
wranglerEnvs.push(result.unwrap());
|
|
104
86
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
undefined,
|
|
141
|
-
TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
|
|
142
|
-
);
|
|
143
|
-
};
|
|
144
|
-
const typeText = getTypeText();
|
|
145
|
-
if (typeText === "CloesceApp") {
|
|
146
|
-
return Either.right(sourceFile.getFilePath().toString());
|
|
147
|
-
}
|
|
148
|
-
return err(ExtractorErrorCode.AppMissingDefaultExport);
|
|
149
|
-
}
|
|
150
|
-
static model(classDecl, sourceFile) {
|
|
151
|
-
const name = classDecl.getName();
|
|
152
|
-
const attributes = [];
|
|
153
|
-
const navigation_properties = [];
|
|
154
|
-
const data_sources = {};
|
|
155
|
-
const methods = {};
|
|
156
|
-
const cruds = new Set();
|
|
157
|
-
let primary_key = undefined;
|
|
158
|
-
// Extract crud methods
|
|
159
|
-
const crudDecorator = classDecl
|
|
160
|
-
.getDecorators()
|
|
161
|
-
.find((d) => getDecoratorName(d) === ClassDecoratorKind.CRUD);
|
|
162
|
-
if (crudDecorator) {
|
|
163
|
-
setCrudKinds(crudDecorator, cruds);
|
|
164
|
-
}
|
|
165
|
-
// Iterate attribtutes
|
|
166
|
-
for (const prop of classDecl.getProperties()) {
|
|
167
|
-
const decorators = prop.getDecorators();
|
|
168
|
-
const typeRes = CidlExtractor.cidlType(prop.getType());
|
|
169
|
-
// Error: invalid property type
|
|
170
|
-
if (typeRes.isLeft()) {
|
|
171
|
-
typeRes.value.context = prop.getName();
|
|
172
|
-
typeRes.value.snippet = prop.getText();
|
|
173
|
-
return typeRes;
|
|
174
|
-
}
|
|
175
|
-
const checkModifierRes = checkAttributeModifier(prop);
|
|
176
|
-
// No decorators means this is a standard attribute
|
|
177
|
-
if (decorators.length === 0) {
|
|
178
|
-
// Error: invalid attribute modifier
|
|
179
|
-
if (checkModifierRes.isLeft()) {
|
|
180
|
-
return checkModifierRes;
|
|
181
|
-
}
|
|
182
|
-
const cidl_type = typeRes.unwrap();
|
|
183
|
-
attributes.push({
|
|
184
|
-
foreign_key_reference: null,
|
|
185
|
-
value: {
|
|
186
|
-
name: prop.getName(),
|
|
187
|
-
cidl_type,
|
|
188
|
-
},
|
|
87
|
+
const extractor = new CidlExtractor(modelDecls);
|
|
88
|
+
// Extract models
|
|
89
|
+
const models = {};
|
|
90
|
+
for (const [_, [classDecl, decorator]] of modelDecls) {
|
|
91
|
+
const res = extractor.model(classDecl, classDecl.getSourceFile(), decorator);
|
|
92
|
+
if (res.isLeft()) {
|
|
93
|
+
res.value.addContext((prev) => `${classDecl.getName()}.${prev}`);
|
|
94
|
+
return res;
|
|
95
|
+
}
|
|
96
|
+
const model = res.unwrap();
|
|
97
|
+
models[model.name] = model;
|
|
98
|
+
}
|
|
99
|
+
// Extract services
|
|
100
|
+
const services = {};
|
|
101
|
+
for (const [_, classDecl] of serviceDecls) {
|
|
102
|
+
const res = extractor.service(classDecl, classDecl.getSourceFile());
|
|
103
|
+
if (res.isLeft()) {
|
|
104
|
+
res.value.addContext((prev) => `${classDecl.getName()}.${prev}`);
|
|
105
|
+
return res;
|
|
106
|
+
}
|
|
107
|
+
const service = res.unwrap();
|
|
108
|
+
services[service.name] = service;
|
|
109
|
+
}
|
|
110
|
+
// Error: Only one wrangler environment can exist
|
|
111
|
+
if (wranglerEnvs.length > 1) {
|
|
112
|
+
return err(ExtractorErrorCode.TooManyWranglerEnvs, (e) => (e.context = wranglerEnvs.map((w) => w.name).toString()));
|
|
113
|
+
}
|
|
114
|
+
const poos = Object.fromEntries(extractor.extractedPoos);
|
|
115
|
+
return Either.right({
|
|
116
|
+
project_name: projectName,
|
|
117
|
+
wrangler_env: wranglerEnvs[0], // undefined if none
|
|
118
|
+
models,
|
|
119
|
+
poos,
|
|
120
|
+
services,
|
|
121
|
+
main_source,
|
|
189
122
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
value: {
|
|
216
|
-
name: prop.getName(),
|
|
217
|
-
cidl_type,
|
|
218
|
-
},
|
|
219
|
-
});
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
case AttributeDecoratorKind.OneToOne: {
|
|
223
|
-
const reference = getDecoratorArgument(decorator, 0);
|
|
224
|
-
// Error: One to one navigation properties requre a reference
|
|
225
|
-
if (!reference) {
|
|
226
|
-
return err(
|
|
227
|
-
ExtractorErrorCode.MissingNavigationPropertyReference,
|
|
228
|
-
(e) => {
|
|
229
|
-
e.snippet = prop.getText();
|
|
230
|
-
e.context = prop.getName();
|
|
231
|
-
},
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
let model_name = getObjectName(cidl_type);
|
|
235
|
-
// Error: navigation properties require a model reference
|
|
236
|
-
if (!model_name) {
|
|
237
|
-
return err(
|
|
238
|
-
ExtractorErrorCode.MissingNavigationPropertyReference,
|
|
239
|
-
(e) => {
|
|
240
|
-
e.snippet = prop.getText();
|
|
241
|
-
e.context = prop.getName();
|
|
242
|
-
},
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
navigation_properties.push({
|
|
246
|
-
var_name: prop.getName(),
|
|
247
|
-
model_name,
|
|
248
|
-
kind: { OneToOne: { reference } },
|
|
249
|
-
});
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
case AttributeDecoratorKind.OneToMany: {
|
|
253
|
-
const reference = getDecoratorArgument(decorator, 0);
|
|
254
|
-
// Error: One to one navigation properties requre a reference
|
|
255
|
-
if (!reference) {
|
|
256
|
-
return err(
|
|
257
|
-
ExtractorErrorCode.MissingNavigationPropertyReference,
|
|
258
|
-
(e) => {
|
|
259
|
-
e.snippet = prop.getText();
|
|
260
|
-
e.context = prop.getName();
|
|
261
|
-
},
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
let model_name = getObjectName(cidl_type);
|
|
265
|
-
// Error: navigation properties require a model reference
|
|
266
|
-
if (!model_name) {
|
|
267
|
-
return err(
|
|
268
|
-
ExtractorErrorCode.MissingNavigationPropertyReference,
|
|
269
|
-
(e) => {
|
|
270
|
-
e.snippet = prop.getText();
|
|
271
|
-
e.context = prop.getName();
|
|
272
|
-
},
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
navigation_properties.push({
|
|
276
|
-
var_name: prop.getName(),
|
|
277
|
-
model_name,
|
|
278
|
-
kind: { OneToMany: { reference } },
|
|
279
|
-
});
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
case AttributeDecoratorKind.ManyToMany: {
|
|
283
|
-
const unique_id = getDecoratorArgument(decorator, 0);
|
|
284
|
-
// Error: many to many attribtues require a unique id
|
|
285
|
-
if (!unique_id)
|
|
286
|
-
return err(ExtractorErrorCode.MissingManyToManyUniqueId, (e) => {
|
|
287
|
-
e.snippet = prop.getText();
|
|
288
|
-
e.context = prop.getName();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* @returns An error if the main function is invalid, or the source code of the app function if valid.
|
|
126
|
+
* Undefined if no main function is defined.
|
|
127
|
+
*/
|
|
128
|
+
static main(sourceFile) {
|
|
129
|
+
const symbol = sourceFile.getDefaultExportSymbol();
|
|
130
|
+
const decl = symbol?.getDeclarations()[0];
|
|
131
|
+
if (!decl || !MorphNode.isFunctionDeclaration(decl)) {
|
|
132
|
+
return Either.right(undefined);
|
|
133
|
+
}
|
|
134
|
+
// Must be named "main"
|
|
135
|
+
const name = decl.getName();
|
|
136
|
+
if (name !== "main") {
|
|
137
|
+
return Either.right(undefined);
|
|
138
|
+
}
|
|
139
|
+
// Must be async
|
|
140
|
+
if (!decl.isAsync()) {
|
|
141
|
+
return err(ExtractorErrorCode.InvalidMain, (e) => (e.context = "Missing async modifier"));
|
|
142
|
+
}
|
|
143
|
+
// Must have exactly 4 parameters
|
|
144
|
+
const params = decl.getParameters();
|
|
145
|
+
if (params.length !== 4) {
|
|
146
|
+
return err(ExtractorErrorCode.InvalidMain, (e) => {
|
|
147
|
+
e.context = `Expected 4 parameters, got ${params.length}`;
|
|
289
148
|
});
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (!prop.isStatic() || !isIncludeTree) {
|
|
318
|
-
return err(ExtractorErrorCode.InvalidDataSourceDefinition, (e) => {
|
|
319
|
-
e.snippet = prop.getText();
|
|
320
|
-
e.context = prop.getName();
|
|
149
|
+
}
|
|
150
|
+
// Expected parameter types in order
|
|
151
|
+
// WranglerEnv does not have a required type annotation
|
|
152
|
+
const expectedTypes = ["Request", null, "CloesceApp", "ExecutionContext"];
|
|
153
|
+
for (let i = 0; i < params.length; i++) {
|
|
154
|
+
const param = params[i];
|
|
155
|
+
const expectedType = expectedTypes[i];
|
|
156
|
+
if (expectedType === null) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const paramType = param.getType();
|
|
160
|
+
const symbol = paramType.getAliasSymbol() ??
|
|
161
|
+
paramType.getSymbol() ??
|
|
162
|
+
paramType.getTargetType()?.getSymbol();
|
|
163
|
+
if (symbol?.getName() !== expectedType) {
|
|
164
|
+
return err(ExtractorErrorCode.InvalidMain, (e) => {
|
|
165
|
+
e.context = `Expected parameter ${i + 1} to be of type ${expectedType}, got ${paramType}`;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Must return Response
|
|
170
|
+
const returnType = decl
|
|
171
|
+
.getReturnType()
|
|
172
|
+
.getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope);
|
|
173
|
+
if (returnType !== "Promise<Response>") {
|
|
174
|
+
return err(ExtractorErrorCode.InvalidMain, (e) => {
|
|
175
|
+
e.context = `Expected return type to be Promise<Response>, got ${returnType}`;
|
|
321
176
|
});
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const treeRes = CidlExtractor.includeTree(
|
|
325
|
-
initializer,
|
|
326
|
-
classDecl,
|
|
327
|
-
sourceFile,
|
|
328
|
-
);
|
|
329
|
-
if (treeRes.isLeft()) {
|
|
330
|
-
treeRes.value.addContext((prev) => `${prop.getName()} ${prev}`);
|
|
331
|
-
treeRes.value.snippet = prop.getText();
|
|
332
|
-
return treeRes;
|
|
333
|
-
}
|
|
334
|
-
data_sources[prop.getName()] = {
|
|
335
|
-
name: prop.getName(),
|
|
336
|
-
tree: treeRes.unwrap(),
|
|
337
|
-
};
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (primary_key == undefined) {
|
|
343
|
-
return err(ExtractorErrorCode.MissingPrimaryKey, (e) => {
|
|
344
|
-
e.snippet = classDecl.getText();
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
// Process methods
|
|
348
|
-
for (const m of classDecl.getMethods()) {
|
|
349
|
-
const httpVerb = m
|
|
350
|
-
.getDecorators()
|
|
351
|
-
.map((d) => getDecoratorName(d))
|
|
352
|
-
.find((name) => Object.values(HttpVerb).includes(name));
|
|
353
|
-
if (!httpVerb) {
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
const result = CidlExtractor.modelMethod(name, m, httpVerb);
|
|
357
|
-
if (result.isLeft()) {
|
|
358
|
-
result.value.addContext((prev) => `${m.getName()} ${prev}`);
|
|
359
|
-
return result;
|
|
360
|
-
}
|
|
361
|
-
methods[result.unwrap().name] = result.unwrap();
|
|
362
|
-
}
|
|
363
|
-
return Either.right({
|
|
364
|
-
name,
|
|
365
|
-
attributes,
|
|
366
|
-
primary_key,
|
|
367
|
-
navigation_properties,
|
|
368
|
-
methods,
|
|
369
|
-
data_sources,
|
|
370
|
-
cruds: Array.from(cruds).sort(),
|
|
371
|
-
source_path: sourceFile.getFilePath().toString(),
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
static modelMethod(modelName, method, verb) {
|
|
375
|
-
// Error: invalid method scope, must be public
|
|
376
|
-
if (method.getScope() != Scope.Public) {
|
|
377
|
-
return err(ExtractorErrorCode.InvalidApiMethodModifier, (e) => {
|
|
378
|
-
e.context = method.getName();
|
|
379
|
-
e.snippet = method.getText();
|
|
380
|
-
});
|
|
177
|
+
}
|
|
178
|
+
return Either.right(sourceFile.getFilePath().toString());
|
|
381
179
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
180
|
+
model(classDecl, sourceFile, decorator) {
|
|
181
|
+
const name = classDecl.getName();
|
|
182
|
+
const columns = [];
|
|
183
|
+
const key_params = [];
|
|
184
|
+
const kv_objects = [];
|
|
185
|
+
const r2_objects = [];
|
|
186
|
+
const navigation_properties = [];
|
|
187
|
+
const data_sources = {};
|
|
188
|
+
const methods = {};
|
|
189
|
+
const cruds = new Set();
|
|
190
|
+
let primary_key = null;
|
|
191
|
+
// Extract crud methods
|
|
192
|
+
const arg = decorator.getArguments()[0];
|
|
193
|
+
if (arg && MorphNode.isArrayLiteralExpression(arg)) {
|
|
194
|
+
for (const a of arg.getElements()) {
|
|
195
|
+
cruds.add((MorphNode.isStringLiteral(a)
|
|
196
|
+
? a.getLiteralValue()
|
|
197
|
+
: a.getText()));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Iterate properties
|
|
201
|
+
for (const prop of classDecl.getProperties()) {
|
|
202
|
+
const typeRes = CidlExtractor.cidlType(prop.getType());
|
|
203
|
+
// Error: invalid property type
|
|
204
|
+
if (typeRes.isLeft()) {
|
|
205
|
+
typeRes.value.context = prop.getName();
|
|
206
|
+
typeRes.value.snippet = prop.getText();
|
|
207
|
+
return typeRes;
|
|
208
|
+
}
|
|
209
|
+
const cidl_type = typeRes.unwrap();
|
|
210
|
+
// Include Trees
|
|
211
|
+
const isIncludeTree = prop
|
|
212
|
+
.getType()
|
|
213
|
+
.getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope) === `IncludeTree<${name}>`;
|
|
214
|
+
if (isIncludeTree) {
|
|
215
|
+
// Error: data sources must be static include trees
|
|
216
|
+
if (!prop.isStatic()) {
|
|
217
|
+
return err(ExtractorErrorCode.InvalidDataSourceDefinition, (e) => {
|
|
218
|
+
e.snippet = prop.getText();
|
|
219
|
+
e.context = prop.getName();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const initializer = prop.getInitializer();
|
|
223
|
+
if (!initializer?.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
224
|
+
return err(ExtractorErrorCode.InvalidDataSourceDefinition, (e) => {
|
|
225
|
+
e.snippet = prop.getText();
|
|
226
|
+
e.context = prop.getName();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
data_sources[prop.getName()] = {
|
|
230
|
+
name: prop.getName(),
|
|
231
|
+
tree: parseIncludeTree(initializer),
|
|
232
|
+
};
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const checkModifierRes = checkPropertyModifier(prop);
|
|
236
|
+
// Error: invalid property modifier
|
|
237
|
+
if (checkModifierRes.isLeft()) {
|
|
238
|
+
return checkModifierRes;
|
|
239
|
+
}
|
|
240
|
+
// Infer decorator
|
|
241
|
+
if (prop.getDecorators().length === 0) {
|
|
242
|
+
this.inferModelDecorator(prop, classDecl, cidl_type);
|
|
243
|
+
}
|
|
244
|
+
const decorators = prop.getDecorators();
|
|
245
|
+
// Scalar column
|
|
246
|
+
if (decorators.length === 0) {
|
|
247
|
+
columns.push({
|
|
248
|
+
foreign_key_reference: null,
|
|
249
|
+
value: {
|
|
250
|
+
name: prop.getName(),
|
|
251
|
+
cidl_type,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const decorator = decorators[0];
|
|
257
|
+
const decoratorName = getDecoratorName(decorator);
|
|
258
|
+
// Process decorator
|
|
259
|
+
switch (decoratorName) {
|
|
260
|
+
case PropertyDecoratorKind.PrimaryKey: {
|
|
261
|
+
primary_key = {
|
|
262
|
+
name: prop.getName(),
|
|
263
|
+
cidl_type,
|
|
264
|
+
};
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
case PropertyDecoratorKind.ForeignKey: {
|
|
268
|
+
columns.push({
|
|
269
|
+
foreign_key_reference: getDecoratorArgument(decorator, 0) ?? null,
|
|
270
|
+
value: {
|
|
271
|
+
name: prop.getName(),
|
|
272
|
+
cidl_type,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case PropertyDecoratorKind.OneToOne: {
|
|
278
|
+
const selector = getSelectorPropertyName(decorator);
|
|
279
|
+
if (selector.isLeft()) {
|
|
280
|
+
return err(ExtractorErrorCode.InvalidSelectorSyntax, (e) => {
|
|
281
|
+
e.snippet = prop.getText();
|
|
282
|
+
e.context = prop.getName();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const model_name = getObjectName(cidl_type);
|
|
286
|
+
// Error: navigation properties require a model reference
|
|
287
|
+
if (!model_name) {
|
|
288
|
+
return err(ExtractorErrorCode.InvalidSelectorSyntax, (e) => {
|
|
289
|
+
e.snippet = prop.getText();
|
|
290
|
+
e.context = prop.getName();
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
navigation_properties.push({
|
|
294
|
+
var_name: prop.getName(),
|
|
295
|
+
model_reference: model_name,
|
|
296
|
+
kind: { OneToOne: { column_reference: selector.unwrap() } },
|
|
297
|
+
});
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
case PropertyDecoratorKind.OneToMany: {
|
|
301
|
+
const selector = getSelectorPropertyName(decorator);
|
|
302
|
+
if (selector.isLeft()) {
|
|
303
|
+
return err(ExtractorErrorCode.InvalidSelectorSyntax, (e) => {
|
|
304
|
+
e.snippet = prop.getText();
|
|
305
|
+
e.context = prop.getName();
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
let model_name = getObjectName(cidl_type);
|
|
309
|
+
// Error: navigation properties require a model reference
|
|
310
|
+
if (!model_name) {
|
|
311
|
+
return err(ExtractorErrorCode.InvalidNavigationProperty, (e) => {
|
|
312
|
+
e.snippet = prop.getText();
|
|
313
|
+
e.context = prop.getName();
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
navigation_properties.push({
|
|
317
|
+
var_name: prop.getName(),
|
|
318
|
+
model_reference: model_name,
|
|
319
|
+
kind: { OneToMany: { column_reference: selector.unwrap() } },
|
|
320
|
+
});
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
case PropertyDecoratorKind.ManyToMany: {
|
|
324
|
+
// Error: navigation properties require a model reference
|
|
325
|
+
let model_name = getObjectName(cidl_type);
|
|
326
|
+
if (!model_name) {
|
|
327
|
+
return err(ExtractorErrorCode.InvalidNavigationProperty, (e) => {
|
|
328
|
+
e.snippet = prop.getText();
|
|
329
|
+
e.context = prop.getName();
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
navigation_properties.push({
|
|
333
|
+
var_name: prop.getName(),
|
|
334
|
+
model_reference: model_name,
|
|
335
|
+
kind: "ManyToMany",
|
|
336
|
+
});
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case PropertyDecoratorKind.KeyParam: {
|
|
340
|
+
key_params.push(prop.getName());
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case PropertyDecoratorKind.KV: {
|
|
344
|
+
// Format and namespace binding are required
|
|
345
|
+
const format = getDecoratorArgument(decorator, 0);
|
|
346
|
+
const namespace_binding = getDecoratorArgument(decorator, 1);
|
|
347
|
+
if (!format || !namespace_binding) {
|
|
348
|
+
return err(ExtractorErrorCode.InvalidTypescriptSyntax, (e) => {
|
|
349
|
+
e.snippet = prop.getText();
|
|
350
|
+
e.context = prop.getName();
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
// Ensure that the prop type is KValue<T>
|
|
354
|
+
const [ty, _] = unwrapNullable(prop.getType());
|
|
355
|
+
const isArray = ty.isArray();
|
|
356
|
+
const elementType = isArray ? ty.getArrayElementTypeOrThrow() : ty;
|
|
357
|
+
const symbolName = elementType.getSymbol()?.getName();
|
|
358
|
+
if (symbolName !== "KValue") {
|
|
359
|
+
return err(ExtractorErrorCode.MissingKValue, (e) => {
|
|
360
|
+
e.snippet = prop.getText();
|
|
361
|
+
e.context = prop.getName();
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
kv_objects.push({
|
|
365
|
+
format,
|
|
366
|
+
namespace_binding,
|
|
367
|
+
value: {
|
|
368
|
+
name: prop.getName(),
|
|
369
|
+
cidl_type: isArray ? cidl_type.Array : cidl_type,
|
|
370
|
+
},
|
|
371
|
+
list_prefix: isArray,
|
|
372
|
+
});
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case PropertyDecoratorKind.R2: {
|
|
376
|
+
// Format and bucket binding are required
|
|
377
|
+
const format = getDecoratorArgument(decorator, 0);
|
|
378
|
+
const bucket_binding = getDecoratorArgument(decorator, 1);
|
|
379
|
+
if (!format || !bucket_binding) {
|
|
380
|
+
return err(ExtractorErrorCode.InvalidTypescriptSyntax, (e) => {
|
|
381
|
+
e.snippet = prop.getText();
|
|
382
|
+
e.context = prop.getName();
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
// Type must be R2ObjectBody
|
|
386
|
+
const [ty, _] = unwrapNullable(prop.getType());
|
|
387
|
+
const isArray = ty.isArray();
|
|
388
|
+
const elementType = isArray ? ty.getArrayElementTypeOrThrow() : ty;
|
|
389
|
+
const symbolName = elementType.getSymbol()?.getName();
|
|
390
|
+
if (symbolName !== "R2ObjectBody") {
|
|
391
|
+
return err(ExtractorErrorCode.MissingR2ObjectBody, (e) => {
|
|
392
|
+
e.snippet = prop.getText();
|
|
393
|
+
e.context = prop.getName();
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
r2_objects.push({
|
|
397
|
+
format,
|
|
398
|
+
bucket_binding,
|
|
399
|
+
var_name: prop.getName(),
|
|
400
|
+
list_prefix: isArray,
|
|
401
|
+
});
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
393
405
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
406
|
+
// Process methods
|
|
407
|
+
for (const m of classDecl.getMethods()) {
|
|
408
|
+
const httpVerb = m
|
|
409
|
+
.getDecorators()
|
|
410
|
+
.map(getDecoratorName)
|
|
411
|
+
.find((name) => Object.values(HttpVerb).includes(name));
|
|
412
|
+
if (!httpVerb) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const result = this.method(m, httpVerb);
|
|
416
|
+
if (result.isLeft()) {
|
|
417
|
+
result.value.addContext((prev) => `${m.getName()} ${prev}`);
|
|
418
|
+
return result;
|
|
419
|
+
}
|
|
420
|
+
methods[result.unwrap().name] = result.unwrap();
|
|
421
|
+
}
|
|
422
|
+
return Either.right({
|
|
423
|
+
name,
|
|
424
|
+
columns,
|
|
425
|
+
primary_key,
|
|
426
|
+
navigation_properties,
|
|
427
|
+
key_params,
|
|
428
|
+
kv_objects,
|
|
429
|
+
r2_objects,
|
|
430
|
+
methods,
|
|
431
|
+
data_sources,
|
|
432
|
+
cruds: Array.from(cruds).sort(),
|
|
433
|
+
source_path: sourceFile.getFilePath().toString(),
|
|
397
434
|
});
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
// Handle all other params
|
|
401
|
-
const typeRes = CidlExtractor.cidlType(param.getType());
|
|
402
|
-
// Error: invalid type
|
|
403
|
-
if (typeRes.isLeft()) {
|
|
404
|
-
typeRes.value.snippet = method.getText();
|
|
405
|
-
typeRes.value.context = param.getName();
|
|
406
|
-
return typeRes;
|
|
407
|
-
}
|
|
408
|
-
if (typeof typeRes.value !== "string" && "DataSource" in typeRes.value) {
|
|
409
|
-
needsDataSource = false;
|
|
410
|
-
}
|
|
411
|
-
parameters.push({
|
|
412
|
-
name: param.getName(),
|
|
413
|
-
cidl_type: typeRes.unwrap(),
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
const typeRes = CidlExtractor.cidlType(method.getReturnType());
|
|
417
|
-
// Error: invalid type
|
|
418
|
-
if (typeRes.isLeft()) {
|
|
419
|
-
typeRes.value.snippet = method.getText();
|
|
420
|
-
return typeRes;
|
|
421
|
-
}
|
|
422
|
-
// Sugaring: add data source
|
|
423
|
-
if (needsDataSource) {
|
|
424
|
-
parameters.push({
|
|
425
|
-
name: "__dataSource",
|
|
426
|
-
cidl_type: { DataSource: modelName },
|
|
427
|
-
});
|
|
428
435
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
436
|
+
service(classDecl, sourceFile) {
|
|
437
|
+
const attributes = [];
|
|
438
|
+
const methods = {};
|
|
439
|
+
// Properties
|
|
440
|
+
for (const prop of classDecl.getProperties()) {
|
|
441
|
+
const typeRes = CidlExtractor.cidlType(prop.getType(), true);
|
|
442
|
+
// Error: invalid property type
|
|
443
|
+
if (typeRes.isLeft()) {
|
|
444
|
+
typeRes.value.context = prop.getName();
|
|
445
|
+
typeRes.value.snippet = prop.getText();
|
|
446
|
+
return typeRes;
|
|
447
|
+
}
|
|
448
|
+
if (typeof typeRes.value === "string" || !("Inject" in typeRes.value)) {
|
|
449
|
+
return err(ExtractorErrorCode.InvalidServiceProperty, (e) => {
|
|
450
|
+
e.context = prop.getName();
|
|
451
|
+
e.snippet = prop.getText();
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
// Error: invalid property modifier
|
|
455
|
+
const checkModifierRes = checkPropertyModifier(prop);
|
|
456
|
+
if (checkModifierRes.isLeft()) {
|
|
457
|
+
return checkModifierRes;
|
|
458
|
+
}
|
|
459
|
+
attributes.push({
|
|
460
|
+
var_name: prop.getName(),
|
|
461
|
+
inject_reference: typeRes.value.Inject,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
// Methods
|
|
465
|
+
for (const m of classDecl.getMethods()) {
|
|
466
|
+
const httpVerb = m
|
|
467
|
+
.getDecorators()
|
|
468
|
+
.map(getDecoratorName)
|
|
469
|
+
.find((name) => Object.values(HttpVerb).includes(name));
|
|
470
|
+
if (!httpVerb) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const res = this.method(m, httpVerb);
|
|
474
|
+
if (res.isLeft()) {
|
|
475
|
+
return res;
|
|
476
|
+
}
|
|
477
|
+
const serviceMethod = res.unwrap();
|
|
478
|
+
methods[serviceMethod.name] = serviceMethod;
|
|
479
|
+
}
|
|
480
|
+
return Either.right({
|
|
481
|
+
name: classDecl.getName(),
|
|
482
|
+
attributes,
|
|
483
|
+
methods,
|
|
484
|
+
source_path: sourceFile.getFilePath().toString(),
|
|
455
485
|
});
|
|
456
|
-
}
|
|
457
|
-
const checkModifierRes = checkAttributeModifier(prop);
|
|
458
|
-
if (checkModifierRes.isLeft()) {
|
|
459
|
-
return checkModifierRes;
|
|
460
|
-
}
|
|
461
|
-
attributes.push({
|
|
462
|
-
var_name: prop.getName(),
|
|
463
|
-
injected: typeRes.value.Inject,
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
// Methods
|
|
467
|
-
for (const m of classDecl.getMethods()) {
|
|
468
|
-
const httpVerb = m
|
|
469
|
-
.getDecorators()
|
|
470
|
-
.map((d) => getDecoratorName(d))
|
|
471
|
-
.find((name) => Object.values(HttpVerb).includes(name));
|
|
472
|
-
if (!httpVerb) {
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
const res = CidlExtractor.serviceMethod(m, httpVerb);
|
|
476
|
-
if (res.isLeft()) {
|
|
477
|
-
return res;
|
|
478
|
-
}
|
|
479
|
-
const serviceMethod = res.unwrap();
|
|
480
|
-
methods[serviceMethod.name] = serviceMethod;
|
|
481
486
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
487
|
+
method(method, verb) {
|
|
488
|
+
// Error: invalid method scope, must be public
|
|
489
|
+
if (method.getScope() != Scope.Public) {
|
|
490
|
+
return err(ExtractorErrorCode.InvalidApiMethodModifier, (e) => {
|
|
491
|
+
e.context = method.getName();
|
|
492
|
+
e.snippet = method.getText();
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
const parameters = [];
|
|
496
|
+
for (const param of method.getParameters()) {
|
|
497
|
+
// Handle injected param
|
|
498
|
+
if (param.getDecorator(ParameterDecoratorKind.Inject)) {
|
|
499
|
+
const typeRes = CidlExtractor.cidlType(param.getType(), true);
|
|
500
|
+
// Error: invalid type
|
|
501
|
+
if (typeRes.isLeft()) {
|
|
502
|
+
typeRes.value.snippet = method.getText();
|
|
503
|
+
typeRes.value.context = param.getName();
|
|
504
|
+
return typeRes;
|
|
505
|
+
}
|
|
506
|
+
parameters.push({
|
|
507
|
+
name: param.getName(),
|
|
508
|
+
cidl_type: typeRes.unwrap(),
|
|
509
|
+
});
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
// Handle all other params
|
|
513
|
+
const typeRes = CidlExtractor.cidlType(param.getType());
|
|
514
|
+
// Error: invalid type
|
|
515
|
+
if (typeRes.isLeft()) {
|
|
516
|
+
typeRes.value.snippet = method.getText();
|
|
517
|
+
typeRes.value.context = param.getName();
|
|
518
|
+
return typeRes;
|
|
519
|
+
}
|
|
520
|
+
// Extract any POOs used as parameter types
|
|
521
|
+
const objectName = getObjectName(typeRes.unwrap());
|
|
522
|
+
if (objectName &&
|
|
523
|
+
!this.extractedPoos.has(objectName) &&
|
|
524
|
+
!this.modelDecls.has(objectName)) {
|
|
525
|
+
const res = this.poo(method.getSourceFile().getClassOrThrow(objectName), method.getSourceFile());
|
|
526
|
+
if (res.isLeft()) {
|
|
527
|
+
res.value.addContext((prev) => `${param.getName()}.${prev}`);
|
|
528
|
+
return res;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
parameters.push({
|
|
532
|
+
name: param.getName(),
|
|
533
|
+
cidl_type: typeRes.unwrap(),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
const typeRes = CidlExtractor.cidlType(method.getReturnType());
|
|
502
537
|
// Error: invalid type
|
|
503
538
|
if (typeRes.isLeft()) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
539
|
+
typeRes.value.snippet = method.getText();
|
|
540
|
+
return typeRes;
|
|
541
|
+
}
|
|
542
|
+
// Extract any POOs used as return types
|
|
543
|
+
const objectName = getObjectName(typeRes.unwrap());
|
|
544
|
+
if (objectName &&
|
|
545
|
+
!this.extractedPoos.has(objectName) &&
|
|
546
|
+
!this.modelDecls.has(objectName)) {
|
|
547
|
+
const res = this.poo(method.getSourceFile().getClassOrThrow(objectName), method.getSourceFile());
|
|
548
|
+
if (res.isLeft()) {
|
|
549
|
+
res.value.addContext((prev) => `returns ${prev}`);
|
|
550
|
+
return res;
|
|
551
|
+
}
|
|
507
552
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
553
|
+
return Either.right({
|
|
554
|
+
name: method.getName(),
|
|
555
|
+
http_verb: verb,
|
|
556
|
+
is_static: method.isStatic(),
|
|
557
|
+
return_media: defaultMediaType(),
|
|
558
|
+
return_type: typeRes.unwrap(),
|
|
559
|
+
parameters_media: defaultMediaType(),
|
|
560
|
+
parameters,
|
|
511
561
|
});
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
// Handle all other params
|
|
515
|
-
const typeRes = CidlExtractor.cidlType(param.getType());
|
|
516
|
-
// Error: invalid type
|
|
517
|
-
if (typeRes.isLeft()) {
|
|
518
|
-
typeRes.value.snippet = method.getText();
|
|
519
|
-
typeRes.value.context = param.getName();
|
|
520
|
-
return typeRes;
|
|
521
|
-
}
|
|
522
|
-
parameters.push({
|
|
523
|
-
name: param.getName(),
|
|
524
|
-
cidl_type: typeRes.unwrap(),
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
const typeRes = CidlExtractor.cidlType(method.getReturnType());
|
|
528
|
-
// Error: invalid type
|
|
529
|
-
if (typeRes.isLeft()) {
|
|
530
|
-
typeRes.value.snippet = method.getText();
|
|
531
|
-
return typeRes;
|
|
532
|
-
}
|
|
533
|
-
return Either.right({
|
|
534
|
-
name: method.getName(),
|
|
535
|
-
http_verb: verb,
|
|
536
|
-
is_static: method.isStatic(),
|
|
537
|
-
return_media: defaultMediaType(),
|
|
538
|
-
return_type: typeRes.unwrap(),
|
|
539
|
-
parameters_media: defaultMediaType(),
|
|
540
|
-
parameters,
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
static poo(classDecl, sourceFile) {
|
|
544
|
-
const name = classDecl.getName();
|
|
545
|
-
const attributes = [];
|
|
546
|
-
for (const prop of classDecl.getProperties()) {
|
|
547
|
-
const typeRes = CidlExtractor.cidlType(prop.getType());
|
|
548
|
-
// Error: invalid property type
|
|
549
|
-
if (typeRes.isLeft()) {
|
|
550
|
-
typeRes.value.context = prop.getName();
|
|
551
|
-
typeRes.value.snippet = prop.getText();
|
|
552
|
-
return typeRes;
|
|
553
|
-
}
|
|
554
|
-
// Error: invalid attribute modifier
|
|
555
|
-
const modifierRes = checkAttributeModifier(prop);
|
|
556
|
-
if (modifierRes.isLeft()) {
|
|
557
|
-
return modifierRes;
|
|
558
|
-
}
|
|
559
|
-
const cidl_type = typeRes.unwrap();
|
|
560
|
-
attributes.push({
|
|
561
|
-
name: prop.getName(),
|
|
562
|
-
cidl_type,
|
|
563
|
-
});
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
return Either.right({
|
|
567
|
-
name,
|
|
568
|
-
attributes,
|
|
569
|
-
source_path: sourceFile.getFilePath().toString(),
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
static env(classDecl, sourceFile) {
|
|
573
|
-
const vars = {};
|
|
574
|
-
let binding;
|
|
575
|
-
for (const prop of classDecl.getProperties()) {
|
|
576
|
-
if (
|
|
577
|
-
prop
|
|
578
|
-
.getType()
|
|
579
|
-
.getText(
|
|
580
|
-
undefined,
|
|
581
|
-
TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
|
|
582
|
-
) === "D1Database"
|
|
583
|
-
) {
|
|
584
|
-
binding = prop.getName();
|
|
585
|
-
continue;
|
|
586
|
-
}
|
|
587
|
-
const ty = CidlExtractor.cidlType(prop.getType());
|
|
588
|
-
if (ty.isLeft()) {
|
|
589
|
-
ty.value.context = prop.getName();
|
|
590
|
-
ty.value.snippet = prop.getText();
|
|
591
|
-
return ty;
|
|
592
|
-
}
|
|
593
|
-
vars[prop.getName()] = ty.unwrap();
|
|
594
|
-
}
|
|
595
|
-
if (!binding) {
|
|
596
|
-
return err(ExtractorErrorCode.MissingDatabaseBinding);
|
|
597
|
-
}
|
|
598
|
-
return Either.right({
|
|
599
|
-
name: classDecl.getName(),
|
|
600
|
-
source_path: sourceFile.getFilePath().toString(),
|
|
601
|
-
db_binding: binding,
|
|
602
|
-
vars,
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
static primTypeMap = {
|
|
606
|
-
number: "Real",
|
|
607
|
-
Number: "Real",
|
|
608
|
-
Integer: "Integer",
|
|
609
|
-
string: "Text",
|
|
610
|
-
String: "Text",
|
|
611
|
-
boolean: "Boolean",
|
|
612
|
-
Boolean: "Boolean",
|
|
613
|
-
Date: "DateIso",
|
|
614
|
-
Uint8Array: "Blob",
|
|
615
|
-
Stream: "Stream",
|
|
616
|
-
};
|
|
617
|
-
static cidlType(type, inject = false) {
|
|
618
|
-
// Void
|
|
619
|
-
if (type.isVoid()) {
|
|
620
|
-
return Either.right("Void");
|
|
621
|
-
}
|
|
622
|
-
// Null
|
|
623
|
-
if (type.isNull()) {
|
|
624
|
-
return Either.right({ Nullable: "Void" });
|
|
625
|
-
}
|
|
626
|
-
// Nullable via union
|
|
627
|
-
const [unwrappedType, nullable] = unwrapNullable(type);
|
|
628
|
-
const tyText = unwrappedType
|
|
629
|
-
.getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope)
|
|
630
|
-
.split("|")[0]
|
|
631
|
-
.trim();
|
|
632
|
-
// Primitives
|
|
633
|
-
const prim = this.primTypeMap[tyText];
|
|
634
|
-
if (prim) {
|
|
635
|
-
return Either.right(wrapNullable(prim, nullable));
|
|
636
|
-
}
|
|
637
|
-
const generics = [
|
|
638
|
-
...unwrappedType.getAliasTypeArguments(),
|
|
639
|
-
...unwrappedType.getTypeArguments(),
|
|
640
|
-
];
|
|
641
|
-
// Error: can't handle multiple generics
|
|
642
|
-
if (generics.length > 1) {
|
|
643
|
-
return err(ExtractorErrorCode.MultipleGenericType);
|
|
644
|
-
}
|
|
645
|
-
// No generics -> inject or object
|
|
646
|
-
if (generics.length === 0) {
|
|
647
|
-
const base = inject ? { Inject: tyText } : { Object: tyText };
|
|
648
|
-
return Either.right(wrapNullable(base, nullable));
|
|
649
|
-
}
|
|
650
|
-
// Single generic
|
|
651
|
-
const genericTy = generics[0];
|
|
652
|
-
const symbolName = unwrappedType.getSymbol()?.getName();
|
|
653
|
-
const aliasName = unwrappedType.getAliasSymbol()?.getName();
|
|
654
|
-
if (aliasName === "DataSourceOf") {
|
|
655
|
-
return Either.right(
|
|
656
|
-
wrapNullable(
|
|
657
|
-
{
|
|
658
|
-
DataSource: genericTy.getText(
|
|
659
|
-
undefined,
|
|
660
|
-
TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
|
|
661
|
-
),
|
|
662
|
-
},
|
|
663
|
-
nullable,
|
|
664
|
-
),
|
|
665
|
-
);
|
|
666
|
-
}
|
|
667
|
-
if (aliasName === "DeepPartial") {
|
|
668
|
-
const [_, genericTyNullable] = unwrapNullable(genericTy);
|
|
669
|
-
const genericTyGenerics = [
|
|
670
|
-
...genericTy.getAliasTypeArguments(),
|
|
671
|
-
...genericTy.getTypeArguments(),
|
|
672
|
-
];
|
|
673
|
-
// Expect partials to be of the exact form DeepPartial<Model>
|
|
674
|
-
if (
|
|
675
|
-
genericTyNullable ||
|
|
676
|
-
genericTy.isUnion() ||
|
|
677
|
-
genericTyGenerics.length > 0
|
|
678
|
-
) {
|
|
679
|
-
return err(ExtractorErrorCode.InvalidPartialType);
|
|
680
|
-
}
|
|
681
|
-
return Either.right(
|
|
682
|
-
wrapNullable(
|
|
683
|
-
{
|
|
684
|
-
Partial: genericTy
|
|
685
|
-
.getText(
|
|
686
|
-
undefined,
|
|
687
|
-
TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
|
|
688
|
-
)
|
|
689
|
-
.split("|")[0]
|
|
690
|
-
.trim(),
|
|
691
|
-
},
|
|
692
|
-
nullable,
|
|
693
|
-
),
|
|
694
|
-
);
|
|
695
562
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
563
|
+
poo(classDecl, sourceFile) {
|
|
564
|
+
const name = classDecl.getName();
|
|
565
|
+
const attributes = [];
|
|
566
|
+
// Error: POOs must be exported
|
|
567
|
+
if (!classDecl.isExported()) {
|
|
568
|
+
return err(ExtractorErrorCode.MissingExport, (e) => {
|
|
569
|
+
e.context = name;
|
|
570
|
+
e.snippet = classDecl.getText();
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
for (const prop of classDecl.getProperties()) {
|
|
574
|
+
// Error: invalid property modifier
|
|
575
|
+
const modifierRes = checkPropertyModifier(prop);
|
|
576
|
+
if (modifierRes.isLeft()) {
|
|
577
|
+
return modifierRes;
|
|
578
|
+
}
|
|
579
|
+
const typeRes = CidlExtractor.cidlType(prop.getType());
|
|
580
|
+
// Error: invalid property type
|
|
581
|
+
if (typeRes.isLeft()) {
|
|
582
|
+
typeRes.value.context = prop.getName();
|
|
583
|
+
typeRes.value.snippet = prop.getText();
|
|
584
|
+
return typeRes;
|
|
585
|
+
}
|
|
586
|
+
const cidl_type = typeRes.unwrap();
|
|
587
|
+
// Check that the type is an already extracted POO, or a model decl.
|
|
588
|
+
// If not, find the source and extract it as a POO.
|
|
589
|
+
const objectName = getObjectName(cidl_type);
|
|
590
|
+
if (objectName &&
|
|
591
|
+
!this.extractedPoos.has(objectName) &&
|
|
592
|
+
!this.modelDecls.has(objectName)) {
|
|
593
|
+
const res = this.poo(classDecl.getSourceFile().getClassOrThrow(objectName), classDecl.getSourceFile());
|
|
594
|
+
if (res.isLeft()) {
|
|
595
|
+
res.value.addContext((prev) => `${prop.getName()}.${prev}`);
|
|
596
|
+
return res;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
attributes.push({
|
|
600
|
+
name: prop.getName(),
|
|
601
|
+
cidl_type,
|
|
602
|
+
});
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
// Mark as extracted
|
|
606
|
+
const poo = {
|
|
607
|
+
name,
|
|
608
|
+
attributes,
|
|
609
|
+
source_path: sourceFile.getFilePath().toString(),
|
|
610
|
+
};
|
|
611
|
+
this.extractedPoos.set(name, poo);
|
|
612
|
+
return Either.right(null);
|
|
721
613
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
614
|
+
// public for tests
|
|
615
|
+
static env(classDecl, sourceFile) {
|
|
616
|
+
const vars = {};
|
|
617
|
+
let d1_binding = undefined;
|
|
618
|
+
const kv_bindings = [];
|
|
619
|
+
const r2_bindings = [];
|
|
620
|
+
for (const prop of classDecl.getProperties()) {
|
|
621
|
+
// Error: invalid property modifier
|
|
622
|
+
const checkModifierRes = checkPropertyModifier(prop);
|
|
623
|
+
if (checkModifierRes.isLeft()) {
|
|
624
|
+
return checkModifierRes;
|
|
625
|
+
}
|
|
626
|
+
// TODO: Support multiple D1 bindings
|
|
627
|
+
if (prop
|
|
628
|
+
.getType()
|
|
629
|
+
.getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope) === "D1Database") {
|
|
630
|
+
d1_binding = prop.getName();
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (prop.getType().getSymbol()?.getName() === "KVNamespace") {
|
|
634
|
+
kv_bindings.push(prop.getName());
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (prop.getType().getSymbol()?.getName() === "R2Bucket") {
|
|
638
|
+
r2_bindings.push(prop.getName());
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const ty = CidlExtractor.cidlType(prop.getType());
|
|
642
|
+
if (ty.isLeft()) {
|
|
643
|
+
ty.value.context = prop.getName();
|
|
644
|
+
ty.value.snippet = prop.getText();
|
|
645
|
+
return ty;
|
|
646
|
+
}
|
|
647
|
+
vars[prop.getName()] = ty.unwrap();
|
|
648
|
+
}
|
|
649
|
+
return Either.right({
|
|
650
|
+
name: classDecl.getName(),
|
|
651
|
+
source_path: sourceFile.getFilePath().toString(),
|
|
652
|
+
d1_binding,
|
|
653
|
+
kv_bindings,
|
|
654
|
+
r2_bindings,
|
|
655
|
+
vars,
|
|
656
|
+
});
|
|
735
657
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
658
|
+
static primTypeMap = {
|
|
659
|
+
number: "Real",
|
|
660
|
+
Number: "Real",
|
|
661
|
+
Integer: "Integer",
|
|
662
|
+
string: "Text",
|
|
663
|
+
String: "Text",
|
|
664
|
+
boolean: "Boolean",
|
|
665
|
+
Boolean: "Boolean",
|
|
666
|
+
Date: "DateIso",
|
|
667
|
+
Uint8Array: "Blob",
|
|
668
|
+
};
|
|
669
|
+
// public for tests
|
|
670
|
+
static cidlType(type, inject = false) {
|
|
671
|
+
// Void
|
|
672
|
+
if (type.isVoid()) {
|
|
673
|
+
return Either.right("Void");
|
|
674
|
+
}
|
|
675
|
+
// Unknown
|
|
676
|
+
if (type.isUnknown()) {
|
|
677
|
+
return Either.right("JsonValue");
|
|
678
|
+
}
|
|
679
|
+
// Null
|
|
680
|
+
if (type.isNull()) {
|
|
681
|
+
return Either.right({ Nullable: "Void" });
|
|
682
|
+
}
|
|
683
|
+
// Nullable via union
|
|
684
|
+
const [unwrappedType, nullable] = unwrapNullable(type);
|
|
685
|
+
const tyText = unwrappedType
|
|
686
|
+
.getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope)
|
|
687
|
+
.split("|")[0]
|
|
688
|
+
.trim();
|
|
689
|
+
// Primitives
|
|
690
|
+
const prim = this.primTypeMap[tyText];
|
|
691
|
+
if (prim) {
|
|
692
|
+
return Either.right(wrapNullable(prim, nullable));
|
|
693
|
+
}
|
|
694
|
+
const generics = [
|
|
695
|
+
...unwrappedType.getAliasTypeArguments(),
|
|
696
|
+
...unwrappedType.getTypeArguments(),
|
|
697
|
+
];
|
|
698
|
+
// Error: can't handle multiple generics
|
|
699
|
+
if (generics.length > 1) {
|
|
700
|
+
return err(ExtractorErrorCode.MultipleGenericType);
|
|
701
|
+
}
|
|
702
|
+
// No generics -> inject or object
|
|
703
|
+
if (generics.length === 0) {
|
|
704
|
+
const base = inject ? { Inject: tyText } : { Object: tyText };
|
|
705
|
+
return Either.right(wrapNullable(base, nullable));
|
|
706
|
+
}
|
|
707
|
+
// Single generic
|
|
708
|
+
const genericTy = generics[0];
|
|
709
|
+
const symbolName = unwrappedType.getSymbol()?.getName();
|
|
710
|
+
const aliasName = unwrappedType.getAliasSymbol()?.getName();
|
|
711
|
+
if (aliasName === "DataSourceOf") {
|
|
712
|
+
return Either.right(wrapNullable({
|
|
713
|
+
DataSource: genericTy.getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope),
|
|
714
|
+
}, nullable));
|
|
715
|
+
}
|
|
716
|
+
if (aliasName === "DeepPartial") {
|
|
717
|
+
const [_, genericTyNullable] = unwrapNullable(genericTy);
|
|
718
|
+
const genericTyGenerics = [
|
|
719
|
+
...genericTy.getAliasTypeArguments(),
|
|
720
|
+
...genericTy.getTypeArguments(),
|
|
721
|
+
];
|
|
722
|
+
// Expect partials to be of the exact form DeepPartial<Model>
|
|
723
|
+
if (genericTyNullable ||
|
|
724
|
+
genericTy.isUnion() ||
|
|
725
|
+
genericTyGenerics.length > 0) {
|
|
726
|
+
return err(ExtractorErrorCode.InvalidPartialType);
|
|
727
|
+
}
|
|
728
|
+
return Either.right(wrapNullable({
|
|
729
|
+
Partial: genericTy
|
|
730
|
+
.getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope)
|
|
731
|
+
.split("|")[0]
|
|
732
|
+
.trim(),
|
|
733
|
+
}, nullable));
|
|
734
|
+
}
|
|
735
|
+
if (symbolName === ReadableStream.name) {
|
|
736
|
+
return Either.right(wrapNullable("Stream", nullable));
|
|
737
|
+
}
|
|
738
|
+
if (symbolName === Promise.name ||
|
|
739
|
+
aliasName === "IncludeTree" ||
|
|
740
|
+
symbolName === "KValue") {
|
|
741
|
+
return wrapGeneric(genericTy, nullable, (inner) => inner);
|
|
742
|
+
}
|
|
743
|
+
if (unwrappedType.isArray()) {
|
|
744
|
+
return wrapGeneric(genericTy, nullable, (inner) => ({ Array: inner }));
|
|
745
|
+
}
|
|
746
|
+
if (symbolName === "HttpResult") {
|
|
747
|
+
return wrapGeneric(genericTy, nullable, (inner) => ({
|
|
748
|
+
HttpResult: inner,
|
|
749
|
+
}));
|
|
750
|
+
}
|
|
751
|
+
// Error: unknown type
|
|
752
|
+
return err(ExtractorErrorCode.UnknownType);
|
|
753
|
+
function wrapNullable(inner, isNullable) {
|
|
754
|
+
if (isNullable) {
|
|
755
|
+
return { Nullable: inner };
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
return inner;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function wrapGeneric(t, isNullable, wrapper) {
|
|
762
|
+
const res = CidlExtractor.cidlType(t, inject);
|
|
763
|
+
return res.map((inner) => wrapNullable(wrapper(inner), isNullable));
|
|
764
|
+
}
|
|
745
765
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
766
|
+
/**
|
|
767
|
+
* Mutates the property declaration to add inferred decorators based on naming conventions.
|
|
768
|
+
*/
|
|
769
|
+
inferModelDecorator(prop, classDecl, cidlType) {
|
|
770
|
+
const className = classDecl.getName();
|
|
771
|
+
const objectName = getObjectName(cidlType);
|
|
772
|
+
const normalizedPropName = normalizeName(prop.getName());
|
|
773
|
+
// Primary Key
|
|
774
|
+
if (normalizedPropName === "id" ||
|
|
775
|
+
normalizedPropName === `${className.toLowerCase()}id`) {
|
|
776
|
+
// Add a primary key decorator
|
|
777
|
+
prop.addDecorator({
|
|
778
|
+
name: PropertyDecoratorKind.PrimaryKey,
|
|
779
|
+
arguments: [],
|
|
780
|
+
});
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
// Foreign Key
|
|
784
|
+
if (normalizedPropName.endsWith("id")) {
|
|
785
|
+
const referencedNavName = prop
|
|
786
|
+
.getName()
|
|
787
|
+
.slice(0, prop.getName().length - (normalizedPropName.endsWith("_id") ? 3 : 2));
|
|
788
|
+
const oneToOneProperties = classDecl
|
|
789
|
+
.getProperties()
|
|
790
|
+
.filter((p) => p.getName() === referencedNavName);
|
|
791
|
+
if (oneToOneProperties.length > 1) {
|
|
792
|
+
console.warn(`
|
|
793
|
+
Cannot infer ForeignKey relationship due to ambiguity, model ${className}, property ${prop.getName()}
|
|
794
|
+
could match ${oneToOneProperties.map((p) => p.getName()).join(", ")}
|
|
795
|
+
`);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
// If a one to one property exists with the expected name, use that
|
|
799
|
+
if (oneToOneProperties[0] !== undefined) {
|
|
800
|
+
const oneToOneProperty = oneToOneProperties[0];
|
|
801
|
+
const navModelTypeRes = CidlExtractor.cidlType(oneToOneProperty?.getType());
|
|
802
|
+
if (navModelTypeRes.isLeft()) {
|
|
803
|
+
navModelTypeRes.value.context = prop.getName();
|
|
804
|
+
navModelTypeRes.value.snippet = prop.getText();
|
|
805
|
+
return navModelTypeRes;
|
|
806
|
+
}
|
|
807
|
+
const navModelType = navModelTypeRes.unwrap();
|
|
808
|
+
const objectName = getObjectName(navModelType);
|
|
809
|
+
if (objectName) {
|
|
810
|
+
// Add a foreign key decorator
|
|
811
|
+
prop.addDecorator({
|
|
812
|
+
name: PropertyDecoratorKind.ForeignKey,
|
|
813
|
+
arguments: [objectName],
|
|
814
|
+
});
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (objectName !== undefined) {
|
|
819
|
+
const oneToManyClassDecl = this.modelDecls.get(objectName)?.[0];
|
|
820
|
+
const containsOneToManyProp = oneToManyClassDecl
|
|
821
|
+
?.getProperties()
|
|
822
|
+
.filter((p) => {
|
|
823
|
+
const tyRes = CidlExtractor.cidlType(p.getType());
|
|
824
|
+
if (tyRes.isLeft()) {
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
const ty = tyRes.unwrap();
|
|
828
|
+
const navObjectName = getObjectName(ty);
|
|
829
|
+
if (navObjectName !== className) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
if (typeof ty === "string" || !("Array" in ty)) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
return true;
|
|
836
|
+
});
|
|
837
|
+
if (containsOneToManyProp) {
|
|
838
|
+
if (containsOneToManyProp.length > 1) {
|
|
839
|
+
console.warn(`
|
|
840
|
+
Cannot infer ForeignKey relationship due to ambiguity, model ${className}, property ${prop.getName()}
|
|
841
|
+
could match ${containsOneToManyProp.map((p) => p.getName()).join(", ")}
|
|
842
|
+
`);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
// Add a foreign key decorator
|
|
846
|
+
prop.addDecorator({
|
|
847
|
+
name: PropertyDecoratorKind.ForeignKey,
|
|
848
|
+
arguments: [objectName],
|
|
849
|
+
});
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// One to Many + Many to Many
|
|
855
|
+
if (objectName !== undefined &&
|
|
856
|
+
typeof cidlType !== "string" &&
|
|
857
|
+
"Array" in cidlType) {
|
|
858
|
+
const referencedModelDecl = this.modelDecls.get(objectName)?.[0];
|
|
859
|
+
const normalizedModelIdName = `${normalizeName(className)}id`;
|
|
860
|
+
const foreignKeyProps = [];
|
|
861
|
+
const manyToManyProps = [];
|
|
862
|
+
for (const prop of referencedModelDecl?.getProperties() ?? []) {
|
|
863
|
+
const tyRes = CidlExtractor.cidlType(prop.getType());
|
|
864
|
+
if (tyRes.isLeft()) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const ty = tyRes.unwrap();
|
|
868
|
+
const navObjectName = getObjectName(ty);
|
|
869
|
+
const normalizedPropName = normalizeName(prop.getName());
|
|
870
|
+
if (typeof ty !== "string" &&
|
|
871
|
+
"Array" in ty &&
|
|
872
|
+
navObjectName === className) {
|
|
873
|
+
// Many to Many
|
|
874
|
+
manyToManyProps.push(prop);
|
|
875
|
+
}
|
|
876
|
+
else if (normalizedPropName === normalizedModelIdName) {
|
|
877
|
+
// One to Many
|
|
878
|
+
foreignKeyProps.push(prop);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (foreignKeyProps.length > 1) {
|
|
882
|
+
console.warn(`
|
|
883
|
+
Cannot infer OneToMany relationship due to ambiguity, model ${className}, property ${prop.getName()}
|
|
884
|
+
could match ${foreignKeyProps.map((p) => p.getName()).join(", ")}
|
|
885
|
+
`);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (manyToManyProps.length > 1) {
|
|
889
|
+
console.warn(`
|
|
890
|
+
Cannot infer ManyToMany relationship due to ambiguity, model ${className}, property ${prop.getName()}
|
|
891
|
+
could match ${manyToManyProps.map((p) => p.getName()).join(", ")}
|
|
892
|
+
`);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const hasForeignKeyProp = foreignKeyProps.at(0);
|
|
896
|
+
const hasManyToManyProp = manyToManyProps.at(0);
|
|
897
|
+
if (hasForeignKeyProp && hasManyToManyProp) {
|
|
898
|
+
console.warn(`
|
|
899
|
+
Cannot infer relationship due to ambiguity, model ${className}, property ${prop.getName()}
|
|
900
|
+
could be OneToMany or ManyToMany
|
|
901
|
+
`);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (hasForeignKeyProp) {
|
|
905
|
+
// Add a one to many decorator
|
|
906
|
+
prop.addDecorator({
|
|
907
|
+
name: PropertyDecoratorKind.OneToMany,
|
|
908
|
+
arguments: [`(m: any) => m.${hasForeignKeyProp.getName()}`],
|
|
909
|
+
});
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (hasManyToManyProp) {
|
|
913
|
+
// Add a many to many decorator
|
|
914
|
+
prop.addDecorator({
|
|
915
|
+
name: PropertyDecoratorKind.ManyToMany,
|
|
916
|
+
arguments: [],
|
|
917
|
+
});
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
// One to One
|
|
922
|
+
if (objectName !== undefined) {
|
|
923
|
+
const normalizedPropIdName = `${normalizedPropName}id`;
|
|
924
|
+
const foreignKeyProps = classDecl.getProperties().filter((p) => {
|
|
925
|
+
const norm = normalizeName(p.getName());
|
|
926
|
+
return norm === normalizedPropIdName;
|
|
927
|
+
});
|
|
928
|
+
if (foreignKeyProps.length > 1) {
|
|
929
|
+
console.warn(`
|
|
930
|
+
Cannot infer OneToOne relationship due to ambiguity, model ${className}, property ${prop.getName()}
|
|
931
|
+
could match ${foreignKeyProps.map((p) => p.getName()).join(", ")}
|
|
932
|
+
`);
|
|
933
|
+
}
|
|
934
|
+
if (foreignKeyProps.at(0) !== undefined) {
|
|
935
|
+
const foreignKey = foreignKeyProps[0];
|
|
936
|
+
// Add a one to one decorator
|
|
937
|
+
prop.addDecorator({
|
|
938
|
+
name: PropertyDecoratorKind.OneToOne,
|
|
939
|
+
arguments: [`(_m: any) => m.${foreignKey.getName()}`],
|
|
940
|
+
});
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
804
944
|
}
|
|
805
|
-
return Either.right(result);
|
|
806
|
-
}
|
|
807
945
|
}
|
|
808
946
|
function err(code, fn) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
947
|
+
let e = new ExtractorError(code);
|
|
948
|
+
if (fn) {
|
|
949
|
+
fn(e);
|
|
950
|
+
}
|
|
951
|
+
return Either.left(e);
|
|
814
952
|
}
|
|
815
953
|
function getDecoratorName(decorator) {
|
|
816
|
-
|
|
817
|
-
|
|
954
|
+
const name = decorator.getName() ?? decorator.getExpression().getText();
|
|
955
|
+
return String(name).replace(/\(.*\)$/, "");
|
|
818
956
|
}
|
|
819
957
|
function getDecoratorArgument(decorator, index) {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
958
|
+
const args = decorator.getArguments();
|
|
959
|
+
if (!args[index])
|
|
960
|
+
return undefined;
|
|
961
|
+
const arg = args[index];
|
|
962
|
+
if (arg.getKind?.() === SyntaxKind.Identifier) {
|
|
963
|
+
return arg.getText();
|
|
964
|
+
}
|
|
965
|
+
return arg.getLiteralValue();
|
|
827
966
|
}
|
|
828
967
|
function getRootType(t) {
|
|
829
|
-
|
|
968
|
+
if (typeof t === "string") {
|
|
969
|
+
return t;
|
|
970
|
+
}
|
|
971
|
+
if ("Nullable" in t) {
|
|
972
|
+
return getRootType(t.Nullable);
|
|
973
|
+
}
|
|
974
|
+
if ("Array" in t) {
|
|
975
|
+
return getRootType(t.Array);
|
|
976
|
+
}
|
|
977
|
+
if ("HttpResult" in t) {
|
|
978
|
+
return getRootType(t.HttpResult);
|
|
979
|
+
}
|
|
830
980
|
return t;
|
|
831
|
-
}
|
|
832
|
-
if ("Nullable" in t) {
|
|
833
|
-
return getRootType(t.Nullable);
|
|
834
|
-
}
|
|
835
|
-
if ("Array" in t) {
|
|
836
|
-
return getRootType(t.Array);
|
|
837
|
-
}
|
|
838
|
-
if ("HttpResult" in t) {
|
|
839
|
-
return getRootType(t.HttpResult);
|
|
840
|
-
}
|
|
841
|
-
return t;
|
|
842
981
|
}
|
|
843
982
|
function getObjectName(t) {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
983
|
+
const root = getRootType(t);
|
|
984
|
+
if (typeof root !== "string" && "Object" in root) {
|
|
985
|
+
return root["Object"];
|
|
986
|
+
}
|
|
987
|
+
if (typeof root !== "string" && "Partial" in root) {
|
|
988
|
+
return root["Partial"];
|
|
989
|
+
}
|
|
990
|
+
return undefined;
|
|
849
991
|
}
|
|
850
|
-
function
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
992
|
+
function parseIncludeTree(objLiteral) {
|
|
993
|
+
const result = {};
|
|
994
|
+
objLiteral.getProperties().forEach((prop) => {
|
|
995
|
+
if (prop.isKind(SyntaxKind.PropertyAssignment)) {
|
|
996
|
+
const name = prop.getName();
|
|
997
|
+
const init = prop.getInitializer();
|
|
998
|
+
// Check if it's a nested object literal
|
|
999
|
+
if (init?.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
1000
|
+
result[name] = parseIncludeTree(init); // Recurse
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
result[name] = {}; // Empty object by default
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
return result;
|
|
1008
|
+
}
|
|
1009
|
+
function checkPropertyModifier(prop) {
|
|
1010
|
+
// Error: properties must be just 'public'
|
|
1011
|
+
if (prop.getScope() != Scope.Public || prop.isReadonly() || prop.isStatic()) {
|
|
1012
|
+
return err(ExtractorErrorCode.InvalidPropertyModifier, (e) => {
|
|
1013
|
+
e.context = prop.getName();
|
|
1014
|
+
e.snippet = prop.getText();
|
|
1015
|
+
});
|
|
860
1016
|
}
|
|
861
|
-
|
|
1017
|
+
return Either.right(null);
|
|
862
1018
|
}
|
|
863
|
-
function
|
|
864
|
-
|
|
865
|
-
return exactMatch;
|
|
1019
|
+
function normalizeName(name) {
|
|
1020
|
+
return name.toLowerCase().replace(/_/g, "");
|
|
866
1021
|
}
|
|
867
|
-
function
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
1022
|
+
function getSelectorPropertyName(decorator) {
|
|
1023
|
+
const call = decorator.getCallExpression();
|
|
1024
|
+
const selector = call?.getArguments()[0];
|
|
1025
|
+
if (!selector?.isKind(SyntaxKind.ArrowFunction)) {
|
|
1026
|
+
return err(ExtractorErrorCode.InvalidSelectorSyntax);
|
|
1027
|
+
}
|
|
1028
|
+
const body = selector.getBody();
|
|
1029
|
+
if (!body.isKind(SyntaxKind.PropertyAccessExpression)) {
|
|
1030
|
+
return err(ExtractorErrorCode.InvalidSelectorSyntax);
|
|
1031
|
+
}
|
|
1032
|
+
return Either.right(body.getName());
|
|
872
1033
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1034
|
+
/**
|
|
1035
|
+
* Unwraps nullable types from a union type,
|
|
1036
|
+
* e.g. `T | null | undefined` becomes `T`.
|
|
1037
|
+
* @param ty Type to unwrap
|
|
1038
|
+
* @returns A tuple containing the unwrapped type and a boolean indicating if it was nullable.
|
|
1039
|
+
*/
|
|
1040
|
+
function unwrapNullable(ty) {
|
|
1041
|
+
if (!ty.isUnion())
|
|
1042
|
+
return [ty, false];
|
|
1043
|
+
const unions = ty.getUnionTypes();
|
|
1044
|
+
const nonNulls = unions.filter((t) => !t.isNull());
|
|
1045
|
+
const hasNullable = nonNulls.length < unions.length;
|
|
1046
|
+
// Booleans seperate into [null, true, false] from the `getUnionTypes` call
|
|
1047
|
+
if (nonNulls.length === 2 && nonNulls.every((t) => t.isBooleanLiteral())) {
|
|
1048
|
+
return [nonNulls[0].getApparentType(), hasNullable];
|
|
1049
|
+
}
|
|
1050
|
+
const stripUndefined = nonNulls.filter((t) => !t.isUndefined());
|
|
1051
|
+
return [stripUndefined[0] ?? ty, hasNullable];
|
|
882
1052
|
}
|