@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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import { getPropertyValue } from '../utils/get-property-value.js'
|
|
3
|
+
import type { AddWiring, InspectorLogger } from '../types.js'
|
|
4
|
+
import { ErrorCode } from '../error-codes.js'
|
|
5
|
+
import type { ScopeNodeMeta } from '@pikku/core/scope'
|
|
6
|
+
|
|
7
|
+
const SEPARATOR = ':'
|
|
8
|
+
const WILDCARD = '*'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validates a single scope segment, reporting rather than throwing so the
|
|
12
|
+
* inspector can surface every problem in one pass.
|
|
13
|
+
*
|
|
14
|
+
* @returns true when the segment is usable.
|
|
15
|
+
*/
|
|
16
|
+
const isValidSegment = (
|
|
17
|
+
segment: string,
|
|
18
|
+
scopeName: string,
|
|
19
|
+
logger: InspectorLogger
|
|
20
|
+
): boolean => {
|
|
21
|
+
if (segment.length === 0) {
|
|
22
|
+
logger.critical(
|
|
23
|
+
ErrorCode.INVALID_VALUE,
|
|
24
|
+
`Scope '${scopeName}' contains an empty segment.`
|
|
25
|
+
)
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
if (segment.includes(SEPARATOR)) {
|
|
29
|
+
logger.critical(
|
|
30
|
+
ErrorCode.INVALID_VALUE,
|
|
31
|
+
`Scope segment '${segment}' in '${scopeName}' contains the '${SEPARATOR}' separator. ` +
|
|
32
|
+
`Nest scopes with the 'scopes' property instead of embedding '${SEPARATOR}' in a name.`
|
|
33
|
+
)
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
if (segment === WILDCARD) {
|
|
37
|
+
logger.critical(
|
|
38
|
+
ErrorCode.INVALID_VALUE,
|
|
39
|
+
`Scope segment '${segment}' in '${scopeName}' is the wildcard. ` +
|
|
40
|
+
`'${WILDCARD}' is reserved for granting a scope and its descendants, and cannot be declared.`
|
|
41
|
+
)
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Recursively extracts a `scopes: { ... }` object literal into nested metadata.
|
|
49
|
+
*/
|
|
50
|
+
const extractScopeNodes = (
|
|
51
|
+
obj: ts.ObjectLiteralExpression,
|
|
52
|
+
scopeName: string,
|
|
53
|
+
logger: InspectorLogger
|
|
54
|
+
): Record<string, ScopeNodeMeta> | undefined => {
|
|
55
|
+
const nodes: Record<string, ScopeNodeMeta> = {}
|
|
56
|
+
|
|
57
|
+
for (const prop of obj.properties) {
|
|
58
|
+
if (!ts.isPropertyAssignment(prop)) {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let segment: string | undefined
|
|
63
|
+
if (ts.isIdentifier(prop.name)) {
|
|
64
|
+
segment = prop.name.text
|
|
65
|
+
} else if (ts.isStringLiteral(prop.name)) {
|
|
66
|
+
segment = prop.name.text
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (segment === undefined) {
|
|
70
|
+
logger.critical(
|
|
71
|
+
ErrorCode.NON_LITERAL_WIRE_NAME,
|
|
72
|
+
`Scope '${scopeName}' has a nested scope whose key is not a literal.`
|
|
73
|
+
)
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!isValidSegment(segment, scopeName, logger)) {
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!ts.isObjectLiteralExpression(prop.initializer)) {
|
|
82
|
+
logger.critical(
|
|
83
|
+
ErrorCode.INVALID_VALUE,
|
|
84
|
+
`Nested scope '${segment}' in '${scopeName}' must be an object literal.`
|
|
85
|
+
)
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const node: ScopeNodeMeta = {}
|
|
90
|
+
const displayName = getPropertyValue(prop.initializer, 'displayName') as
|
|
91
|
+
| string
|
|
92
|
+
| null
|
|
93
|
+
if (displayName) {
|
|
94
|
+
node.displayName = displayName
|
|
95
|
+
}
|
|
96
|
+
const description = getPropertyValue(prop.initializer, 'description') as
|
|
97
|
+
| string
|
|
98
|
+
| null
|
|
99
|
+
if (description) {
|
|
100
|
+
node.description = description
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const nestedProp = prop.initializer.properties.find(
|
|
104
|
+
(p) =>
|
|
105
|
+
ts.isPropertyAssignment(p) &&
|
|
106
|
+
ts.isIdentifier(p.name) &&
|
|
107
|
+
p.name.text === 'scopes'
|
|
108
|
+
)
|
|
109
|
+
if (
|
|
110
|
+
nestedProp &&
|
|
111
|
+
ts.isPropertyAssignment(nestedProp) &&
|
|
112
|
+
ts.isObjectLiteralExpression(nestedProp.initializer)
|
|
113
|
+
) {
|
|
114
|
+
const nested = extractScopeNodes(
|
|
115
|
+
nestedProp.initializer,
|
|
116
|
+
scopeName,
|
|
117
|
+
logger
|
|
118
|
+
)
|
|
119
|
+
if (nested) {
|
|
120
|
+
node.scopes = nested
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
nodes[segment] = node
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return Object.keys(nodes).length > 0 ? nodes : undefined
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Unwraps `x as const` / `x as any` so a cast declaration is still extracted
|
|
132
|
+
* rather than silently skipped.
|
|
133
|
+
*/
|
|
134
|
+
const unwrapAs = (node: ts.Expression): ts.Expression =>
|
|
135
|
+
ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
|
|
136
|
+
? unwrapAs(node.expression)
|
|
137
|
+
: node
|
|
138
|
+
|
|
139
|
+
export const addScope: AddWiring = (logger, node, checker, state, _options) => {
|
|
140
|
+
if (!ts.isCallExpression(node)) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const expression = node.expression
|
|
145
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'wireScope') {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const firstArg = node.arguments[0]
|
|
150
|
+
if (!firstArg) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const unwrapped = unwrapAs(firstArg)
|
|
155
|
+
if (!ts.isObjectLiteralExpression(unwrapped)) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const sourceFile = node.getSourceFile().fileName
|
|
160
|
+
|
|
161
|
+
// Roots are keyed exactly like the nodes beneath them, so each property of
|
|
162
|
+
// the call's single argument is one tree.
|
|
163
|
+
for (const prop of unwrapped.properties) {
|
|
164
|
+
if (!ts.isPropertyAssignment(prop)) {
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let name: string | undefined
|
|
169
|
+
if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
|
|
170
|
+
name = prop.name.text
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (name === undefined) {
|
|
174
|
+
logger.critical(
|
|
175
|
+
ErrorCode.NON_LITERAL_WIRE_NAME,
|
|
176
|
+
'A scope is declared with a key that is not a literal.'
|
|
177
|
+
)
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!isValidSegment(name, name, logger)) {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!ts.isObjectLiteralExpression(prop.initializer)) {
|
|
186
|
+
logger.critical(
|
|
187
|
+
ErrorCode.INVALID_VALUE,
|
|
188
|
+
`Scope '${name}' must be an object literal.`
|
|
189
|
+
)
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const root = prop.initializer
|
|
194
|
+
const displayNameValue = getPropertyValue(root, 'displayName') as
|
|
195
|
+
| string
|
|
196
|
+
| null
|
|
197
|
+
const descriptionValue = getPropertyValue(root, 'description') as
|
|
198
|
+
| string
|
|
199
|
+
| null
|
|
200
|
+
|
|
201
|
+
let scopes: Record<string, ScopeNodeMeta> | undefined
|
|
202
|
+
const scopesProp = root.properties.find(
|
|
203
|
+
(p) =>
|
|
204
|
+
ts.isPropertyAssignment(p) &&
|
|
205
|
+
ts.isIdentifier(p.name) &&
|
|
206
|
+
p.name.text === 'scopes'
|
|
207
|
+
)
|
|
208
|
+
if (
|
|
209
|
+
scopesProp &&
|
|
210
|
+
ts.isPropertyAssignment(scopesProp) &&
|
|
211
|
+
ts.isObjectLiteralExpression(scopesProp.initializer)
|
|
212
|
+
) {
|
|
213
|
+
scopes = extractScopeNodes(scopesProp.initializer, name, logger)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
state.scopes.files.add(sourceFile)
|
|
217
|
+
state.scopes.definitions.push({
|
|
218
|
+
name,
|
|
219
|
+
displayName: displayNameValue || undefined,
|
|
220
|
+
description: descriptionValue || undefined,
|
|
221
|
+
scopes,
|
|
222
|
+
sourceFile,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
}
|
package/src/add/add-trigger.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'
|
|
@@ -83,6 +84,16 @@ const addWireTrigger: (
|
|
|
83
84
|
return
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// Register metadata for a func inlined into the wiring (see helper).
|
|
88
|
+
ensureInlineWiringFunction(
|
|
89
|
+
state,
|
|
90
|
+
pikkuFuncId,
|
|
91
|
+
nameValue,
|
|
92
|
+
funcInitializer,
|
|
93
|
+
checker,
|
|
94
|
+
extracted.isHelper
|
|
95
|
+
)
|
|
96
|
+
|
|
86
97
|
// --- resolve middleware ---
|
|
87
98
|
const middleware = resolveMiddleware(state, obj, tags, checker)
|
|
88
99
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import type { InspectorState, InspectorLogger } from '../types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detect wireRemoteAddon({ name: '...', package: '...' }) call expressions and
|
|
6
|
+
* record the namespace as a **remote** addon declaration. Only name + package
|
|
7
|
+
* are needed for codegen (serverUrl/auth are runtime closures resolved by the
|
|
8
|
+
* RPC runner); the `remote` marker tells the consumer's RPC-map generator to
|
|
9
|
+
* import the addon's `.remote.gen` map instead of `.internal.gen`.
|
|
10
|
+
*/
|
|
11
|
+
export function addWireRemoteAddon(
|
|
12
|
+
node: ts.Node,
|
|
13
|
+
state: InspectorState,
|
|
14
|
+
logger: InspectorLogger
|
|
15
|
+
) {
|
|
16
|
+
if (!ts.isCallExpression(node)) return
|
|
17
|
+
|
|
18
|
+
const { expression, arguments: args } = node
|
|
19
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'wireRemoteAddon')
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
const [firstArg] = args
|
|
23
|
+
if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
|
|
24
|
+
|
|
25
|
+
let name: string | undefined
|
|
26
|
+
let pkg: string | undefined
|
|
27
|
+
// The auth binding is a runtime closure; we only extract the statically
|
|
28
|
+
// knowable slot names so `pikku verify` can check they exist in the
|
|
29
|
+
// consumer's own wirings. `hasAuth` distinguishes "public" (omitted) from
|
|
30
|
+
// "bound via a custom resolve()" (present but no static id).
|
|
31
|
+
let hasAuth = false
|
|
32
|
+
let authCredentialId: string | undefined
|
|
33
|
+
let authSecretId: string | undefined
|
|
34
|
+
|
|
35
|
+
for (const prop of firstArg.properties) {
|
|
36
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue
|
|
37
|
+
const key = prop.name.text
|
|
38
|
+
if (key === 'name' && ts.isStringLiteral(prop.initializer)) {
|
|
39
|
+
name = prop.initializer.text
|
|
40
|
+
} else if (key === 'package' && ts.isStringLiteral(prop.initializer)) {
|
|
41
|
+
pkg = prop.initializer.text
|
|
42
|
+
} else if (
|
|
43
|
+
key === 'auth' &&
|
|
44
|
+
ts.isObjectLiteralExpression(prop.initializer)
|
|
45
|
+
) {
|
|
46
|
+
hasAuth = true
|
|
47
|
+
for (const authProp of prop.initializer.properties) {
|
|
48
|
+
if (!ts.isPropertyAssignment(authProp) || !ts.isIdentifier(authProp.name))
|
|
49
|
+
continue
|
|
50
|
+
if (
|
|
51
|
+
authProp.name.text === 'credentialId' &&
|
|
52
|
+
ts.isStringLiteral(authProp.initializer)
|
|
53
|
+
) {
|
|
54
|
+
authCredentialId = authProp.initializer.text
|
|
55
|
+
} else if (
|
|
56
|
+
authProp.name.text === 'secretId' &&
|
|
57
|
+
ts.isStringLiteral(authProp.initializer)
|
|
58
|
+
) {
|
|
59
|
+
authSecretId = authProp.initializer.text
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!name || !pkg) return
|
|
66
|
+
|
|
67
|
+
logger.debug(`• Found wireRemoteAddon: ${name} → ${pkg} (remote)`)
|
|
68
|
+
state.rpc.wireAddonDeclarations.set(name, {
|
|
69
|
+
package: pkg,
|
|
70
|
+
remote: true,
|
|
71
|
+
hasAuth,
|
|
72
|
+
authCredentialId,
|
|
73
|
+
authSecretId,
|
|
74
|
+
})
|
|
75
|
+
state.rpc.usedAddons.add(name)
|
|
76
|
+
state.rpc.wireAddonFiles.add(node.getSourceFile().fileName)
|
|
77
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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 type { InspectorLogger } from '../types.js'
|
|
8
|
+
|
|
9
|
+
function makeLogger(): InspectorLogger {
|
|
10
|
+
return {
|
|
11
|
+
debug: () => {},
|
|
12
|
+
info: () => {},
|
|
13
|
+
warn: () => {},
|
|
14
|
+
error: () => {},
|
|
15
|
+
diagnostic: () => {},
|
|
16
|
+
critical: () => {},
|
|
17
|
+
hasCriticalErrors: () => false,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('addWorkflowGraph — node input extraction', () => {
|
|
22
|
+
test('`as const` and imported template() values survive into the serialized input', async () => {
|
|
23
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-graph-input-'))
|
|
24
|
+
const stepFile = join(rootDir, 'my.steps.ts')
|
|
25
|
+
const graphFile = join(rootDir, 'my.graph.ts')
|
|
26
|
+
|
|
27
|
+
await writeFile(
|
|
28
|
+
stepFile,
|
|
29
|
+
[
|
|
30
|
+
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
31
|
+
'export const edit = pikkuSessionlessFunc({ func: async () => ({ ok: true }) })',
|
|
32
|
+
].join('\n')
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
await writeFile(
|
|
36
|
+
graphFile,
|
|
37
|
+
[
|
|
38
|
+
"import { pikkuWorkflowGraph } from '@pikku/core/workflow'",
|
|
39
|
+
"import { template } from '@pikku/core/workflow'",
|
|
40
|
+
'export const myGraph = pikkuWorkflowGraph({',
|
|
41
|
+
" name: 'input-graph',",
|
|
42
|
+
" nodes: { edit: 'edit' },",
|
|
43
|
+
' config: {',
|
|
44
|
+
' edit: {',
|
|
45
|
+
' input: (ref) => ({',
|
|
46
|
+
" method: 'GET' as const,",
|
|
47
|
+
' operations: [',
|
|
48
|
+
" { field: 'product', operation: 'set' as const, value: 'widget' },",
|
|
49
|
+
" { field: 'email', operation: 'set' as const, value: ref('trigger', 'body.email') },",
|
|
50
|
+
" { field: 'greeting', operation: 'set' as const, value: template('Hi $0', [ref('trigger', 'body.name')]) },",
|
|
51
|
+
' ],',
|
|
52
|
+
' }),',
|
|
53
|
+
' },',
|
|
54
|
+
' },',
|
|
55
|
+
'})',
|
|
56
|
+
].join('\n')
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const state = await inspect(makeLogger(), [stepFile, graphFile], {
|
|
61
|
+
rootDir,
|
|
62
|
+
})
|
|
63
|
+
const graph = state.workflows.graphMeta['input-graph']
|
|
64
|
+
assert.ok(graph, 'graph meta should be registered')
|
|
65
|
+
const input = (graph.nodes['edit'] as { input?: any }).input
|
|
66
|
+
assert.ok(input, 'node input should be extracted')
|
|
67
|
+
|
|
68
|
+
// Bug 1 — top-level `X as const` must not be dropped.
|
|
69
|
+
assert.equal(input.method, 'GET', 'top-level `as const` value preserved')
|
|
70
|
+
|
|
71
|
+
// Bug 1 — nested `as const` inside an array element must not be dropped.
|
|
72
|
+
assert.equal(input.operations[0].operation, 'set')
|
|
73
|
+
assert.equal(input.operations[0].value, 'widget')
|
|
74
|
+
assert.equal(input.operations[1].operation, 'set')
|
|
75
|
+
|
|
76
|
+
// ref() still works alongside the fix.
|
|
77
|
+
assert.deepEqual(input.operations[1].value, {
|
|
78
|
+
$ref: 'trigger',
|
|
79
|
+
path: 'body.email',
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Bug 2 — imported template() (arrow has only `ref`, no 2nd param) preserved.
|
|
83
|
+
assert.equal(input.operations[2].operation, 'set')
|
|
84
|
+
assert.deepEqual(input.operations[2].value, {
|
|
85
|
+
$template: {
|
|
86
|
+
parts: ['Hi ', ''],
|
|
87
|
+
expressions: [{ $ref: 'trigger', path: 'body.name' }],
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
} finally {
|
|
91
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
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 type { InspectorLogger } from '../types.js'
|
|
8
|
+
|
|
9
|
+
function makeLogger(): InspectorLogger {
|
|
10
|
+
return {
|
|
11
|
+
debug: () => {},
|
|
12
|
+
info: () => {},
|
|
13
|
+
warn: () => {},
|
|
14
|
+
error: () => {},
|
|
15
|
+
diagnostic: () => {},
|
|
16
|
+
critical: () => {},
|
|
17
|
+
hasCriticalErrors: () => false,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('addWorkflowGraph — notes extraction', () => {
|
|
22
|
+
test('node-level and graph-level notes are carried into the serialized graph', async () => {
|
|
23
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-graph-notes-'))
|
|
24
|
+
const stepFile = join(rootDir, 'my.steps.ts')
|
|
25
|
+
const graphFile = join(rootDir, 'my.graph.ts')
|
|
26
|
+
|
|
27
|
+
await writeFile(
|
|
28
|
+
stepFile,
|
|
29
|
+
[
|
|
30
|
+
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
31
|
+
'export const assess = pikkuSessionlessFunc({ func: async () => ({ ok: true }) })',
|
|
32
|
+
'export const notify = pikkuSessionlessFunc({ func: async () => ({ sent: true }) })',
|
|
33
|
+
].join('\n')
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
await writeFile(
|
|
37
|
+
graphFile,
|
|
38
|
+
[
|
|
39
|
+
"import { pikkuWorkflowGraph } from '@pikku/core/workflow'",
|
|
40
|
+
'export const myGraph = pikkuWorkflowGraph({',
|
|
41
|
+
" name: 'my-graph',",
|
|
42
|
+
" notes: ['imported sticky note'],",
|
|
43
|
+
' nodes: {',
|
|
44
|
+
" assess: 'assess',",
|
|
45
|
+
" notify: 'notify',",
|
|
46
|
+
' },',
|
|
47
|
+
' config: {',
|
|
48
|
+
" assess: { next: 'notify', notes: 'STUB — generated from node \"Assess\"' },",
|
|
49
|
+
' notify: {},',
|
|
50
|
+
' },',
|
|
51
|
+
'})',
|
|
52
|
+
].join('\n')
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const state = await inspect(makeLogger(), [stepFile, graphFile], {
|
|
57
|
+
rootDir,
|
|
58
|
+
})
|
|
59
|
+
const graph = state.workflows.graphMeta['my-graph']
|
|
60
|
+
assert.ok(graph, 'graph meta should be registered')
|
|
61
|
+
assert.deepEqual(graph.notes, ['imported sticky note'])
|
|
62
|
+
assert.equal(
|
|
63
|
+
(graph.nodes['assess'] as { notes?: string }).notes,
|
|
64
|
+
'STUB — generated from node "Assess"'
|
|
65
|
+
)
|
|
66
|
+
assert.equal(
|
|
67
|
+
(graph.nodes['notify'] as { notes?: string }).notes,
|
|
68
|
+
undefined
|
|
69
|
+
)
|
|
70
|
+
} finally {
|
|
71
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -12,6 +12,11 @@ function extractAstValue(
|
|
|
12
12
|
refParamName: string,
|
|
13
13
|
templateParamName: string | undefined
|
|
14
14
|
): unknown {
|
|
15
|
+
// `'GET' as const` / parenthesised values wrap the real initializer — see
|
|
16
|
+
// through them or the whole property is silently dropped from the meta.
|
|
17
|
+
while (ts.isAsExpression(expr) || ts.isParenthesizedExpression(expr)) {
|
|
18
|
+
expr = expr.expression
|
|
19
|
+
}
|
|
15
20
|
if (ts.isStringLiteral(expr)) {
|
|
16
21
|
return expr.text
|
|
17
22
|
}
|
|
@@ -146,10 +151,14 @@ function extractInputMapping(
|
|
|
146
151
|
? node.parameters[0].name.text
|
|
147
152
|
: 'ref'
|
|
148
153
|
|
|
154
|
+
// The canonical form imports `template` from '@pikku/core/workflow' and calls
|
|
155
|
+
// it inside a single-param `(ref) => (...)` arrow; the older form passes it as
|
|
156
|
+
// a second arrow parameter. Support both — default to the imported name so a
|
|
157
|
+
// `template(...)` value is never dropped just because it isn't a 2nd param.
|
|
149
158
|
const templateParamName =
|
|
150
159
|
node.parameters.length > 1 && ts.isIdentifier(node.parameters[1].name)
|
|
151
160
|
? node.parameters[1].name.text
|
|
152
|
-
:
|
|
161
|
+
: 'template'
|
|
153
162
|
|
|
154
163
|
const input: Record<string, unknown | DataRef> = {}
|
|
155
164
|
|
|
@@ -184,6 +193,13 @@ function extractNextConfig(
|
|
|
184
193
|
node: ts.Node,
|
|
185
194
|
_checker: ts.TypeChecker
|
|
186
195
|
): string | string[] | Record<string, string | string[]> | undefined {
|
|
196
|
+
// Key-based branch `next` (`{ key: [targets] }`) is authored with an `as any`
|
|
197
|
+
// cast because the graph's NextConfig can't narrow record targets to node-id
|
|
198
|
+
// literals; see through that (and parens) so the routing survives into the meta.
|
|
199
|
+
while (ts.isAsExpression(node) || ts.isParenthesizedExpression(node)) {
|
|
200
|
+
node = node.expression
|
|
201
|
+
}
|
|
202
|
+
|
|
187
203
|
if (ts.isStringLiteral(node)) {
|
|
188
204
|
return node.text
|
|
189
205
|
}
|
|
@@ -254,6 +270,7 @@ interface PikkuWorkflowGraphExtract {
|
|
|
254
270
|
name?: string
|
|
255
271
|
description?: string
|
|
256
272
|
tags?: string[]
|
|
273
|
+
notes?: string[]
|
|
257
274
|
disabled?: true
|
|
258
275
|
nodesNode?: ts.ObjectLiteralExpression
|
|
259
276
|
configNode?: ts.ObjectLiteralExpression
|
|
@@ -270,6 +287,7 @@ function extractWorkflowGraphConfig(
|
|
|
270
287
|
let name: string | undefined
|
|
271
288
|
let description: string | undefined
|
|
272
289
|
let tags: string[] | undefined
|
|
290
|
+
let notes: string[] | undefined
|
|
273
291
|
let disabled: true | undefined
|
|
274
292
|
let nodesNode: ts.ObjectLiteralExpression | undefined
|
|
275
293
|
let configNode: ts.ObjectLiteralExpression | undefined
|
|
@@ -293,6 +311,13 @@ function extractWorkflowGraphConfig(
|
|
|
293
311
|
tags = prop.initializer.elements
|
|
294
312
|
.filter(ts.isStringLiteral)
|
|
295
313
|
.map((el) => (el as ts.StringLiteral).text)
|
|
314
|
+
} else if (
|
|
315
|
+
propName === 'notes' &&
|
|
316
|
+
ts.isArrayLiteralExpression(prop.initializer)
|
|
317
|
+
) {
|
|
318
|
+
notes = prop.initializer.elements
|
|
319
|
+
.filter(ts.isStringLiteral)
|
|
320
|
+
.map((el) => (el as ts.StringLiteral).text)
|
|
296
321
|
} else if (
|
|
297
322
|
propName === 'nodes' &&
|
|
298
323
|
ts.isObjectLiteralExpression(prop.initializer)
|
|
@@ -306,7 +331,7 @@ function extractWorkflowGraphConfig(
|
|
|
306
331
|
}
|
|
307
332
|
}
|
|
308
333
|
|
|
309
|
-
return { name, description, tags, disabled, nodesNode, configNode }
|
|
334
|
+
return { name, description, tags, notes, disabled, nodesNode, configNode }
|
|
310
335
|
}
|
|
311
336
|
|
|
312
337
|
/**
|
|
@@ -359,6 +384,7 @@ function extractGraphFromNewFormat(
|
|
|
359
384
|
onError: undefined,
|
|
360
385
|
retries: undefined,
|
|
361
386
|
retryDelay: undefined,
|
|
387
|
+
notes: undefined,
|
|
362
388
|
}
|
|
363
389
|
}
|
|
364
390
|
|
|
@@ -386,6 +412,7 @@ function extractGraphFromNewFormat(
|
|
|
386
412
|
nodes[nodeId].input = nodeConfig.input
|
|
387
413
|
nodes[nodeId].retries = nodeConfig.retries
|
|
388
414
|
nodes[nodeId].retryDelay = nodeConfig.retryDelay
|
|
415
|
+
nodes[nodeId].notes = nodeConfig.notes
|
|
389
416
|
}
|
|
390
417
|
}
|
|
391
418
|
}
|
|
@@ -406,12 +433,14 @@ function extractNodeConfigFromObject(
|
|
|
406
433
|
input: Record<string, any>
|
|
407
434
|
retries: number | undefined
|
|
408
435
|
retryDelay: string | number | undefined
|
|
436
|
+
notes: string | undefined
|
|
409
437
|
} {
|
|
410
438
|
let next: any = undefined
|
|
411
439
|
let onError: any = undefined
|
|
412
440
|
let input: Record<string, any> = {}
|
|
413
441
|
let retries: number | undefined = undefined
|
|
414
442
|
let retryDelay: string | number | undefined = undefined
|
|
443
|
+
let notes: string | undefined = undefined
|
|
415
444
|
|
|
416
445
|
for (const prop of obj.properties) {
|
|
417
446
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue
|
|
@@ -434,10 +463,12 @@ function extractNodeConfigFromObject(
|
|
|
434
463
|
} else if (ts.isStringLiteral(prop.initializer)) {
|
|
435
464
|
retryDelay = prop.initializer.text
|
|
436
465
|
}
|
|
466
|
+
} else if (propName === 'notes') {
|
|
467
|
+
notes = extractStringLiteral(prop.initializer, checker)
|
|
437
468
|
}
|
|
438
469
|
}
|
|
439
470
|
|
|
440
|
-
return { next, onError, input, retries, retryDelay }
|
|
471
|
+
return { next, onError, input, retries, retryDelay, notes }
|
|
441
472
|
}
|
|
442
473
|
|
|
443
474
|
/**
|
|
@@ -526,6 +557,7 @@ export const addWorkflowGraph: AddWiring = (logger, node, checker, state) => {
|
|
|
526
557
|
source: 'graph',
|
|
527
558
|
description: graphConfig.description,
|
|
528
559
|
tags: graphConfig.tags,
|
|
560
|
+
notes: graphConfig.notes,
|
|
529
561
|
nodes: graphNodes,
|
|
530
562
|
entryNodeIds,
|
|
531
563
|
}
|