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