@filipebraida/adonis-function-points 0.2.0 → 0.3.0

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,55 +1,4 @@
1
1
  import { Node, Project, SyntaxKind } from "ts-morph";
2
- //#region src/inventory/resolvers/action_object.ts
3
- /**
4
- * "Action object" pattern: the transaction delegates to an action instantiated
5
- * at the call site.
6
- *
7
- * await new ExpireInvite().handle({ invite })
8
- *
9
- * const mark = new MarkContentChanged()
10
- * await mark.handle({ documentId })
11
- *
12
- * The second form keeps the instance in a local variable, so the declaration
13
- * has to be followed back to the `new` — that is what `classOfReceiver` does.
14
- */
15
- const actionObjectResolver = {
16
- name: "action-object",
17
- order: 10,
18
- resolve(call, ctx) {
19
- const expr = call.getExpression();
20
- if (!expr.isKind(SyntaxKind.PropertyAccessExpression)) return [];
21
- const member = expr.getName();
22
- const className = classOfReceiver(expr.getExpression());
23
- if (!className) return [];
24
- const file = ctx.imports.get(className);
25
- if (!file) return [];
26
- return [{
27
- file,
28
- member
29
- }];
30
- }
31
- };
32
- /**
33
- * Finds the class behind a call receiver.
34
- *
35
- * new Foo().handle() -> 'Foo'
36
- * foo.handle() where const foo = new Foo() -> 'Foo'
37
- */
38
- function classOfReceiver(receiver) {
39
- if (receiver.isKind(SyntaxKind.NewExpression)) {
40
- const target = receiver.getExpression();
41
- return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
42
- }
43
- if (receiver.isKind(SyntaxKind.Identifier)) {
44
- const init = (receiver.getSymbol()?.getDeclarations().find((d) => d.isKind(SyntaxKind.VariableDeclaration)))?.asKind(SyntaxKind.VariableDeclaration)?.getInitializer();
45
- if (init?.isKind(SyntaxKind.NewExpression)) {
46
- const target = init.getExpression();
47
- return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
48
- }
49
- }
50
- return null;
51
- }
52
- //#endregion
53
2
  //#region src/inventory/paths.ts
54
3
  /**
55
4
  * One canonical spelling for every path the inventory emits.
@@ -170,29 +119,335 @@ function importedFrom(local, from, app) {
170
119
  if (!named && !isDefault) continue;
171
120
  return app.resolveSpecifier(declaration.getModuleSpecifierValue());
172
121
  }
173
- return null;
174
- }
122
+ return null;
123
+ }
124
+ /**
125
+ * The file a key of a generated registry points at.
126
+ *
127
+ * Both shapes the generators emit are handled: a direct reference to an
128
+ * imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
129
+ * per artefact and per framework version, and reading only one of them silently
130
+ * lost half the graph.
131
+ */
132
+ function registryEntry(registryFile, key, project, app) {
133
+ const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
134
+ if (!file) return null;
135
+ for (const declaration of file.getVariableDeclarations()) {
136
+ const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
137
+ if (!value) continue;
138
+ if (Node.isIdentifier(value)) {
139
+ const target = importedFrom(value.getText(), file, app);
140
+ return target ? toPosix(target) : null;
141
+ }
142
+ const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
143
+ const target = specifier ? app.resolveSpecifier(specifier) : null;
144
+ return target ? toPosix(target) : null;
145
+ }
146
+ return null;
147
+ }
148
+ //#endregion
149
+ //#region src/inventory/detectors/lucid.ts
150
+ const WRITE_METHODS = new Set([
151
+ "save",
152
+ "delete",
153
+ "create",
154
+ "createMany",
155
+ "merge",
156
+ "fill",
157
+ "updateOrCreate",
158
+ "fetchOrCreateMany",
159
+ "firstOrCreate",
160
+ "updateOrCreateMany",
161
+ "attach",
162
+ "detach",
163
+ "sync",
164
+ "increment",
165
+ "decrement",
166
+ "update",
167
+ "truncate",
168
+ "restore",
169
+ "forceDelete"
170
+ ]);
171
+ /**
172
+ * Which hooks each access fires, by decorator name — counting-decisions §3.
173
+ *
174
+ * `save()` fires the save pair AND the create-or-update pair, and which of the
175
+ * two runs is not knowable statically. That is not a compromise here: AFP
176
+ * §6.5.3 requires treating multiple optional paths as part of the same
177
+ * transaction, so following both is the specified behaviour.
178
+ */
179
+ const HOOKS_BY_METHOD = {
180
+ save: [
181
+ "beforeSave",
182
+ "afterSave",
183
+ "beforeCreate",
184
+ "afterCreate",
185
+ "beforeUpdate",
186
+ "afterUpdate"
187
+ ],
188
+ create: [
189
+ "beforeCreate",
190
+ "afterCreate",
191
+ "beforeSave",
192
+ "afterSave"
193
+ ],
194
+ createMany: [
195
+ "beforeCreate",
196
+ "afterCreate",
197
+ "beforeSave",
198
+ "afterSave"
199
+ ],
200
+ firstOrCreate: [
201
+ "beforeCreate",
202
+ "afterCreate",
203
+ "beforeSave",
204
+ "afterSave"
205
+ ],
206
+ fetchOrCreateMany: [
207
+ "beforeCreate",
208
+ "afterCreate",
209
+ "beforeSave",
210
+ "afterSave"
211
+ ],
212
+ updateOrCreate: [
213
+ "beforeCreate",
214
+ "afterCreate",
215
+ "beforeUpdate",
216
+ "afterUpdate",
217
+ "beforeSave",
218
+ "afterSave"
219
+ ],
220
+ updateOrCreateMany: [
221
+ "beforeCreate",
222
+ "afterCreate",
223
+ "beforeUpdate",
224
+ "afterUpdate",
225
+ "beforeSave",
226
+ "afterSave"
227
+ ],
228
+ delete: ["beforeDelete", "afterDelete"],
229
+ forceDelete: ["beforeDelete", "afterDelete"],
230
+ find: ["beforeFind", "afterFind"],
231
+ findOrFail: ["beforeFind", "afterFind"],
232
+ findBy: ["beforeFind", "afterFind"],
233
+ findByOrFail: ["beforeFind", "afterFind"],
234
+ first: ["beforeFind", "afterFind"],
235
+ firstOrFail: ["beforeFind", "afterFind"],
236
+ all: ["beforeFetch", "afterFetch"],
237
+ findMany: ["beforeFetch", "afterFetch"]
238
+ };
239
+ new Set(Object.values(HOOKS_BY_METHOD).flat());
240
+ /**
241
+ * Hook decorators fired by an access, or `[]` when it fires none.
242
+ *
243
+ * `truncate`, `increment`, `decrement` and the pivot operations change rows
244
+ * without instantiating a model, so no hook runs.
245
+ */
246
+ function hooksFiredBy(access) {
247
+ if (!access.firesHooks) return [];
248
+ return HOOKS_BY_METHOD[access.method] ?? [];
249
+ }
250
+ const READ_METHODS = new Set([
251
+ "find",
252
+ "findOrFail",
253
+ "findBy",
254
+ "findByOrFail",
255
+ "findMany",
256
+ "first",
257
+ "firstOrFail",
258
+ "all",
259
+ "query",
260
+ "preload",
261
+ "load",
262
+ "paginate",
263
+ "count",
264
+ "exists",
265
+ "related",
266
+ "where",
267
+ "orderBy"
268
+ ]);
269
+ function detectAccess(call, symbols, relations = /* @__PURE__ */ new Map()) {
270
+ const expression = call.getExpression();
271
+ if (!Node.isPropertyAccessExpression(expression)) return null;
272
+ const method = expression.getName();
273
+ const isWrite = WRITE_METHODS.has(method);
274
+ if (!isWrite && !READ_METHODS.has(method)) return null;
275
+ const receiver = expression.getExpression();
276
+ /**
277
+ * Looks up the PATH before the root: `input.invite.save()` has root `input`,
278
+ * which is no store at all — `input.invite` is.
279
+ *
280
+ * This is the dominant shape in action objects with a typed input, and
281
+ * without it the graph reaches the action and sees no write.
282
+ */
283
+ const store = symbols.get(pathSymbolOf(receiver) ?? "") ?? symbols.get(rootSymbolOf(receiver) ?? "");
284
+ if (!store) return null;
285
+ /**
286
+ * `distribution.related('files').create({…})` — the relation is the SUBJECT of
287
+ * the write, not a table read along the way.
288
+ *
289
+ * `relationTargetOf` reads the current method, and here the current method is
290
+ * `create`, whose receiver is the `related(…)` call. Without looking back up the
291
+ * chain the write was attributed to `distributions` alone and `distribution_files`
292
+ * came out as a table this application only reads — an EIF, maintained by
293
+ * somebody else. That is what a production application reported, and it is
294
+ * ordinary Lucid: `related(…)` followed by `create`, `createMany`, `save`,
295
+ * `saveMany`, `attach`, `detach` or `sync` writes the related table.
296
+ */
297
+ const related = relatedCallIn(receiver);
298
+ const viaRelation = relationTargetOf(method, call, store, relations) ?? (related ? relationTargetOf("related", related, store, relations) : void 0);
299
+ return {
300
+ mode: isWrite ? "write" : "read",
301
+ store,
302
+ method,
303
+ line: call.getStartLineNumber(),
304
+ viaRelation,
305
+ /** the relation is written when the method acting on it writes */
306
+ relationWritten: isWrite,
307
+ firesHooks: firesHooks(receiver)
308
+ };
309
+ }
310
+ /**
311
+ * The `related('x')` call inside a receiver chain, if any.
312
+ *
313
+ * Only `related` qualifies: `preload` and `load` hand back the parent, so a write
314
+ * after them acts on the parent. `related` hands back the relation's own query
315
+ * builder, and that is what makes the difference.
316
+ */
317
+ function relatedCallIn(receiver) {
318
+ let current = receiver;
319
+ for (let depth = 0; depth < 20 && current; depth++) {
320
+ if (Node.isCallExpression(current)) {
321
+ const expression = current.getExpression();
322
+ if (Node.isPropertyAccessExpression(expression) && expression.getName() === "related") return current;
323
+ current = expression;
324
+ continue;
325
+ }
326
+ if (Node.isPropertyAccessExpression(current) || Node.isAwaitExpression(current)) {
327
+ current = current.getExpression();
328
+ continue;
329
+ }
330
+ break;
331
+ }
332
+ return null;
333
+ }
334
+ /**
335
+ * An access fires hooks unless it went through the query builder.
336
+ *
337
+ * The signal is a CALL anywhere in the receiver chain: `document.delete()` has
338
+ * none, `Document.query().where(…).delete()` has two. It errs towards NOT
339
+ * following — `(await Document.find(id))!.delete()` is read as bulk — because
340
+ * an FTR that is missing understates, and one that is invented overstates.
341
+ */
342
+ function firesHooks(receiver) {
343
+ let current = receiver;
344
+ for (let depth = 0; depth < 20; depth++) {
345
+ if (Node.isCallExpression(current)) return false;
346
+ if (!Node.isPropertyAccessExpression(current)) return Node.isIdentifier(current);
347
+ current = current.getExpression();
348
+ }
349
+ return false;
350
+ }
351
+ const RELATION_ACCESSORS = new Set([
352
+ "preload",
353
+ "load",
354
+ "related",
355
+ "withCount"
356
+ ]);
357
+ /**
358
+ * `.preload('author')` on a store declaring `{ author: 'Author' }` reaches
359
+ * `Author`.
360
+ */
361
+ function relationTargetOf(method, call, store, relations) {
362
+ if (!RELATION_ACCESSORS.has(method)) return void 0;
363
+ const name = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
364
+ if (!name) return void 0;
365
+ return relations.get(store)?.[name];
366
+ }
367
+ /**
368
+ * Dotted path of a receiver made only of property accesses: `input.invite`
369
+ * yields "input.invite". Any call in between invalidates the path, because the
370
+ * value stops being statically traceable.
371
+ */
372
+ function pathSymbolOf(node) {
373
+ const parts = [];
374
+ let current = node;
375
+ for (let depth = 0; depth < 20; depth++) {
376
+ if (Node.isIdentifier(current)) return [current.getText(), ...parts].join(".");
377
+ if (!Node.isPropertyAccessExpression(current)) return null;
378
+ parts.unshift(current.getName());
379
+ current = current.getExpression();
380
+ }
381
+ return null;
382
+ }
383
+ /**
384
+ * Root of an `a.b().c()` chain — the left-most identifier.
385
+ *
386
+ * It must traverse `await`, calls, property access and `new`, otherwise
387
+ * `await new Action().handle()` and `Invite.query().where().update()` stop at
388
+ * the first node and the write disappears.
389
+ */
390
+ function rootSymbolOf(node) {
391
+ let current = node;
392
+ for (let depth = 0; depth < 60 && current; depth++) {
393
+ if (Node.isIdentifier(current)) return current.getText();
394
+ if (current.getKind() === SyntaxKind.ThisKeyword) return "this";
395
+ if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current) || Node.isCallExpression(current) || Node.isNewExpression(current) || Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
396
+ current = current.getExpression();
397
+ continue;
398
+ }
399
+ return null;
400
+ }
401
+ return null;
402
+ }
403
+ //#endregion
404
+ //#region src/inventory/resolvers/action_object.ts
405
+ /**
406
+ * "Action object" pattern: the transaction delegates to an action instantiated
407
+ * at the call site.
408
+ *
409
+ * await new ExpireInvite().handle({ invite })
410
+ *
411
+ * const mark = new MarkContentChanged()
412
+ * await mark.handle({ documentId })
413
+ *
414
+ * The second form keeps the instance in a local variable, so the declaration
415
+ * has to be followed back to the `new` — that is what `classOfReceiver` does.
416
+ */
417
+ const actionObjectResolver = {
418
+ name: "action-object",
419
+ order: 10,
420
+ resolve(call, ctx) {
421
+ const expr = call.getExpression();
422
+ if (!expr.isKind(SyntaxKind.PropertyAccessExpression)) return [];
423
+ const member = expr.getName();
424
+ const className = classOfReceiver(expr.getExpression());
425
+ if (!className) return [];
426
+ const file = ctx.imports.get(className);
427
+ if (!file) return [];
428
+ return [{
429
+ file,
430
+ member
431
+ }];
432
+ }
433
+ };
175
434
  /**
176
- * The file a key of a generated registry points at.
435
+ * Finds the class behind a call receiver.
177
436
  *
178
- * Both shapes the generators emit are handled: a direct reference to an
179
- * imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
180
- * per artefact and per framework version, and reading only one of them silently
181
- * lost half the graph.
437
+ * new Foo().handle() -> 'Foo'
438
+ * foo.handle() where const foo = new Foo() -> 'Foo'
182
439
  */
183
- function registryEntry(registryFile, key, project, app) {
184
- const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
185
- if (!file) return null;
186
- for (const declaration of file.getVariableDeclarations()) {
187
- const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
188
- if (!value) continue;
189
- if (Node.isIdentifier(value)) {
190
- const target = importedFrom(value.getText(), file, app);
191
- return target ? toPosix(target) : null;
440
+ function classOfReceiver(receiver) {
441
+ if (receiver.isKind(SyntaxKind.NewExpression)) {
442
+ const target = receiver.getExpression();
443
+ return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
444
+ }
445
+ if (receiver.isKind(SyntaxKind.Identifier)) {
446
+ const init = (receiver.getSymbol()?.getDeclarations().find((d) => d.isKind(SyntaxKind.VariableDeclaration)))?.asKind(SyntaxKind.VariableDeclaration)?.getInitializer();
447
+ if (init?.isKind(SyntaxKind.NewExpression)) {
448
+ const target = init.getExpression();
449
+ return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
192
450
  }
193
- const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
194
- const target = specifier ? app.resolveSpecifier(specifier) : null;
195
- return target ? toPosix(target) : null;
196
451
  }
197
452
  return null;
198
453
  }
@@ -239,17 +494,24 @@ const DISPATCH_METHODS = new Set([
239
494
  /**
240
495
  * The method that actually runs the job, by queue package.
241
496
  *
242
- * There is no single name: `@adonisjs/queue` and `@rlanz/bull-queue` call it
243
- * `handle`, `@nemoventures/adonis-jobs` calls it `process`. Looking only for
244
- * `handle` meant every job in an application using the second one resolved to
245
- * a file and then to no body, so the dispatch was reported as an unknown while
246
- * the writes inside it went uncounted — the worst of both outcomes.
497
+ * There is no single name, and this list grew twice by measurement rather than by
498
+ * reasoning. `@rlanz/bull-queue` uses `handle`; `@nemoventures/adonis-jobs` calls
499
+ * it `process`; `@adonisjs/queue` — the official package — generates
500
+ * `async execute()` in its own `make:job` stub. Each omission cost the same: the
501
+ * file resolved, no body was found, the dispatch was reported as an unknown, and
502
+ * every write inside the job went uncounted.
503
+ *
504
+ * `execute` surfaced only once event dispatch started being followed, because the
505
+ * listener was what enqueued the job and that path had never been walked. Which is
506
+ * the argument for adding a name when a real application shows it: a list written
507
+ * from imagination would have missed this one too.
247
508
  *
248
509
  * Ordered: a class declaring more than one is answering the dispatcher with the
249
510
  * first, and `handle` is the most common.
250
511
  */
251
512
  const EXECUTION_METHODS = [
252
513
  "handle",
514
+ "execute",
253
515
  "process",
254
516
  "run",
255
517
  "perform"
@@ -434,221 +696,6 @@ const staticServiceResolver = {
434
696
  }
435
697
  };
436
698
  //#endregion
437
- //#region src/inventory/detectors/lucid.ts
438
- const WRITE_METHODS = new Set([
439
- "save",
440
- "delete",
441
- "create",
442
- "createMany",
443
- "merge",
444
- "fill",
445
- "updateOrCreate",
446
- "fetchOrCreateMany",
447
- "firstOrCreate",
448
- "updateOrCreateMany",
449
- "attach",
450
- "detach",
451
- "sync",
452
- "increment",
453
- "decrement",
454
- "update",
455
- "truncate",
456
- "restore",
457
- "forceDelete"
458
- ]);
459
- /**
460
- * Which hooks each access fires, by decorator name — counting-decisions §3.
461
- *
462
- * `save()` fires the save pair AND the create-or-update pair, and which of the
463
- * two runs is not knowable statically. That is not a compromise here: AFP
464
- * §6.5.3 requires treating multiple optional paths as part of the same
465
- * transaction, so following both is the specified behaviour.
466
- */
467
- const HOOKS_BY_METHOD = {
468
- save: [
469
- "beforeSave",
470
- "afterSave",
471
- "beforeCreate",
472
- "afterCreate",
473
- "beforeUpdate",
474
- "afterUpdate"
475
- ],
476
- create: [
477
- "beforeCreate",
478
- "afterCreate",
479
- "beforeSave",
480
- "afterSave"
481
- ],
482
- createMany: [
483
- "beforeCreate",
484
- "afterCreate",
485
- "beforeSave",
486
- "afterSave"
487
- ],
488
- firstOrCreate: [
489
- "beforeCreate",
490
- "afterCreate",
491
- "beforeSave",
492
- "afterSave"
493
- ],
494
- fetchOrCreateMany: [
495
- "beforeCreate",
496
- "afterCreate",
497
- "beforeSave",
498
- "afterSave"
499
- ],
500
- updateOrCreate: [
501
- "beforeCreate",
502
- "afterCreate",
503
- "beforeUpdate",
504
- "afterUpdate",
505
- "beforeSave",
506
- "afterSave"
507
- ],
508
- updateOrCreateMany: [
509
- "beforeCreate",
510
- "afterCreate",
511
- "beforeUpdate",
512
- "afterUpdate",
513
- "beforeSave",
514
- "afterSave"
515
- ],
516
- delete: ["beforeDelete", "afterDelete"],
517
- forceDelete: ["beforeDelete", "afterDelete"],
518
- find: ["beforeFind", "afterFind"],
519
- findOrFail: ["beforeFind", "afterFind"],
520
- findBy: ["beforeFind", "afterFind"],
521
- findByOrFail: ["beforeFind", "afterFind"],
522
- first: ["beforeFind", "afterFind"],
523
- firstOrFail: ["beforeFind", "afterFind"],
524
- all: ["beforeFetch", "afterFetch"],
525
- findMany: ["beforeFetch", "afterFetch"]
526
- };
527
- new Set(Object.values(HOOKS_BY_METHOD).flat());
528
- /**
529
- * Hook decorators fired by an access, or `[]` when it fires none.
530
- *
531
- * `truncate`, `increment`, `decrement` and the pivot operations change rows
532
- * without instantiating a model, so no hook runs.
533
- */
534
- function hooksFiredBy(access) {
535
- if (!access.firesHooks) return [];
536
- return HOOKS_BY_METHOD[access.method] ?? [];
537
- }
538
- const READ_METHODS = new Set([
539
- "find",
540
- "findOrFail",
541
- "findBy",
542
- "findByOrFail",
543
- "findMany",
544
- "first",
545
- "firstOrFail",
546
- "all",
547
- "query",
548
- "preload",
549
- "load",
550
- "paginate",
551
- "count",
552
- "exists",
553
- "related",
554
- "where",
555
- "orderBy"
556
- ]);
557
- function detectAccess(call, symbols, relations = /* @__PURE__ */ new Map()) {
558
- const expression = call.getExpression();
559
- if (!Node.isPropertyAccessExpression(expression)) return null;
560
- const method = expression.getName();
561
- const isWrite = WRITE_METHODS.has(method);
562
- if (!isWrite && !READ_METHODS.has(method)) return null;
563
- const receiver = expression.getExpression();
564
- /**
565
- * Looks up the PATH before the root: `input.invite.save()` has root `input`,
566
- * which is no store at all — `input.invite` is.
567
- *
568
- * This is the dominant shape in action objects with a typed input, and
569
- * without it the graph reaches the action and sees no write.
570
- */
571
- const store = symbols.get(pathSymbolOf(receiver) ?? "") ?? symbols.get(rootSymbolOf(receiver) ?? "");
572
- if (!store) return null;
573
- return {
574
- mode: isWrite ? "write" : "read",
575
- store,
576
- method,
577
- line: call.getStartLineNumber(),
578
- viaRelation: relationTargetOf(method, call, store, relations),
579
- firesHooks: firesHooks(receiver)
580
- };
581
- }
582
- /**
583
- * An access fires hooks unless it went through the query builder.
584
- *
585
- * The signal is a CALL anywhere in the receiver chain: `document.delete()` has
586
- * none, `Document.query().where(…).delete()` has two. It errs towards NOT
587
- * following — `(await Document.find(id))!.delete()` is read as bulk — because
588
- * an FTR that is missing understates, and one that is invented overstates.
589
- */
590
- function firesHooks(receiver) {
591
- let current = receiver;
592
- for (let depth = 0; depth < 20; depth++) {
593
- if (Node.isCallExpression(current)) return false;
594
- if (!Node.isPropertyAccessExpression(current)) return Node.isIdentifier(current);
595
- current = current.getExpression();
596
- }
597
- return false;
598
- }
599
- const RELATION_ACCESSORS = new Set([
600
- "preload",
601
- "load",
602
- "related",
603
- "withCount"
604
- ]);
605
- /**
606
- * `.preload('author')` on a store declaring `{ author: 'Author' }` reaches
607
- * `Author`.
608
- */
609
- function relationTargetOf(method, call, store, relations) {
610
- if (!RELATION_ACCESSORS.has(method)) return void 0;
611
- const name = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
612
- if (!name) return void 0;
613
- return relations.get(store)?.[name];
614
- }
615
- /**
616
- * Dotted path of a receiver made only of property accesses: `input.invite`
617
- * yields "input.invite". Any call in between invalidates the path, because the
618
- * value stops being statically traceable.
619
- */
620
- function pathSymbolOf(node) {
621
- const parts = [];
622
- let current = node;
623
- for (let depth = 0; depth < 20; depth++) {
624
- if (Node.isIdentifier(current)) return [current.getText(), ...parts].join(".");
625
- if (!Node.isPropertyAccessExpression(current)) return null;
626
- parts.unshift(current.getName());
627
- current = current.getExpression();
628
- }
629
- return null;
630
- }
631
- /**
632
- * Root of an `a.b().c()` chain — the left-most identifier.
633
- *
634
- * It must traverse `await`, calls, property access and `new`, otherwise
635
- * `await new Action().handle()` and `Invite.query().where().update()` stop at
636
- * the first node and the write disappears.
637
- */
638
- function rootSymbolOf(node) {
639
- let current = node;
640
- for (let depth = 0; depth < 60 && current; depth++) {
641
- if (Node.isIdentifier(current)) return current.getText();
642
- if (current.getKind() === SyntaxKind.ThisKeyword) return "this";
643
- if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current) || Node.isCallExpression(current) || Node.isNewExpression(current) || Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
644
- current = current.getExpression();
645
- continue;
646
- }
647
- return null;
648
- }
649
- return null;
650
- }
651
- //#endregion
652
699
  //#region src/inventory/resolvers/transformer.ts
653
700
  /** BaseTransformer's public API; all of it funnels through `toObject` */
654
701
  const TRANSFORMER_METHODS = new Set([