cloesce 0.0.5-unstable.1 → 0.0.5-unstable.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,882 +1,820 @@
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
5
  import { Either } from "../ui/common.js";
6
6
  var AttributeDecoratorKind;
7
7
  (function (AttributeDecoratorKind) {
8
- AttributeDecoratorKind["PrimaryKey"] = "PrimaryKey";
9
- AttributeDecoratorKind["ForeignKey"] = "ForeignKey";
10
- AttributeDecoratorKind["OneToOne"] = "OneToOne";
11
- AttributeDecoratorKind["OneToMany"] = "OneToMany";
12
- AttributeDecoratorKind["ManyToMany"] = "ManyToMany";
13
- AttributeDecoratorKind["DataSource"] = "DataSource";
8
+ AttributeDecoratorKind["PrimaryKey"] = "PrimaryKey";
9
+ AttributeDecoratorKind["ForeignKey"] = "ForeignKey";
10
+ AttributeDecoratorKind["OneToOne"] = "OneToOne";
11
+ AttributeDecoratorKind["OneToMany"] = "OneToMany";
12
+ AttributeDecoratorKind["ManyToMany"] = "ManyToMany";
13
+ AttributeDecoratorKind["DataSource"] = "DataSource";
14
14
  })(AttributeDecoratorKind || (AttributeDecoratorKind = {}));
15
15
  var ClassDecoratorKind;
16
16
  (function (ClassDecoratorKind) {
17
- ClassDecoratorKind["D1"] = "D1";
18
- ClassDecoratorKind["WranglerEnv"] = "WranglerEnv";
19
- ClassDecoratorKind["PlainOldObject"] = "PlainOldObject";
20
- ClassDecoratorKind["Service"] = "Service";
21
- ClassDecoratorKind["CRUD"] = "CRUD";
17
+ ClassDecoratorKind["D1"] = "D1";
18
+ ClassDecoratorKind["WranglerEnv"] = "WranglerEnv";
19
+ ClassDecoratorKind["PlainOldObject"] = "PlainOldObject";
20
+ ClassDecoratorKind["Service"] = "Service";
21
+ ClassDecoratorKind["CRUD"] = "CRUD";
22
22
  })(ClassDecoratorKind || (ClassDecoratorKind = {}));
23
23
  var ParameterDecoratorKind;
24
24
  (function (ParameterDecoratorKind) {
25
- ParameterDecoratorKind["Inject"] = "Inject";
25
+ ParameterDecoratorKind["Inject"] = "Inject";
26
26
  })(ParameterDecoratorKind || (ParameterDecoratorKind = {}));
27
27
  export class CidlExtractor {
28
- projectName;
29
- version;
30
- constructor(projectName, version) {
31
- this.projectName = projectName;
32
- this.version = version;
33
- }
34
- extract(project) {
35
- const models = {};
36
- const poos = {};
37
- const wranglerEnvs = [];
38
- const services = {};
39
- let app_source = null;
40
- for (const sourceFile of project.getSourceFiles()) {
41
- if (
42
- sourceFile.getBaseName() === "app.cloesce.ts" ||
43
- sourceFile.getBaseName() === "seed__app.cloesce.ts" // hardcoding for tests
44
- ) {
45
- const app = CidlExtractor.app(sourceFile);
46
- if (app.isLeft()) {
47
- return app;
48
- }
49
- app_source = app.unwrap();
50
- }
51
- for (const classDecl of sourceFile.getClasses()) {
52
- const notExportedErr = err(ExtractorErrorCode.MissingExport, (e) => {
53
- e.context = classDecl.getName();
54
- e.snippet = classDecl.getText();
55
- });
56
- if (hasDecorator(classDecl, ClassDecoratorKind.D1)) {
57
- if (!classDecl.isExported()) return notExportedErr;
58
- const result = CidlExtractor.model(classDecl, sourceFile);
59
- // Error: propogate from models
60
- if (result.isLeft()) {
61
- result.value.addContext((prev) => `${classDecl.getName()}.${prev}`);
62
- return result;
63
- }
64
- const model = result.unwrap();
65
- models[model.name] = model;
66
- continue;
67
- }
68
- if (hasDecorator(classDecl, ClassDecoratorKind.Service)) {
69
- if (!classDecl.isExported()) return notExportedErr;
70
- const result = CidlExtractor.service(classDecl, sourceFile);
71
- // Error: propogate from service
72
- if (result.isLeft()) {
73
- result.value.addContext((prev) => `${classDecl.getName()}.${prev}`);
74
- return result;
75
- }
76
- const service = result.unwrap();
77
- services[service.name] = service;
78
- continue;
79
- }
80
- if (hasDecorator(classDecl, ClassDecoratorKind.PlainOldObject)) {
81
- if (!classDecl.isExported()) return notExportedErr;
82
- const result = CidlExtractor.poo(classDecl, sourceFile);
83
- // Error: propogate from models
84
- if (result.isLeft()) {
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
+ projectName;
29
+ version;
30
+ constructor(projectName, version) {
31
+ this.projectName = projectName;
32
+ this.version = version;
33
+ }
34
+ extract(project) {
35
+ const models = {};
36
+ const poos = {};
37
+ const wranglerEnvs = [];
38
+ const services = {};
39
+ let app_source = null;
40
+ for (const sourceFile of project.getSourceFiles()) {
41
+ if (sourceFile.getBaseName() === "app.cloesce.ts" ||
42
+ sourceFile.getBaseName() === "seed__app.cloesce.ts" // hardcoding for tests
43
+ ) {
44
+ const app = CidlExtractor.app(sourceFile);
45
+ if (app.isLeft()) {
46
+ return app;
47
+ }
48
+ app_source = app.unwrap();
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
+ if (hasDecorator(classDecl, ClassDecoratorKind.D1)) {
56
+ if (!classDecl.isExported())
57
+ return notExportedErr;
58
+ const result = CidlExtractor.model(classDecl, sourceFile);
59
+ // Error: propogate from models
60
+ if (result.isLeft()) {
61
+ result.value.addContext((prev) => `${classDecl.getName()}.${prev}`);
62
+ return result;
63
+ }
64
+ const model = result.unwrap();
65
+ models[model.name] = model;
66
+ continue;
67
+ }
68
+ if (hasDecorator(classDecl, ClassDecoratorKind.Service)) {
69
+ if (!classDecl.isExported())
70
+ return notExportedErr;
71
+ const result = CidlExtractor.service(classDecl, sourceFile);
72
+ // Error: propogate from service
73
+ if (result.isLeft()) {
74
+ result.value.addContext((prev) => `${classDecl.getName()}.${prev}`);
75
+ return result;
76
+ }
77
+ const service = result.unwrap();
78
+ services[service.name] = service;
79
+ continue;
80
+ }
81
+ if (hasDecorator(classDecl, ClassDecoratorKind.PlainOldObject)) {
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;
99
+ }
100
+ }
101
+ const result = CidlExtractor.env(classDecl, sourceFile);
102
+ if (result.isLeft()) {
103
+ return result;
104
+ }
105
+ wranglerEnvs.push(result.unwrap());
106
+ }
97
107
  }
98
- }
99
- const result = CidlExtractor.env(classDecl, sourceFile);
100
- if (result.isLeft()) {
101
- return result;
102
- }
103
- wranglerEnvs.push(result.unwrap());
104
108
  }
105
- }
106
- }
107
- // Error: Only one wrangler environment can exist
108
- if (wranglerEnvs.length > 1) {
109
- return err(
110
- ExtractorErrorCode.TooManyWranglerEnvs,
111
- (e) => (e.context = wranglerEnvs.map((w) => w.name).toString()),
112
- );
113
- }
114
- return Either.right({
115
- version: this.version,
116
- project_name: this.projectName,
117
- language: "TypeScript",
118
- wrangler_env: wranglerEnvs[0],
119
- models,
120
- poos,
121
- services,
122
- app_source,
123
- });
124
- }
125
- static app(sourceFile) {
126
- const symbol = sourceFile.getDefaultExportSymbol();
127
- const decl = symbol?.getDeclarations()[0];
128
- if (!decl) {
129
- return err(ExtractorErrorCode.AppMissingDefaultExport);
130
- }
131
- const getTypeText = () => {
132
- let type = undefined;
133
- if (MorphNode.isExportAssignment(decl)) {
134
- type = decl.getExpression()?.getType();
135
- }
136
- if (MorphNode.isVariableDeclaration(decl)) {
137
- type = decl.getInitializer()?.getType();
138
- }
139
- return type?.getText(
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);
109
+ // Error: Only one wrangler environment can exist
110
+ if (wranglerEnvs.length > 1) {
111
+ return err(ExtractorErrorCode.TooManyWranglerEnvs, (e) => (e.context = wranglerEnvs.map((w) => w.name).toString()));
112
+ }
113
+ return Either.right({
114
+ version: this.version,
115
+ project_name: this.projectName,
116
+ language: "TypeScript",
117
+ wrangler_env: wranglerEnvs[0],
118
+ models,
119
+ poos,
120
+ services,
121
+ app_source,
122
+ });
164
123
  }
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
- },
124
+ static app(sourceFile) {
125
+ const symbol = sourceFile.getDefaultExportSymbol();
126
+ const decl = symbol?.getDeclarations()[0];
127
+ if (!decl) {
128
+ return err(ExtractorErrorCode.AppMissingDefaultExport);
129
+ }
130
+ const getTypeText = () => {
131
+ let type = undefined;
132
+ if (MorphNode.isExportAssignment(decl)) {
133
+ type = decl.getExpression()?.getType();
134
+ }
135
+ if (MorphNode.isVariableDeclaration(decl)) {
136
+ type = decl.getInitializer()?.getType();
137
+ }
138
+ return type?.getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope);
139
+ };
140
+ const typeText = getTypeText();
141
+ if (typeText === "CloesceApp") {
142
+ return Either.right(sourceFile.getFilePath().toString());
143
+ }
144
+ return err(ExtractorErrorCode.AppMissingDefaultExport);
145
+ }
146
+ static model(classDecl, sourceFile) {
147
+ const name = classDecl.getName();
148
+ const attributes = [];
149
+ const navigation_properties = [];
150
+ const data_sources = {};
151
+ const methods = {};
152
+ const cruds = new Set();
153
+ let primary_key = undefined;
154
+ // Extract crud methods
155
+ const crudDecorator = classDecl
156
+ .getDecorators()
157
+ .find((d) => getDecoratorName(d) === ClassDecoratorKind.CRUD);
158
+ if (crudDecorator) {
159
+ setCrudKinds(crudDecorator, cruds);
160
+ }
161
+ // Iterate attribtutes
162
+ for (const prop of classDecl.getProperties()) {
163
+ const decorators = prop.getDecorators();
164
+ const typeRes = CidlExtractor.cidlType(prop.getType());
165
+ // Error: invalid property type
166
+ if (typeRes.isLeft()) {
167
+ typeRes.value.context = prop.getName();
168
+ typeRes.value.snippet = prop.getText();
169
+ return typeRes;
170
+ }
171
+ const checkModifierRes = checkAttributeModifier(prop);
172
+ // No decorators means this is a standard attribute
173
+ if (decorators.length === 0) {
174
+ // Error: invalid attribute modifier
175
+ if (checkModifierRes.isLeft()) {
176
+ return checkModifierRes;
177
+ }
178
+ const cidl_type = typeRes.unwrap();
179
+ attributes.push({
180
+ foreign_key_reference: null,
181
+ value: {
182
+ name: prop.getName(),
183
+ cidl_type,
184
+ },
185
+ });
186
+ continue;
187
+ }
188
+ // TODO: Limiting to one decorator. Can't get too fancy on us.
189
+ const decorator = decorators[0];
190
+ const decoratorName = getDecoratorName(decorator);
191
+ // Error: invalid attribute modifier
192
+ if (checkModifierRes.isLeft() &&
193
+ decoratorName !== AttributeDecoratorKind.DataSource) {
194
+ return checkModifierRes;
195
+ }
196
+ // Process decorator
197
+ const cidl_type = typeRes.unwrap();
198
+ switch (decoratorName) {
199
+ case AttributeDecoratorKind.PrimaryKey: {
200
+ primary_key = {
201
+ name: prop.getName(),
202
+ cidl_type,
203
+ };
204
+ break;
205
+ }
206
+ case AttributeDecoratorKind.ForeignKey: {
207
+ attributes.push({
208
+ foreign_key_reference: getDecoratorArgument(decorator, 0) ?? null,
209
+ value: {
210
+ name: prop.getName(),
211
+ cidl_type,
212
+ },
213
+ });
214
+ break;
215
+ }
216
+ case AttributeDecoratorKind.OneToOne: {
217
+ const reference = getDecoratorArgument(decorator, 0);
218
+ // Error: One to one navigation properties requre a reference
219
+ if (!reference) {
220
+ return err(ExtractorErrorCode.MissingNavigationPropertyReference, (e) => {
221
+ e.snippet = prop.getText();
222
+ e.context = prop.getName();
223
+ });
224
+ }
225
+ let model_name = getObjectName(cidl_type);
226
+ // Error: navigation properties require a model reference
227
+ if (!model_name) {
228
+ return err(ExtractorErrorCode.MissingNavigationPropertyReference, (e) => {
229
+ e.snippet = prop.getText();
230
+ e.context = prop.getName();
231
+ });
232
+ }
233
+ navigation_properties.push({
234
+ var_name: prop.getName(),
235
+ model_name,
236
+ kind: { OneToOne: { reference } },
237
+ });
238
+ break;
239
+ }
240
+ case AttributeDecoratorKind.OneToMany: {
241
+ const reference = getDecoratorArgument(decorator, 0);
242
+ // Error: One to one navigation properties requre a reference
243
+ if (!reference) {
244
+ return err(ExtractorErrorCode.MissingNavigationPropertyReference, (e) => {
245
+ e.snippet = prop.getText();
246
+ e.context = prop.getName();
247
+ });
248
+ }
249
+ let model_name = getObjectName(cidl_type);
250
+ // Error: navigation properties require a model reference
251
+ if (!model_name) {
252
+ return err(ExtractorErrorCode.MissingNavigationPropertyReference, (e) => {
253
+ e.snippet = prop.getText();
254
+ e.context = prop.getName();
255
+ });
256
+ }
257
+ navigation_properties.push({
258
+ var_name: prop.getName(),
259
+ model_name,
260
+ kind: { OneToMany: { reference } },
261
+ });
262
+ break;
263
+ }
264
+ case AttributeDecoratorKind.ManyToMany: {
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
+ });
272
+ // Error: navigation properties require a model reference
273
+ let model_name = getObjectName(cidl_type);
274
+ if (!model_name) {
275
+ return err(ExtractorErrorCode.MissingNavigationPropertyReference, (e) => {
276
+ e.snippet = prop.getText();
277
+ e.context = prop.getName();
278
+ });
279
+ }
280
+ navigation_properties.push({
281
+ var_name: prop.getName(),
282
+ model_name,
283
+ kind: { ManyToMany: { unique_id } },
284
+ });
285
+ break;
286
+ }
287
+ case AttributeDecoratorKind.DataSource: {
288
+ const isIncludeTree = prop
289
+ .getType()
290
+ .getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope) === `IncludeTree<${name}>`;
291
+ // Error: data sources must be static include trees
292
+ if (!prop.isStatic() || !isIncludeTree) {
293
+ return err(ExtractorErrorCode.InvalidDataSourceDefinition, (e) => {
294
+ e.snippet = prop.getText();
295
+ e.context = prop.getName();
296
+ });
297
+ }
298
+ const initializer = prop.getInitializer();
299
+ const treeRes = CidlExtractor.includeTree(initializer, classDecl, sourceFile);
300
+ if (treeRes.isLeft()) {
301
+ treeRes.value.addContext((prev) => `${prop.getName()} ${prev}`);
302
+ treeRes.value.snippet = prop.getText();
303
+ return treeRes;
304
+ }
305
+ data_sources[prop.getName()] = {
306
+ name: prop.getName(),
307
+ tree: treeRes.unwrap(),
308
+ };
309
+ break;
310
+ }
311
+ }
312
+ }
313
+ if (primary_key == undefined) {
314
+ return err(ExtractorErrorCode.MissingPrimaryKey, (e) => {
315
+ e.snippet = classDecl.getText();
316
+ });
317
+ }
318
+ // Process methods
319
+ for (const m of classDecl.getMethods()) {
320
+ const httpVerb = m
321
+ .getDecorators()
322
+ .map((d) => getDecoratorName(d))
323
+ .find((name) => Object.values(HttpVerb).includes(name));
324
+ if (!httpVerb) {
325
+ continue;
326
+ }
327
+ const result = CidlExtractor.modelMethod(name, m, httpVerb);
328
+ if (result.isLeft()) {
329
+ result.value.addContext((prev) => `${m.getName()} ${prev}`);
330
+ return result;
331
+ }
332
+ methods[result.unwrap().name] = result.unwrap();
333
+ }
334
+ return Either.right({
335
+ name,
336
+ attributes,
337
+ primary_key,
338
+ navigation_properties,
339
+ methods,
340
+ data_sources,
341
+ cruds: Array.from(cruds).sort(),
342
+ source_path: sourceFile.getFilePath().toString(),
189
343
  });
190
- continue;
191
- }
192
- // TODO: Limiting to one decorator. Can't get too fancy on us.
193
- const decorator = decorators[0];
194
- const decoratorName = getDecoratorName(decorator);
195
- // Error: invalid attribute modifier
196
- if (
197
- checkModifierRes.isLeft() &&
198
- decoratorName !== AttributeDecoratorKind.DataSource
199
- ) {
200
- return checkModifierRes;
201
- }
202
- // Process decorator
203
- const cidl_type = typeRes.unwrap();
204
- switch (decoratorName) {
205
- case AttributeDecoratorKind.PrimaryKey: {
206
- primary_key = {
207
- name: prop.getName(),
208
- cidl_type,
209
- };
210
- break;
211
- }
212
- case AttributeDecoratorKind.ForeignKey: {
213
- attributes.push({
214
- foreign_key_reference: getDecoratorArgument(decorator, 0) ?? null,
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();
344
+ }
345
+ static modelMethod(modelName, method, verb) {
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();
289
351
  });
290
- // Error: navigation properties require a model reference
291
- let model_name = getObjectName(cidl_type);
292
- if (!model_name) {
293
- return err(
294
- ExtractorErrorCode.MissingNavigationPropertyReference,
295
- (e) => {
296
- e.snippet = prop.getText();
297
- e.context = prop.getName();
298
- },
299
- );
300
- }
301
- navigation_properties.push({
302
- var_name: prop.getName(),
303
- model_name,
304
- kind: { ManyToMany: { unique_id } },
305
- });
306
- break;
307
- }
308
- case AttributeDecoratorKind.DataSource: {
309
- const isIncludeTree =
310
- prop
311
- .getType()
312
- .getText(
313
- undefined,
314
- TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
315
- ) === `IncludeTree<${name}>`;
316
- // Error: data sources must be static include trees
317
- if (!prop.isStatic() || !isIncludeTree) {
318
- return err(ExtractorErrorCode.InvalidDataSourceDefinition, (e) => {
319
- e.snippet = prop.getText();
320
- e.context = prop.getName();
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(),
321
385
  });
322
- }
323
- const initializer = prop.getInitializer();
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
- });
381
- }
382
- let needsDataSource = !method.isStatic();
383
- const parameters = [];
384
- for (const param of method.getParameters()) {
385
- // Handle injected param
386
- if (param.getDecorator(ParameterDecoratorKind.Inject)) {
387
- const typeRes = CidlExtractor.cidlType(param.getType(), true);
386
+ }
387
+ const typeRes = CidlExtractor.cidlType(method.getReturnType());
388
388
  // Error: invalid type
389
389
  if (typeRes.isLeft()) {
390
- typeRes.value.snippet = method.getText();
391
- typeRes.value.context = param.getName();
392
- return typeRes;
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
+ });
393
399
  }
394
- parameters.push({
395
- name: param.getName(),
396
- cidl_type: typeRes.unwrap(),
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,
397
408
  });
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
409
  }
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
- }
429
- return Either.right({
430
- name: method.getName(),
431
- is_static: method.isStatic(),
432
- http_verb: verb,
433
- return_media: defaultMediaType(),
434
- return_type: typeRes.unwrap(),
435
- parameters_media: defaultMediaType(),
436
- parameters,
437
- });
438
- }
439
- static service(classDecl, sourceFile) {
440
- const attributes = [];
441
- const methods = {};
442
- // Attributes
443
- for (const prop of classDecl.getProperties()) {
444
- const typeRes = CidlExtractor.cidlType(prop.getType(), true);
445
- // Error: invalid property type
446
- if (typeRes.isLeft()) {
447
- typeRes.value.context = prop.getName();
448
- typeRes.value.snippet = prop.getText();
449
- return typeRes;
450
- }
451
- if (typeof typeRes.value === "string" || !("Inject" in typeRes.value)) {
452
- return err(ExtractorErrorCode.InvalidServiceAttribute, (e) => {
453
- e.context = prop.getName();
454
- e.snippet = prop.getText();
410
+ static service(classDecl, sourceFile) {
411
+ const attributes = [];
412
+ const methods = {};
413
+ // Attributes
414
+ for (const prop of classDecl.getProperties()) {
415
+ const typeRes = CidlExtractor.cidlType(prop.getType(), true);
416
+ // Error: invalid property type
417
+ if (typeRes.isLeft()) {
418
+ typeRes.value.context = prop.getName();
419
+ typeRes.value.snippet = prop.getText();
420
+ return typeRes;
421
+ }
422
+ if (typeof typeRes.value === "string" || !("Inject" in typeRes.value)) {
423
+ return err(ExtractorErrorCode.InvalidServiceAttribute, (e) => {
424
+ e.context = prop.getName();
425
+ e.snippet = prop.getText();
426
+ });
427
+ }
428
+ const checkModifierRes = checkAttributeModifier(prop);
429
+ if (checkModifierRes.isLeft()) {
430
+ return checkModifierRes;
431
+ }
432
+ attributes.push({
433
+ var_name: prop.getName(),
434
+ injected: typeRes.value.Inject,
435
+ });
436
+ }
437
+ // Methods
438
+ for (const m of classDecl.getMethods()) {
439
+ const httpVerb = m
440
+ .getDecorators()
441
+ .map((d) => getDecoratorName(d))
442
+ .find((name) => Object.values(HttpVerb).includes(name));
443
+ if (!httpVerb) {
444
+ continue;
445
+ }
446
+ const res = CidlExtractor.serviceMethod(m, httpVerb);
447
+ if (res.isLeft()) {
448
+ return res;
449
+ }
450
+ const serviceMethod = res.unwrap();
451
+ methods[serviceMethod.name] = serviceMethod;
452
+ }
453
+ return Either.right({
454
+ name: classDecl.getName(),
455
+ attributes,
456
+ methods,
457
+ source_path: sourceFile.getFilePath().toString(),
455
458
  });
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
459
  }
482
- return Either.right({
483
- name: classDecl.getName(),
484
- attributes,
485
- methods,
486
- source_path: sourceFile.getFilePath().toString(),
487
- });
488
- }
489
- static serviceMethod(method, verb) {
490
- // Error: invalid method scope, must be public
491
- if (method.getScope() != Scope.Public) {
492
- return err(ExtractorErrorCode.InvalidApiMethodModifier, (e) => {
493
- e.context = method.getName();
494
- e.snippet = method.getText();
495
- });
496
- }
497
- const parameters = [];
498
- for (const param of method.getParameters()) {
499
- // Handle injected param
500
- if (param.getDecorator(ParameterDecoratorKind.Inject)) {
501
- const typeRes = CidlExtractor.cidlType(param.getType(), true);
460
+ static serviceMethod(method, verb) {
461
+ // Error: invalid method scope, must be public
462
+ if (method.getScope() != Scope.Public) {
463
+ return err(ExtractorErrorCode.InvalidApiMethodModifier, (e) => {
464
+ e.context = method.getName();
465
+ e.snippet = method.getText();
466
+ });
467
+ }
468
+ const parameters = [];
469
+ for (const param of method.getParameters()) {
470
+ // Handle injected param
471
+ if (param.getDecorator(ParameterDecoratorKind.Inject)) {
472
+ const typeRes = CidlExtractor.cidlType(param.getType(), true);
473
+ // Error: invalid type
474
+ if (typeRes.isLeft()) {
475
+ typeRes.value.snippet = method.getText();
476
+ typeRes.value.context = param.getName();
477
+ return typeRes;
478
+ }
479
+ parameters.push({
480
+ name: param.getName(),
481
+ cidl_type: typeRes.unwrap(),
482
+ });
483
+ continue;
484
+ }
485
+ // Handle all other params
486
+ const typeRes = CidlExtractor.cidlType(param.getType());
487
+ // Error: invalid type
488
+ if (typeRes.isLeft()) {
489
+ typeRes.value.snippet = method.getText();
490
+ typeRes.value.context = param.getName();
491
+ return typeRes;
492
+ }
493
+ parameters.push({
494
+ name: param.getName(),
495
+ cidl_type: typeRes.unwrap(),
496
+ });
497
+ }
498
+ const typeRes = CidlExtractor.cidlType(method.getReturnType());
502
499
  // Error: invalid type
503
500
  if (typeRes.isLeft()) {
504
- typeRes.value.snippet = method.getText();
505
- typeRes.value.context = param.getName();
506
- return typeRes;
501
+ typeRes.value.snippet = method.getText();
502
+ return typeRes;
507
503
  }
508
- parameters.push({
509
- name: param.getName(),
510
- cidl_type: typeRes.unwrap(),
504
+ return Either.right({
505
+ name: method.getName(),
506
+ http_verb: verb,
507
+ is_static: method.isStatic(),
508
+ return_media: defaultMediaType(),
509
+ return_type: typeRes.unwrap(),
510
+ parameters_media: defaultMediaType(),
511
+ parameters,
511
512
  });
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
513
  }
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
- }
696
- if (symbolName === "Promise" || aliasName === "IncludeTree") {
697
- // Unwrap promises
698
- return wrapGeneric(genericTy, nullable, (inner) => inner);
699
- }
700
- if (unwrappedType.isArray()) {
701
- return wrapGeneric(genericTy, nullable, (inner) => ({ Array: inner }));
702
- }
703
- if (symbolName === "HttpResult") {
704
- return wrapGeneric(genericTy, nullable, (inner) => ({
705
- HttpResult: inner,
706
- }));
707
- }
708
- // Error: unknown type
709
- return err(ExtractorErrorCode.UnknownType);
710
- function wrapNullable(inner, isNullable) {
711
- if (isNullable) {
712
- return { Nullable: inner };
713
- } else {
714
- return inner;
715
- }
716
- }
717
- function wrapGeneric(t, isNullable, wrapper) {
718
- const res = CidlExtractor.cidlType(t, inject);
719
- // Error: propogated from `cidlType`
720
- return res.map((inner) => wrapNullable(wrapper(inner), isNullable));
514
+ static poo(classDecl, sourceFile) {
515
+ const name = classDecl.getName();
516
+ const attributes = [];
517
+ for (const prop of classDecl.getProperties()) {
518
+ const typeRes = CidlExtractor.cidlType(prop.getType());
519
+ // Error: invalid property type
520
+ if (typeRes.isLeft()) {
521
+ typeRes.value.context = prop.getName();
522
+ typeRes.value.snippet = prop.getText();
523
+ return typeRes;
524
+ }
525
+ // Error: invalid attribute modifier
526
+ const modifierRes = checkAttributeModifier(prop);
527
+ if (modifierRes.isLeft()) {
528
+ return modifierRes;
529
+ }
530
+ const cidl_type = typeRes.unwrap();
531
+ attributes.push({
532
+ name: prop.getName(),
533
+ cidl_type,
534
+ });
535
+ continue;
536
+ }
537
+ return Either.right({
538
+ name,
539
+ attributes,
540
+ source_path: sourceFile.getFilePath().toString(),
541
+ });
721
542
  }
722
- function unwrapNullable(ty) {
723
- if (!ty.isUnion()) return [ty, false];
724
- const unions = ty.getUnionTypes();
725
- const nonNulls = unions.filter((t) => !t.isNull() && !t.isUndefined());
726
- const hasNullable = nonNulls.length < unions.length;
727
- // Booleans seperate into [null, true, false] from the `getUnionTypes` call
728
- if (
729
- nonNulls.length === 2 &&
730
- nonNulls.every((t) => t.isBooleanLiteral())
731
- ) {
732
- return [nonNulls[0].getApparentType(), hasNullable];
733
- }
734
- return [nonNulls[0] ?? ty, hasNullable];
543
+ static env(classDecl, sourceFile) {
544
+ const vars = {};
545
+ let binding;
546
+ for (const prop of classDecl.getProperties()) {
547
+ if (prop
548
+ .getType()
549
+ .getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope) === "D1Database") {
550
+ binding = prop.getName();
551
+ continue;
552
+ }
553
+ const ty = CidlExtractor.cidlType(prop.getType());
554
+ if (ty.isLeft()) {
555
+ ty.value.context = prop.getName();
556
+ ty.value.snippet = prop.getText();
557
+ return ty;
558
+ }
559
+ vars[prop.getName()] = ty.unwrap();
560
+ }
561
+ if (!binding) {
562
+ return err(ExtractorErrorCode.MissingDatabaseBinding);
563
+ }
564
+ return Either.right({
565
+ name: classDecl.getName(),
566
+ source_path: sourceFile.getFilePath().toString(),
567
+ db_binding: binding,
568
+ vars,
569
+ });
735
570
  }
736
- }
737
- static includeTree(expr, currentClass, sf) {
738
- // Include trees must be of the expected form
739
- if (
740
- !expr ||
741
- !expr.isKind ||
742
- !expr.isKind(SyntaxKind.ObjectLiteralExpression)
743
- ) {
744
- return err(ExtractorErrorCode.InvalidIncludeTree);
571
+ static primTypeMap = {
572
+ number: "Real",
573
+ Number: "Real",
574
+ Integer: "Integer",
575
+ string: "Text",
576
+ String: "Text",
577
+ boolean: "Boolean",
578
+ Boolean: "Boolean",
579
+ Date: "DateIso",
580
+ Uint8Array: "Blob",
581
+ Stream: "Stream",
582
+ };
583
+ static cidlType(type, inject = false) {
584
+ // Void
585
+ if (type.isVoid()) {
586
+ return Either.right("Void");
587
+ }
588
+ // Null
589
+ if (type.isNull()) {
590
+ return Either.right({ Nullable: "Void" });
591
+ }
592
+ // Nullable via union
593
+ const [unwrappedType, nullable] = unwrapNullable(type);
594
+ const tyText = unwrappedType
595
+ .getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope)
596
+ .split("|")[0]
597
+ .trim();
598
+ // Primitives
599
+ const prim = this.primTypeMap[tyText];
600
+ if (prim) {
601
+ return Either.right(wrapNullable(prim, nullable));
602
+ }
603
+ const generics = [
604
+ ...unwrappedType.getAliasTypeArguments(),
605
+ ...unwrappedType.getTypeArguments(),
606
+ ];
607
+ // Error: can't handle multiple generics
608
+ if (generics.length > 1) {
609
+ return err(ExtractorErrorCode.MultipleGenericType);
610
+ }
611
+ // No generics -> inject or object
612
+ if (generics.length === 0) {
613
+ const base = inject ? { Inject: tyText } : { Object: tyText };
614
+ return Either.right(wrapNullable(base, nullable));
615
+ }
616
+ // Single generic
617
+ const genericTy = generics[0];
618
+ const symbolName = unwrappedType.getSymbol()?.getName();
619
+ const aliasName = unwrappedType.getAliasSymbol()?.getName();
620
+ if (aliasName === "DataSourceOf") {
621
+ return Either.right(wrapNullable({
622
+ DataSource: genericTy.getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope),
623
+ }, nullable));
624
+ }
625
+ if (aliasName === "DeepPartial") {
626
+ const [_, genericTyNullable] = unwrapNullable(genericTy);
627
+ const genericTyGenerics = [
628
+ ...genericTy.getAliasTypeArguments(),
629
+ ...genericTy.getTypeArguments(),
630
+ ];
631
+ // Expect partials to be of the exact form DeepPartial<Model>
632
+ if (genericTyNullable ||
633
+ genericTy.isUnion() ||
634
+ genericTyGenerics.length > 0) {
635
+ return err(ExtractorErrorCode.InvalidPartialType);
636
+ }
637
+ return Either.right(wrapNullable({
638
+ Partial: genericTy
639
+ .getText(undefined, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope)
640
+ .split("|")[0]
641
+ .trim(),
642
+ }, nullable));
643
+ }
644
+ if (symbolName === "Promise" || aliasName === "IncludeTree") {
645
+ // Unwrap promises
646
+ return wrapGeneric(genericTy, nullable, (inner) => inner);
647
+ }
648
+ if (unwrappedType.isArray()) {
649
+ return wrapGeneric(genericTy, nullable, (inner) => ({ Array: inner }));
650
+ }
651
+ if (symbolName === "HttpResult") {
652
+ return wrapGeneric(genericTy, nullable, (inner) => ({
653
+ HttpResult: inner,
654
+ }));
655
+ }
656
+ // Error: unknown type
657
+ return err(ExtractorErrorCode.UnknownType);
658
+ function wrapNullable(inner, isNullable) {
659
+ if (isNullable) {
660
+ return { Nullable: inner };
661
+ }
662
+ else {
663
+ return inner;
664
+ }
665
+ }
666
+ function wrapGeneric(t, isNullable, wrapper) {
667
+ const res = CidlExtractor.cidlType(t, inject);
668
+ // Error: propogated from `cidlType`
669
+ return res.map((inner) => wrapNullable(wrapper(inner), isNullable));
670
+ }
671
+ function unwrapNullable(ty) {
672
+ if (!ty.isUnion())
673
+ return [ty, false];
674
+ const unions = ty.getUnionTypes();
675
+ const nonNulls = unions.filter((t) => !t.isNull() && !t.isUndefined());
676
+ const hasNullable = nonNulls.length < unions.length;
677
+ // Booleans seperate into [null, true, false] from the `getUnionTypes` call
678
+ if (nonNulls.length === 2 &&
679
+ nonNulls.every((t) => t.isBooleanLiteral())) {
680
+ return [nonNulls[0].getApparentType(), hasNullable];
681
+ }
682
+ return [nonNulls[0] ?? ty, hasNullable];
683
+ }
745
684
  }
746
- const result = {};
747
- for (const prop of expr.getProperties()) {
748
- if (!prop.isKind(SyntaxKind.PropertyAssignment)) continue;
749
- // Error: navigation property not found
750
- const navProp = findPropertyByName(currentClass, prop.getName());
751
- if (!navProp) {
752
- return err(
753
- ExtractorErrorCode.UnknownNavigationPropertyReference,
754
- (e) => {
755
- e.snippet = expr.getText();
756
- e.context = prop.getName();
757
- },
758
- );
759
- }
760
- const typeRes = CidlExtractor.cidlType(navProp.getType());
761
- // Error: invalid referenced nav prop type
762
- if (typeRes.isLeft()) {
763
- typeRes.value.snippet = navProp.getText();
764
- typeRes.value.context = prop.getName();
765
- return typeRes;
766
- }
767
- // Error: invalid referenced nav prop type
768
- const cidl_type = typeRes.unwrap();
769
- if (typeof cidl_type === "string") {
770
- return err(
771
- ExtractorErrorCode.InvalidNavigationPropertyReference,
772
- (e) => {
773
- e.snippet = navProp.getText();
774
- e.context = prop.getName();
775
- },
776
- );
777
- }
778
- // Recurse for nested includes
779
- const initializer = prop.getInitializer?.();
780
- let nestedTree = {};
781
- if (initializer?.isKind?.(SyntaxKind.ObjectLiteralExpression)) {
782
- const targetModel = getObjectName(cidl_type);
783
- const targetClass = currentClass
784
- .getSourceFile()
785
- .getProject()
786
- .getSourceFiles()
787
- .flatMap((f) => f.getClasses())
788
- .find((c) => c.getName() === targetModel);
789
- if (targetClass) {
790
- const treeRes = CidlExtractor.includeTree(
791
- initializer,
792
- targetClass,
793
- sf,
794
- );
795
- // Error: Propogated from `includeTree`
796
- if (treeRes.isLeft()) {
797
- treeRes.value.snippet = expr.getText();
798
- return treeRes;
799
- }
800
- nestedTree = treeRes.unwrap();
801
- }
802
- }
803
- result[navProp.getName()] = nestedTree;
685
+ static includeTree(expr, currentClass, sf) {
686
+ // Include trees must be of the expected form
687
+ if (!expr ||
688
+ !expr.isKind ||
689
+ !expr.isKind(SyntaxKind.ObjectLiteralExpression)) {
690
+ return err(ExtractorErrorCode.InvalidIncludeTree);
691
+ }
692
+ const result = {};
693
+ for (const prop of expr.getProperties()) {
694
+ if (!prop.isKind(SyntaxKind.PropertyAssignment))
695
+ continue;
696
+ // Error: navigation property not found
697
+ const navProp = findPropertyByName(currentClass, prop.getName());
698
+ if (!navProp) {
699
+ return err(ExtractorErrorCode.UnknownNavigationPropertyReference, (e) => {
700
+ e.snippet = expr.getText();
701
+ e.context = prop.getName();
702
+ });
703
+ }
704
+ const typeRes = CidlExtractor.cidlType(navProp.getType());
705
+ // Error: invalid referenced nav prop type
706
+ if (typeRes.isLeft()) {
707
+ typeRes.value.snippet = navProp.getText();
708
+ typeRes.value.context = prop.getName();
709
+ return typeRes;
710
+ }
711
+ // Error: invalid referenced nav prop type
712
+ const cidl_type = typeRes.unwrap();
713
+ if (typeof cidl_type === "string") {
714
+ return err(ExtractorErrorCode.InvalidNavigationPropertyReference, (e) => {
715
+ e.snippet = navProp.getText();
716
+ e.context = prop.getName();
717
+ });
718
+ }
719
+ // Recurse for nested includes
720
+ const initializer = prop.getInitializer?.();
721
+ let nestedTree = {};
722
+ if (initializer?.isKind?.(SyntaxKind.ObjectLiteralExpression)) {
723
+ const targetModel = getObjectName(cidl_type);
724
+ const targetClass = currentClass
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;
736
+ }
737
+ nestedTree = treeRes.unwrap();
738
+ }
739
+ }
740
+ result[navProp.getName()] = nestedTree;
741
+ }
742
+ return Either.right(result);
804
743
  }
805
- return Either.right(result);
806
- }
807
744
  }
808
745
  function err(code, fn) {
809
- let e = new ExtractorError(code);
810
- if (fn) {
811
- fn(e);
812
- }
813
- return Either.left(e);
746
+ let e = new ExtractorError(code);
747
+ if (fn) {
748
+ fn(e);
749
+ }
750
+ return Either.left(e);
814
751
  }
815
752
  function getDecoratorName(decorator) {
816
- const name = decorator.getName() ?? decorator.getExpression().getText();
817
- return String(name).replace(/\(.*\)$/, "");
753
+ const name = decorator.getName() ?? decorator.getExpression().getText();
754
+ return String(name).replace(/\(.*\)$/, "");
818
755
  }
819
756
  function getDecoratorArgument(decorator, index) {
820
- const args = decorator.getArguments();
821
- if (!args[index]) return undefined;
822
- const arg = args[index];
823
- if (arg.getKind?.() === SyntaxKind.Identifier) {
824
- return arg.getText();
825
- }
826
- return arg.getLiteralValue();
757
+ const args = decorator.getArguments();
758
+ if (!args[index])
759
+ return undefined;
760
+ const arg = args[index];
761
+ if (arg.getKind?.() === SyntaxKind.Identifier) {
762
+ return arg.getText();
763
+ }
764
+ return arg.getLiteralValue();
827
765
  }
828
766
  function getRootType(t) {
829
- if (typeof t === "string") {
767
+ if (typeof t === "string") {
768
+ return t;
769
+ }
770
+ if ("Nullable" in t) {
771
+ return getRootType(t.Nullable);
772
+ }
773
+ if ("Array" in t) {
774
+ return getRootType(t.Array);
775
+ }
776
+ if ("HttpResult" in t) {
777
+ return getRootType(t.HttpResult);
778
+ }
830
779
  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
780
  }
843
781
  function getObjectName(t) {
844
- const root = getRootType(t);
845
- if (typeof root !== "string" && "Object" in root) {
846
- return root["Object"];
847
- }
848
- return undefined;
782
+ const root = getRootType(t);
783
+ if (typeof root !== "string" && "Object" in root) {
784
+ return root["Object"];
785
+ }
786
+ return undefined;
849
787
  }
850
788
  function setCrudKinds(d, cruds) {
851
- const arg = d.getArguments()[0];
852
- if (!arg) {
853
- return;
854
- }
855
- if (MorphNode.isArrayLiteralExpression(arg)) {
856
- for (const a of arg.getElements()) {
857
- cruds.add(
858
- MorphNode.isStringLiteral(a) ? a.getLiteralValue() : a.getText(),
859
- );
789
+ const arg = d.getArguments()[0];
790
+ if (!arg) {
791
+ return;
792
+ }
793
+ if (MorphNode.isArrayLiteralExpression(arg)) {
794
+ for (const a of arg.getElements()) {
795
+ cruds.add((MorphNode.isStringLiteral(a)
796
+ ? a.getLiteralValue()
797
+ : a.getText()));
798
+ }
860
799
  }
861
- }
862
800
  }
863
801
  function findPropertyByName(cls, name) {
864
- const exactMatch = cls.getProperties().find((p) => p.getName() === name);
865
- return exactMatch;
802
+ const exactMatch = cls.getProperties().find((p) => p.getName() === name);
803
+ return exactMatch;
866
804
  }
867
805
  function hasDecorator(node, name) {
868
- return node.getDecorators().some((d) => {
869
- const decoratorName = getDecoratorName(d);
870
- return decoratorName === name || decoratorName.endsWith("." + name);
871
- });
806
+ return node.getDecorators().some((d) => {
807
+ const decoratorName = getDecoratorName(d);
808
+ return decoratorName === name || decoratorName.endsWith("." + name);
809
+ });
872
810
  }
873
811
  function checkAttributeModifier(prop) {
874
- // Error: attributes must be just 'public'
875
- if (prop.getScope() != Scope.Public || prop.isReadonly() || prop.isStatic()) {
876
- return err(ExtractorErrorCode.InvalidAttributeModifier, (e) => {
877
- e.context = prop.getName();
878
- e.snippet = prop.getText();
879
- });
880
- }
881
- return Either.right(null);
812
+ // Error: attributes must be just 'public'
813
+ if (prop.getScope() != Scope.Public || prop.isReadonly() || prop.isStatic()) {
814
+ return err(ExtractorErrorCode.InvalidAttributeModifier, (e) => {
815
+ e.context = prop.getName();
816
+ e.snippet = prop.getText();
817
+ });
818
+ }
819
+ return Either.right(null);
882
820
  }