@pikku/inspector 0.12.41 → 0.12.43
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 +104 -0
- package/dist/add/add-addon-bans.js +1 -0
- package/dist/add/add-ai-agent.d.ts +3 -1
- package/dist/add/add-ai-agent.js +82 -0
- package/dist/add/add-credential.js +3 -1
- package/dist/add/add-functions.js +10 -9
- package/dist/add/add-gateway.js +3 -0
- package/dist/add/add-http-route.js +2 -2
- package/dist/add/add-mcp-prompt.js +0 -1
- package/dist/add/add-mcp-resource.js +0 -1
- package/dist/add/add-permission.d.ts +1 -1
- package/dist/add/add-permission.js +3 -177
- package/dist/add/add-queue-worker.js +3 -0
- package/dist/add/add-schedule.js +3 -0
- package/dist/add/add-scope.d.ts +2 -0
- package/dist/add/add-scope.js +146 -0
- package/dist/add/add-trigger.js +3 -0
- package/dist/add/add-wire-remote-addon.d.ts +10 -0
- package/dist/add/add-wire-remote-addon.js +66 -0
- package/dist/add/add-workflow-graph.js +32 -3
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.js +4 -0
- package/dist/inspector.js +19 -7
- package/dist/types.d.ts +19 -13
- package/dist/utils/custom-types-generator.js +2 -1
- package/dist/utils/ensure-function-metadata.d.ts +15 -0
- package/dist/utils/ensure-function-metadata.js +24 -0
- package/dist/utils/filter-inspector-state.js +9 -0
- package/dist/utils/get-property-value.d.ts +7 -2
- package/dist/utils/get-property-value.js +48 -1
- package/dist/utils/load-addon-functions-meta.js +81 -6
- package/dist/utils/nested-project-filter.d.ts +1 -0
- package/dist/utils/nested-project-filter.js +29 -0
- package/dist/utils/permissions.d.ts +1 -21
- package/dist/utils/permissions.js +17 -108
- package/dist/utils/post-process.d.ts +30 -1
- package/dist/utils/post-process.js +150 -72
- package/dist/utils/serialize-inspector-state.d.ts +13 -8
- package/dist/utils/serialize-inspector-state.js +12 -6
- package/dist/utils/serialize-permissions-groups-meta.d.ts +0 -2
- package/dist/utils/serialize-permissions-groups-meta.js +0 -26
- package/dist/utils/workflow/derive-workflow-plan.js +9 -3
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +56 -1
- package/dist/utils/workflow/dsl/patterns.d.ts +10 -0
- package/dist/utils/workflow/dsl/patterns.js +24 -8
- package/dist/utils/workflow/dsl/validation.js +3 -4
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +14 -0
- package/dist/utils/workflow/graph/finalize-workflows.d.ts +7 -0
- package/dist/utils/workflow/graph/finalize-workflows.js +11 -2
- package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +2 -0
- package/dist/utils/workflow/graph/serialize-workflow-graph.js +4 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +6 -2
- package/dist/visit.js +4 -0
- package/package.json +3 -3
- package/src/add/add-addon-bans.ts +1 -0
- package/src/add/add-ai-agent.test.ts +87 -0
- package/src/add/add-ai-agent.ts +122 -0
- package/src/add/add-credential.ts +6 -0
- package/src/add/add-functions.ts +10 -12
- package/src/add/add-gateway.ts +11 -0
- package/src/add/add-http-route.ts +2 -8
- package/src/add/add-mcp-prompt.ts +0 -1
- package/src/add/add-mcp-resource.ts +0 -1
- package/src/add/add-permission.ts +4 -242
- package/src/add/add-queue-worker.ts +11 -0
- package/src/add/add-schedule.ts +11 -0
- package/src/add/add-scope.test.ts +346 -0
- package/src/add/add-scope.ts +225 -0
- package/src/add/add-trigger.ts +11 -0
- package/src/add/add-wire-remote-addon.ts +77 -0
- package/src/add/add-workflow-graph-input.test.ts +94 -0
- package/src/add/add-workflow-graph-notes.test.ts +74 -0
- package/src/add/add-workflow-graph.ts +35 -3
- package/src/add/inline-wiring-function.test.ts +104 -0
- package/src/add/validate-workflow-graph-addons.test.ts +102 -0
- package/src/error-codes.ts +5 -0
- package/src/inspector.ts +22 -6
- package/src/types.ts +16 -17
- package/src/utils/custom-types-generator.test.ts +26 -1
- package/src/utils/custom-types-generator.ts +2 -1
- package/src/utils/ensure-function-metadata.ts +42 -0
- package/src/utils/filter-inspector-state.test.ts +0 -2
- package/src/utils/filter-inspector-state.ts +9 -0
- package/src/utils/get-property-value.test.ts +141 -0
- package/src/utils/get-property-value.ts +62 -1
- package/src/utils/load-addon-functions-meta.test.ts +157 -0
- package/src/utils/load-addon-functions-meta.ts +94 -6
- package/src/utils/load-addon-scopes.test.ts +138 -0
- package/src/utils/nested-project-filter.test.ts +53 -0
- package/src/utils/nested-project-filter.ts +28 -0
- package/src/utils/permissions.test.ts +7 -232
- package/src/utils/permissions.ts +20 -160
- package/src/utils/post-process.test.ts +269 -4
- package/src/utils/post-process.ts +216 -95
- package/src/utils/serialize-inspector-state.ts +22 -25
- package/src/utils/serialize-permissions-groups-meta.ts +0 -28
- package/src/utils/workflow/derive-workflow-plan.test.ts +41 -3
- package/src/utils/workflow/derive-workflow-plan.ts +11 -4
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +66 -0
- package/src/utils/workflow/dsl/patterns.ts +34 -8
- package/src/utils/workflow/dsl/validation.ts +3 -4
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +15 -0
- package/src/utils/workflow/graph/finalize-workflows.test.ts +50 -0
- package/src/utils/workflow/graph/finalize-workflows.ts +13 -2
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +6 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +8 -0
- package/src/visit.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,53 +1,21 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
2
|
import type { FunctionWiresMeta } from '@pikku/core'
|
|
3
|
-
import type { AddWiring
|
|
4
|
-
import {
|
|
5
|
-
extractFunctionName,
|
|
6
|
-
isNamedExport,
|
|
7
|
-
makeContextBasedId,
|
|
8
|
-
} from '../utils/extract-function-name.js'
|
|
3
|
+
import type { AddWiring } from '../types.js'
|
|
4
|
+
import { extractFunctionName } from '../utils/extract-function-name.js'
|
|
9
5
|
import {
|
|
10
6
|
extractServicesFromFunction,
|
|
11
7
|
extractUsedWires,
|
|
12
8
|
} from '../utils/extract-services.js'
|
|
13
|
-
import { extractPermissionPikkuNames } from '../utils/permissions.js'
|
|
14
9
|
import { getPropertyValue } from '../utils/get-property-value.js'
|
|
15
10
|
import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
|
|
16
11
|
|
|
17
|
-
function renameTempDefinitions(
|
|
18
|
-
state: InspectorState,
|
|
19
|
-
definitionIds: string[],
|
|
20
|
-
groupType: string,
|
|
21
|
-
groupKey: string
|
|
22
|
-
): void {
|
|
23
|
-
const tempIndices = definitionIds
|
|
24
|
-
.map((name, i) => (name.startsWith('__temp_') ? i : -1))
|
|
25
|
-
.filter((i) => i >= 0)
|
|
26
|
-
|
|
27
|
-
for (const idx of tempIndices) {
|
|
28
|
-
const oldId = definitionIds[idx]
|
|
29
|
-
const newId =
|
|
30
|
-
tempIndices.length === 1
|
|
31
|
-
? makeContextBasedId(groupType, groupKey)
|
|
32
|
-
: makeContextBasedId(groupType, groupKey, String(idx))
|
|
33
|
-
const existing = state.permissions.definitions[oldId]
|
|
34
|
-
if (existing) {
|
|
35
|
-
delete state.permissions.definitions[oldId]
|
|
36
|
-
state.permissions.definitions[newId] = existing
|
|
37
|
-
}
|
|
38
|
-
definitionIds[idx] = newId
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
12
|
function isInsidePermissionContainer(node: ts.Node): boolean {
|
|
43
13
|
let current = node.parent
|
|
44
14
|
while (current) {
|
|
45
15
|
if (
|
|
46
16
|
ts.isCallExpression(current) &&
|
|
47
17
|
ts.isIdentifier(current.expression) &&
|
|
48
|
-
|
|
49
|
-
current.expression.text === 'addTagPermission' ||
|
|
50
|
-
current.expression.text === 'addHTTPPermission')
|
|
18
|
+
current.expression.text === 'pikkuPermissionFactory'
|
|
51
19
|
) {
|
|
52
20
|
return true
|
|
53
21
|
}
|
|
@@ -57,7 +25,7 @@ function isInsidePermissionContainer(node: ts.Node): boolean {
|
|
|
57
25
|
}
|
|
58
26
|
|
|
59
27
|
/**
|
|
60
|
-
* Inspect pikkuPermission
|
|
28
|
+
* Inspect pikkuPermission, pikkuAuth, and pikkuPermissionFactory definitions.
|
|
61
29
|
*/
|
|
62
30
|
export const addPermission: AddWiring = (logger, node, checker, state) => {
|
|
63
31
|
if (!ts.isCallExpression(node)) return
|
|
@@ -340,210 +308,4 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
|
|
|
340
308
|
)
|
|
341
309
|
return
|
|
342
310
|
}
|
|
343
|
-
|
|
344
|
-
// Handle addPermission('tag', [permission1, permission2])
|
|
345
|
-
// Supports two patterns:
|
|
346
|
-
// 1. export const x = () => addTagPermission('tag', [...]) (factory - tree-shakeable)
|
|
347
|
-
// 2. export const x = addTagPermission('tag', [...]) (direct - no tree-shaking)
|
|
348
|
-
if (expression.text === 'addTagPermission') {
|
|
349
|
-
const tagArg = args[0]
|
|
350
|
-
const permissionsArrayArg = args[1]
|
|
351
|
-
|
|
352
|
-
if (!tagArg || !permissionsArrayArg) return
|
|
353
|
-
|
|
354
|
-
// Extract tag name
|
|
355
|
-
let tag: string | undefined
|
|
356
|
-
if (ts.isStringLiteral(tagArg)) {
|
|
357
|
-
tag = tagArg.text
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (!tag) {
|
|
361
|
-
logger.warn(`• addTagPermission call without valid tag string`)
|
|
362
|
-
return
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Check if permissions is a literal array or object
|
|
366
|
-
if (
|
|
367
|
-
!ts.isArrayLiteralExpression(permissionsArrayArg) &&
|
|
368
|
-
!ts.isObjectLiteralExpression(permissionsArrayArg)
|
|
369
|
-
) {
|
|
370
|
-
logger.error(
|
|
371
|
-
`• addTagPermission('${tag}', ...) must have a literal array or object as second argument`
|
|
372
|
-
)
|
|
373
|
-
return
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Extract permission pikkuFuncIds from array
|
|
377
|
-
const permissionNames = extractPermissionPikkuNames(
|
|
378
|
-
permissionsArrayArg,
|
|
379
|
-
checker,
|
|
380
|
-
state.rootDir
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
if (permissionNames.length > 0) {
|
|
384
|
-
renameTempDefinitions(state, permissionNames, 'tag', tag)
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const allServices = new Set<string>()
|
|
388
|
-
for (const permissionName of permissionNames) {
|
|
389
|
-
const permissionMeta = state.permissions.definitions[permissionName]
|
|
390
|
-
if (permissionMeta && permissionMeta.services) {
|
|
391
|
-
for (const service of permissionMeta.services.services) {
|
|
392
|
-
allServices.add(service)
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
let isFactory = false
|
|
398
|
-
let exportedName: string | null = null
|
|
399
|
-
let parent = node.parent
|
|
400
|
-
|
|
401
|
-
if (parent && ts.isArrowFunction(parent)) {
|
|
402
|
-
if (parent.parameters.length === 0) {
|
|
403
|
-
isFactory = true
|
|
404
|
-
|
|
405
|
-
const arrowParent = parent.parent
|
|
406
|
-
if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
|
|
407
|
-
if (ts.isIdentifier(arrowParent.name)) {
|
|
408
|
-
if (isNamedExport(arrowParent)) {
|
|
409
|
-
exportedName = arrowParent.name.text
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (!isFactory) {
|
|
417
|
-
const extracted = extractFunctionName(node, checker, state.rootDir)
|
|
418
|
-
exportedName = extracted.exportedName
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (!isFactory && exportedName) {
|
|
422
|
-
logger.warn(
|
|
423
|
-
`• Permission group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
|
|
424
|
-
`For tree-shaking, use: export const ${exportedName} = () => addTagPermission('${tag}', [...])`
|
|
425
|
-
)
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
state.permissions.tagPermissions.set(tag, {
|
|
429
|
-
exportName: exportedName,
|
|
430
|
-
sourceFile: node.getSourceFile().fileName,
|
|
431
|
-
position: node.getStart(),
|
|
432
|
-
services: {
|
|
433
|
-
optimized: false,
|
|
434
|
-
services: Array.from(allServices),
|
|
435
|
-
},
|
|
436
|
-
count: permissionNames.length,
|
|
437
|
-
instanceIds: permissionNames,
|
|
438
|
-
isFactory,
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
logger.debug(
|
|
442
|
-
`• Found tag permission group: ${tag} -> [${permissionNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`
|
|
443
|
-
)
|
|
444
|
-
return
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Handle addHTTPPermission(pattern, [permission1, permission2])
|
|
448
|
-
// Supports two patterns:
|
|
449
|
-
// 1. export const x = () => addHTTPPermission('*', [...]) (factory - tree-shakeable)
|
|
450
|
-
// 2. export const x = addHTTPPermission('*', [...]) (direct - no tree-shaking)
|
|
451
|
-
if (expression.text === 'addHTTPPermission') {
|
|
452
|
-
const patternArg = args[0]
|
|
453
|
-
const permissionsArrayArg = args[1]
|
|
454
|
-
|
|
455
|
-
if (!patternArg || !permissionsArrayArg) return
|
|
456
|
-
|
|
457
|
-
// Extract route pattern
|
|
458
|
-
let pattern: string | undefined
|
|
459
|
-
if (ts.isStringLiteral(patternArg)) {
|
|
460
|
-
pattern = patternArg.text
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (!pattern) {
|
|
464
|
-
logger.warn(`• addHTTPPermission call without valid pattern string`)
|
|
465
|
-
return
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Check if permissions is a literal array or object
|
|
469
|
-
if (
|
|
470
|
-
!ts.isArrayLiteralExpression(permissionsArrayArg) &&
|
|
471
|
-
!ts.isObjectLiteralExpression(permissionsArrayArg)
|
|
472
|
-
) {
|
|
473
|
-
logger.error(
|
|
474
|
-
`• addHTTPPermission('${pattern}', ...) must have a literal array or object as second argument`
|
|
475
|
-
)
|
|
476
|
-
return
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Extract permission pikkuFuncIds from array
|
|
480
|
-
const permissionNames = extractPermissionPikkuNames(
|
|
481
|
-
permissionsArrayArg,
|
|
482
|
-
checker,
|
|
483
|
-
state.rootDir
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
if (permissionNames.length > 0) {
|
|
487
|
-
renameTempDefinitions(state, permissionNames, 'http', pattern)
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const allServices = new Set<string>()
|
|
491
|
-
for (const permissionName of permissionNames) {
|
|
492
|
-
const permissionMeta = state.permissions.definitions[permissionName]
|
|
493
|
-
if (permissionMeta && permissionMeta.services) {
|
|
494
|
-
for (const service of permissionMeta.services.services) {
|
|
495
|
-
allServices.add(service)
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
let isFactory = false
|
|
501
|
-
let exportedName: string | null = null
|
|
502
|
-
let parent = node.parent
|
|
503
|
-
|
|
504
|
-
if (parent && ts.isArrowFunction(parent)) {
|
|
505
|
-
if (parent.parameters.length === 0) {
|
|
506
|
-
isFactory = true
|
|
507
|
-
|
|
508
|
-
const arrowParent = parent.parent
|
|
509
|
-
if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
|
|
510
|
-
if (ts.isIdentifier(arrowParent.name)) {
|
|
511
|
-
if (isNamedExport(arrowParent)) {
|
|
512
|
-
exportedName = arrowParent.name.text
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (!isFactory) {
|
|
520
|
-
const extracted = extractFunctionName(node, checker, state.rootDir)
|
|
521
|
-
exportedName = extracted.exportedName
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (!isFactory && exportedName) {
|
|
525
|
-
logger.warn(
|
|
526
|
-
`• HTTP permission group '${exportedName}' for pattern '${pattern}' is not wrapped in a factory function. ` +
|
|
527
|
-
`For tree-shaking, use: export const ${exportedName} = () => addHTTPPermission('${pattern}', [...])`
|
|
528
|
-
)
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
state.http.routePermissions.set(pattern, {
|
|
532
|
-
exportName: exportedName,
|
|
533
|
-
sourceFile: node.getSourceFile().fileName,
|
|
534
|
-
position: node.getStart(),
|
|
535
|
-
services: {
|
|
536
|
-
optimized: false,
|
|
537
|
-
services: Array.from(allServices),
|
|
538
|
-
},
|
|
539
|
-
count: permissionNames.length,
|
|
540
|
-
instanceIds: permissionNames,
|
|
541
|
-
isFactory,
|
|
542
|
-
})
|
|
543
|
-
|
|
544
|
-
logger.debug(
|
|
545
|
-
`• Found HTTP route permission group: ${pattern} -> [${permissionNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`
|
|
546
|
-
)
|
|
547
|
-
return
|
|
548
|
-
}
|
|
549
311
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
makeContextBasedId,
|
|
10
10
|
} from '../utils/extract-function-name.js'
|
|
11
11
|
import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
|
|
12
|
+
import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js'
|
|
12
13
|
import { resolveMiddleware } from '../utils/middleware.js'
|
|
13
14
|
import { extractWireNames } from '../utils/post-process.js'
|
|
14
15
|
import { resolveAddonName } from '../utils/resolve-addon-package.js'
|
|
@@ -82,6 +83,16 @@ export const addQueueWorker: AddWiring = (logger, node, checker, state) => {
|
|
|
82
83
|
return
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
// Register metadata for a func inlined into the wiring (see helper).
|
|
87
|
+
ensureInlineWiringFunction(
|
|
88
|
+
state,
|
|
89
|
+
pikkuFuncId,
|
|
90
|
+
name,
|
|
91
|
+
funcInitializer,
|
|
92
|
+
checker,
|
|
93
|
+
extracted.isHelper
|
|
94
|
+
)
|
|
95
|
+
|
|
85
96
|
// --- resolve middleware ---
|
|
86
97
|
const middleware = resolveMiddleware(state, obj, tags, checker)
|
|
87
98
|
|
package/src/add/add-schedule.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
makeContextBasedId,
|
|
10
10
|
} from '../utils/extract-function-name.js'
|
|
11
11
|
import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
|
|
12
|
+
import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js'
|
|
12
13
|
import { resolveMiddleware } from '../utils/middleware.js'
|
|
13
14
|
import { extractWireNames } from '../utils/post-process.js'
|
|
14
15
|
import { resolveAddonName } from '../utils/resolve-addon-package.js'
|
|
@@ -84,6 +85,16 @@ export const addSchedule: AddWiring = (
|
|
|
84
85
|
return
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// Register metadata for a func inlined into the wiring (see helper).
|
|
89
|
+
ensureInlineWiringFunction(
|
|
90
|
+
state,
|
|
91
|
+
pikkuFuncId,
|
|
92
|
+
nameValue,
|
|
93
|
+
funcInitializer,
|
|
94
|
+
checker,
|
|
95
|
+
extracted.isHelper
|
|
96
|
+
)
|
|
97
|
+
|
|
87
98
|
// --- resolve middleware ---
|
|
88
99
|
const middleware = resolveMiddleware(state, obj, tags, checker)
|
|
89
100
|
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { strict as assert } from 'assert'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { inspect } from '../inspector.js'
|
|
7
|
+
import { ErrorCode } from '../error-codes.js'
|
|
8
|
+
import type { InspectorLogger } from '../types.js'
|
|
9
|
+
|
|
10
|
+
const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
|
|
11
|
+
({
|
|
12
|
+
debug: () => {},
|
|
13
|
+
info: () => {},
|
|
14
|
+
warn: () => {},
|
|
15
|
+
error: () => {},
|
|
16
|
+
diagnostic: ({ code, message }) => {
|
|
17
|
+
criticals.push({ code, message })
|
|
18
|
+
},
|
|
19
|
+
critical: (code: ErrorCode, message: string) => {
|
|
20
|
+
criticals.push({ code, message })
|
|
21
|
+
},
|
|
22
|
+
hasCriticalErrors: () => criticals.length > 0,
|
|
23
|
+
}) satisfies InspectorLogger
|
|
24
|
+
|
|
25
|
+
const inspectSource = async (source: string) => {
|
|
26
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-scope-'))
|
|
27
|
+
const file = join(rootDir, 'scopes.ts')
|
|
28
|
+
await writeFile(file, source)
|
|
29
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
30
|
+
try {
|
|
31
|
+
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
32
|
+
return { state, criticals, file }
|
|
33
|
+
} finally {
|
|
34
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('addScope inspector', () => {
|
|
39
|
+
test('extracts a flat scope', async () => {
|
|
40
|
+
const { state, criticals } = await inspectSource(
|
|
41
|
+
[
|
|
42
|
+
"import { wireScope } from '@pikku/core/scope'",
|
|
43
|
+
'wireScope({',
|
|
44
|
+
' admin: {',
|
|
45
|
+
" displayName: 'Administration',",
|
|
46
|
+
" description: 'Administrative access',",
|
|
47
|
+
' },',
|
|
48
|
+
'})',
|
|
49
|
+
].join('\n')
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert.equal(criticals.length, 0)
|
|
53
|
+
assert.equal(state.scopes.definitions.length, 1)
|
|
54
|
+
assert.equal(state.scopes.definitions[0]!.name, 'admin')
|
|
55
|
+
assert.equal(state.scopes.definitions[0]!.displayName, 'Administration')
|
|
56
|
+
assert.equal(
|
|
57
|
+
state.scopes.definitions[0]!.description,
|
|
58
|
+
'Administrative access'
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('extracts a displayName at every depth', async () => {
|
|
63
|
+
const { state, criticals } = await inspectSource(
|
|
64
|
+
[
|
|
65
|
+
"import { wireScope } from '@pikku/core/scope'",
|
|
66
|
+
'wireScope({',
|
|
67
|
+
' admin: {',
|
|
68
|
+
" displayName: 'Administration',",
|
|
69
|
+
' scopes: {',
|
|
70
|
+
' invoices: {',
|
|
71
|
+
" displayName: 'Invoice Management',",
|
|
72
|
+
' scopes: {',
|
|
73
|
+
" create: { displayName: 'Create Invoices' },",
|
|
74
|
+
' },',
|
|
75
|
+
' },',
|
|
76
|
+
' },',
|
|
77
|
+
' },',
|
|
78
|
+
'})',
|
|
79
|
+
].join('\n')
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert.equal(criticals.length, 0)
|
|
83
|
+
const admin = state.scopes.definitions[0]!
|
|
84
|
+
assert.equal(admin.displayName, 'Administration')
|
|
85
|
+
const invoices = admin.scopes!.invoices!
|
|
86
|
+
assert.equal(invoices.displayName, 'Invoice Management')
|
|
87
|
+
assert.equal(invoices.scopes!.create!.displayName, 'Create Invoices')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('extracts a nested scope tree', async () => {
|
|
91
|
+
const { state, criticals } = await inspectSource(
|
|
92
|
+
[
|
|
93
|
+
"import { wireScope } from '@pikku/core/scope'",
|
|
94
|
+
'wireScope({',
|
|
95
|
+
' admin: {',
|
|
96
|
+
' scopes: {',
|
|
97
|
+
' invoices: {',
|
|
98
|
+
" description: 'Invoice management',",
|
|
99
|
+
' scopes: {',
|
|
100
|
+
" create: { description: 'Create invoices' },",
|
|
101
|
+
' },',
|
|
102
|
+
' },',
|
|
103
|
+
' },',
|
|
104
|
+
' },',
|
|
105
|
+
'})',
|
|
106
|
+
].join('\n')
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
assert.equal(criticals.length, 0)
|
|
110
|
+
assert.deepEqual(state.scopes.definitions[0]!.scopes, {
|
|
111
|
+
invoices: {
|
|
112
|
+
description: 'Invoice management',
|
|
113
|
+
scopes: {
|
|
114
|
+
create: { description: 'Create invoices' },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('records the source file', async () => {
|
|
121
|
+
const { state, file } = await inspectSource(
|
|
122
|
+
[
|
|
123
|
+
"import { wireScope } from '@pikku/core/scope'",
|
|
124
|
+
'wireScope({ admin: {} })',
|
|
125
|
+
].join('\n')
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
assert.equal(state.scopes.definitions[0]!.sourceFile, file)
|
|
129
|
+
assert.ok(state.scopes.files.has(file))
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// The point of the keyed form: one call declares as many roots as you like,
|
|
133
|
+
// and a root reads exactly like the nodes beneath it.
|
|
134
|
+
test('extracts several roots from one declaration', async () => {
|
|
135
|
+
const { state, criticals } = await inspectSource(
|
|
136
|
+
[
|
|
137
|
+
"import { wireScope } from '@pikku/core/scope'",
|
|
138
|
+
'wireScope({ admin: {}, billing: { scopes: { read: {} } } })',
|
|
139
|
+
].join('\n')
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
assert.equal(criticals.length, 0)
|
|
143
|
+
assert.deepEqual(
|
|
144
|
+
state.scopes.definitions.map((d) => d.name),
|
|
145
|
+
['admin', 'billing']
|
|
146
|
+
)
|
|
147
|
+
assert.deepEqual(state.scopes.definitions[1]!.scopes, { read: {} })
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('extracts several declarations', async () => {
|
|
151
|
+
const { state } = await inspectSource(
|
|
152
|
+
[
|
|
153
|
+
"import { wireScope } from '@pikku/core/scope'",
|
|
154
|
+
'wireScope({ admin: {} })',
|
|
155
|
+
'wireScope({ billing: {} })',
|
|
156
|
+
].join('\n')
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
assert.deepEqual(
|
|
160
|
+
state.scopes.definitions.map((d) => d.name),
|
|
161
|
+
['admin', 'billing']
|
|
162
|
+
)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('is critical when a root embeds the separator', async () => {
|
|
166
|
+
const { criticals } = await inspectSource(
|
|
167
|
+
[
|
|
168
|
+
"import { wireScope } from '@pikku/core/scope'",
|
|
169
|
+
"wireScope({ 'admin:users': {} })",
|
|
170
|
+
].join('\n')
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
assert.ok(
|
|
174
|
+
criticals.some((c) => c.code === ErrorCode.INVALID_VALUE),
|
|
175
|
+
`expected an INVALID_VALUE critical, got ${JSON.stringify(criticals)}`
|
|
176
|
+
)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('is critical when a root is the wildcard', async () => {
|
|
180
|
+
const { criticals } = await inspectSource(
|
|
181
|
+
[
|
|
182
|
+
"import { wireScope } from '@pikku/core/scope'",
|
|
183
|
+
"wireScope({ '*': {} })",
|
|
184
|
+
].join('\n')
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
assert.ok(
|
|
188
|
+
criticals.some((c) => c.code === ErrorCode.INVALID_VALUE),
|
|
189
|
+
`expected an INVALID_VALUE critical, got ${JSON.stringify(criticals)}`
|
|
190
|
+
)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('is critical when a root key is not a literal', async () => {
|
|
194
|
+
const { criticals } = await inspectSource(
|
|
195
|
+
[
|
|
196
|
+
"import { wireScope } from '@pikku/core/scope'",
|
|
197
|
+
'const k = String(1)',
|
|
198
|
+
'wireScope({ [k]: {} } as any)',
|
|
199
|
+
].join('\n')
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
assert.ok(
|
|
203
|
+
criticals.some((c) => c.code === ErrorCode.NON_LITERAL_WIRE_NAME),
|
|
204
|
+
`expected a NON_LITERAL_WIRE_NAME critical, got ${JSON.stringify(criticals)}`
|
|
205
|
+
)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('is critical when a root is not an object literal', async () => {
|
|
209
|
+
const { criticals } = await inspectSource(
|
|
210
|
+
[
|
|
211
|
+
"import { wireScope } from '@pikku/core/scope'",
|
|
212
|
+
"wireScope({ admin: 'nope' } as any)",
|
|
213
|
+
].join('\n')
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
assert.ok(
|
|
217
|
+
criticals.some((c) => c.code === ErrorCode.INVALID_VALUE),
|
|
218
|
+
`expected an INVALID_VALUE critical, got ${JSON.stringify(criticals)}`
|
|
219
|
+
)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('validateScopeReferences', () => {
|
|
224
|
+
const funcSource = (scopes: string, decls: string[] = []) =>
|
|
225
|
+
[
|
|
226
|
+
"import { wireScope } from '@pikku/core/scope'",
|
|
227
|
+
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
228
|
+
...decls,
|
|
229
|
+
'export const f = pikkuSessionlessFunc({',
|
|
230
|
+
` scopes: ${scopes},`,
|
|
231
|
+
" func: async () => 'ok',",
|
|
232
|
+
'})',
|
|
233
|
+
].join('\n')
|
|
234
|
+
|
|
235
|
+
test('accepts a declared scope', async () => {
|
|
236
|
+
const { criticals } = await inspectSource(
|
|
237
|
+
funcSource("['admin:invoices:create']", [
|
|
238
|
+
'wireScope({',
|
|
239
|
+
' admin: {',
|
|
240
|
+
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
241
|
+
' },',
|
|
242
|
+
'})',
|
|
243
|
+
])
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
assert.deepEqual(criticals, [])
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('accepts an intermediate node of a declared tree', async () => {
|
|
250
|
+
const { criticals } = await inspectSource(
|
|
251
|
+
funcSource("['admin:invoices']", [
|
|
252
|
+
'wireScope({',
|
|
253
|
+
' admin: {',
|
|
254
|
+
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
255
|
+
' },',
|
|
256
|
+
'})',
|
|
257
|
+
])
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
assert.deepEqual(criticals, [])
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('rejects an undeclared scope', async () => {
|
|
264
|
+
const { criticals } = await inspectSource(
|
|
265
|
+
funcSource("['billing:read']", ['wireScope({ admin: {} })'])
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
assert.ok(
|
|
269
|
+
criticals.some(
|
|
270
|
+
(c) =>
|
|
271
|
+
c.code === ErrorCode.INVALID_VALUE &&
|
|
272
|
+
c.message.includes('billing:read')
|
|
273
|
+
),
|
|
274
|
+
`expected an INVALID_VALUE critical for billing:read, got ${JSON.stringify(criticals)}`
|
|
275
|
+
)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('lists the available scopes when one is undeclared', async () => {
|
|
279
|
+
const { criticals } = await inspectSource(
|
|
280
|
+
funcSource("['nope']", ['wireScope({ admin: {} })'])
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
assert.ok(
|
|
284
|
+
criticals.some((c) => c.message.includes('Available scopes: admin')),
|
|
285
|
+
`expected the message to list available scopes, got ${JSON.stringify(criticals)}`
|
|
286
|
+
)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test('rejects a typo in a nested scope', async () => {
|
|
290
|
+
const { criticals } = await inspectSource(
|
|
291
|
+
funcSource("['admin:invoice:create']", [
|
|
292
|
+
'wireScope({',
|
|
293
|
+
' admin: {',
|
|
294
|
+
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
295
|
+
' },',
|
|
296
|
+
'})',
|
|
297
|
+
])
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
assert.ok(
|
|
301
|
+
criticals.some((c) => c.message.includes('admin:invoice:create')),
|
|
302
|
+
`expected a critical for the typo, got ${JSON.stringify(criticals)}`
|
|
303
|
+
)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test('accepts a wildcard requirement whose node is declared', async () => {
|
|
307
|
+
const { criticals } = await inspectSource(
|
|
308
|
+
funcSource("['admin:*']", ['wireScope({ admin: {} })'])
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
assert.deepEqual(criticals, [])
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('rejects a wildcard requirement whose node is undeclared', async () => {
|
|
315
|
+
const { criticals } = await inspectSource(
|
|
316
|
+
funcSource("['billing:*']", ['wireScope({ admin: {} })'])
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
assert.ok(
|
|
320
|
+
criticals.some((c) => c.message.includes('billing:*')),
|
|
321
|
+
`expected a critical for billing:*, got ${JSON.stringify(criticals)}`
|
|
322
|
+
)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
test('rejects a bare wildcard requirement', async () => {
|
|
326
|
+
const { criticals } = await inspectSource(
|
|
327
|
+
funcSource("['*']", ['wireScope({ admin: {} })'])
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
assert.ok(
|
|
331
|
+
criticals.some((c) => c.message.includes('bare wildcard')),
|
|
332
|
+
`expected a critical for the bare wildcard, got ${JSON.stringify(criticals)}`
|
|
333
|
+
)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
test('is silent for a function declaring no scopes', async () => {
|
|
337
|
+
const { criticals } = await inspectSource(
|
|
338
|
+
[
|
|
339
|
+
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
340
|
+
"export const f = pikkuSessionlessFunc({ func: async () => 'ok' })",
|
|
341
|
+
].join('\n')
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
assert.deepEqual(criticals, [])
|
|
345
|
+
})
|
|
346
|
+
})
|