@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.
- package/CHANGELOG.md +45 -0
- package/README.md +1 -1
- package/build/calibration-8eV8CEix.js +403 -0
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-Cm079xWL.js → fp_calibrate-iFAec0tA.js} +1 -1
- package/build/{fp_count-CfcXPuj5.js → fp_count-D21tQ_pv.js} +1 -1
- package/build/{fp_diff-DE_t3twv.js → fp_diff-D0pHGMgi.js} +1 -1
- package/build/{fp_explain-MKyEoi0h.js → fp_explain-BwFs-LW-.js} +1 -1
- package/build/{fp_inventory-DSrCVhEy.js → fp_inventory-Bu6O1Nn0.js} +1 -1
- package/build/{fp_metrics-BUWj9dLw.js → fp_metrics-MGDppfSa.js} +1 -1
- package/build/index.d.ts +28 -0
- package/build/index.js +4 -3
- package/build/{pipeline-DySlMWcN.js → pipeline-CIAydCcT.js} +162 -22
- package/build/{resolvers-MFjRl2ef.js → resolvers-CRB6lXoo.js} +337 -290
- package/build/{runners-DpMd-yZM.js → runners-CmxNHuuq.js} +3 -387
- package/build/src/albrecht/counter.d.ts +9 -5
- package/build/src/cli.js +2 -2
- package/build/src/inventory/detectors/lucid.d.ts +8 -0
- package/build/src/inventory/graph/call_graph.d.ts +2 -0
- package/build/src/inventory/resolvers/index.js +1 -1
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +7 -0
- package/package.json +1 -1
- package/build/define_config-DOqWyPwV.js +0 -19
|
@@ -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
|
-
*
|
|
435
|
+
* Finds the class behind a call receiver.
|
|
177
436
|
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
243
|
-
* `handle
|
|
244
|
-
* `
|
|
245
|
-
*
|
|
246
|
-
*
|
|
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([
|