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.
@@ -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 "../ui/common.js";
6
- var AttributeDecoratorKind;
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";
14
- })(AttributeDecoratorKind || (AttributeDecoratorKind = {}));
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["D1"] = "D1";
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
- projectName;
29
- version;
30
- constructor(projectName, version) {
31
- this.projectName = projectName;
32
- this.version = version;
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 models = {};
36
- const poos = {};
34
+ static extract(projectName, project) {
35
+ const modelDecls = new Map();
36
+ const serviceDecls = new Map();
37
37
  const wranglerEnvs = [];
38
- const services = {};
39
- let app_source = null;
38
+ let main_source = null;
39
+ // TODO: Concurrently across several threads?
40
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();
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
- 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;
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
- version: this.version,
115
- project_name: this.projectName,
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
- app_source,
121
+ main_source,
122
122
  });
123
123
  }
124
- static app(sourceFile) {
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 err(ExtractorErrorCode.AppMissingDefaultExport);
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
- const getTypeText = () => {
131
- let type = undefined;
132
- if (MorphNode.isExportAssignment(decl)) {
133
- type = decl.getExpression()?.getType();
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
- if (MorphNode.isVariableDeclaration(decl)) {
136
- type = decl.getInitializer()?.getType();
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 err(ExtractorErrorCode.AppMissingDefaultExport);
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
- static model(classDecl, sourceFile) {
180
+ model(classDecl, sourceFile, decorator) {
147
181
  const name = classDecl.getName();
148
- const attributes = [];
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 = undefined;
190
+ let primary_key = null;
154
191
  // Extract crud methods
155
- const crudDecorator = classDecl
156
- .getDecorators()
157
- .find((d) => getDecoratorName(d) === ClassDecoratorKind.CRUD);
158
- if (crudDecorator) {
159
- setCrudKinds(crudDecorator, cruds);
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 attribtutes
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 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;
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
- const cidl_type = typeRes.unwrap();
179
- attributes.push({
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 AttributeDecoratorKind.PrimaryKey: {
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 AttributeDecoratorKind.ForeignKey: {
207
- attributes.push({
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 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) => {
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
- let model_name = getObjectName(cidl_type);
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.MissingNavigationPropertyReference, (e) => {
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: { reference } },
295
+ model_reference: model_name,
296
+ kind: { OneToOne: { column_reference: selector.unwrap() } },
237
297
  });
238
298
  break;
239
299
  }
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) => {
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.MissingNavigationPropertyReference, (e) => {
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: { reference } },
318
+ model_reference: model_name,
319
+ kind: { OneToMany: { column_reference: selector.unwrap() } },
261
320
  });
262
321
  break;
263
322
  }
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
- });
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.MissingNavigationPropertyReference, (e) => {
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: { ManyToMany: { unique_id } },
334
+ model_reference: model_name,
335
+ kind: "ManyToMany",
284
336
  });
285
337
  break;
286
338
  }
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) => {
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
- 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;
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
- data_sources[prop.getName()] = {
306
- name: prop.getName(),
307
- tree: treeRes.unwrap(),
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((d) => getDecoratorName(d))
410
+ .map(getDecoratorName)
323
411
  .find((name) => Object.values(HttpVerb).includes(name));
324
412
  if (!httpVerb) {
325
413
  continue;
326
414
  }
327
- const result = CidlExtractor.modelMethod(name, m, httpVerb);
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
- attributes,
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
- 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();
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
- // Attributes
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.InvalidServiceAttribute, (e) => {
449
+ return err(ExtractorErrorCode.InvalidServiceProperty, (e) => {
424
450
  e.context = prop.getName();
425
451
  e.snippet = prop.getText();
426
452
  });
427
453
  }
428
- const checkModifierRes = checkAttributeModifier(prop);
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
- injected: typeRes.value.Inject,
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((d) => getDecoratorName(d))
468
+ .map(getDecoratorName)
442
469
  .find((name) => Object.values(HttpVerb).includes(name));
443
470
  if (!httpVerb) {
444
471
  continue;
445
472
  }
446
- const res = CidlExtractor.serviceMethod(m, httpVerb);
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
- static serviceMethod(method, verb) {
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
- static poo(classDecl, sourceFile) {
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
- return Either.right({
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 binding;
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
- binding = prop.getName();
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
- db_binding: binding,
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 === "Promise" || aliasName === "IncludeTree") {
645
- // Unwrap promises
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() && !t.isUndefined());
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
- return [nonNulls[0] ?? ty, hasNullable];
776
+ const stripUndefined = nonNulls.filter((t) => !t.isUndefined());
777
+ return [stripUndefined[0] ?? ty, hasNullable];
683
778
  }
684
779
  }
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
- });
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
- 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;
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
- // 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();
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
- // 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;
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
- nestedTree = treeRes.unwrap();
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
- return Either.right(result);
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 setCrudKinds(d, cruds) {
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()));
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 checkAttributeModifier(prop) {
812
- // Error: attributes must be just 'public'
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.InvalidAttributeModifier, (e) => {
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
+ }