@pikku/inspector 0.12.9 → 0.12.11
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 +26 -9
- package/dist/add/add-functions.js +74 -4
- package/dist/add/add-http-route.js +20 -1
- package/dist/add/add-workflow.d.ts +1 -1
- package/dist/add/add-workflow.js +10 -10
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +1 -0
- package/dist/utils/extract-node-value.js +4 -0
- package/dist/utils/schema-generator.js +3 -3
- package/dist/utils/validate-auth-sessionless.d.ts +1 -1
- package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +5 -4
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +47 -28
- package/dist/utils/workflow/dsl/patterns.d.ts +5 -1
- package/dist/utils/workflow/dsl/patterns.js +2 -2
- package/dist/utils/workflow/dsl/validation.d.ts +4 -2
- package/dist/utils/workflow/dsl/validation.js +16 -8
- package/package.json +2 -2
- package/src/add/add-functions.test.ts +169 -0
- package/src/add/add-functions.ts +135 -8
- package/src/add/add-http-route.ts +28 -1
- package/src/add/add-workflow.ts +11 -11
- package/src/error-codes.ts +1 -0
- package/src/utils/extract-node-value.ts +5 -0
- package/src/utils/schema-generator.ts +3 -3
- package/src/utils/validate-auth-sessionless.ts +1 -1
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +58 -32
- package/src/utils/workflow/dsl/patterns.ts +2 -2
- package/src/utils/workflow/dsl/validation.ts +21 -9
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -12,7 +12,7 @@ import * as ts from 'typescript';
|
|
|
12
12
|
* - ThrowStatement (for WorkflowCancelledException)
|
|
13
13
|
* - Block (containers)
|
|
14
14
|
*/
|
|
15
|
-
export function validateNoDisallowedPatterns(node) {
|
|
15
|
+
export function validateNoDisallowedPatterns(node, options) {
|
|
16
16
|
const errors = [];
|
|
17
17
|
function visitBlock(block) {
|
|
18
18
|
for (const statement of block.statements) {
|
|
@@ -30,7 +30,7 @@ export function validateNoDisallowedPatterns(node) {
|
|
|
30
30
|
// Unknown/disallowed statement type
|
|
31
31
|
const nodeType = ts.SyntaxKind[statement.kind];
|
|
32
32
|
errors.push({
|
|
33
|
-
message: `Statement type '${nodeType}' is not allowed in
|
|
33
|
+
message: `Statement type '${nodeType}' is not allowed in DSL workflows. Allowed: const/let, if/else, switch/case, for..of, return, throw, and workflow calls. If this should be supported, please report the node type: ${nodeType}`,
|
|
34
34
|
node: statement,
|
|
35
35
|
});
|
|
36
36
|
}
|
|
@@ -40,7 +40,7 @@ export function validateNoDisallowedPatterns(node) {
|
|
|
40
40
|
// Disallow while and do-while
|
|
41
41
|
if (ts.isWhileStatement(node) || ts.isDoStatement(node)) {
|
|
42
42
|
errors.push({
|
|
43
|
-
message: 'while and do-while loops are not allowed in
|
|
43
|
+
message: 'while and do-while loops are not allowed in DSL workflows',
|
|
44
44
|
node,
|
|
45
45
|
});
|
|
46
46
|
return;
|
|
@@ -48,13 +48,13 @@ export function validateNoDisallowedPatterns(node) {
|
|
|
48
48
|
// Disallow for and for-in loops
|
|
49
49
|
if (ts.isForInStatement(node) || ts.isForStatement(node)) {
|
|
50
50
|
errors.push({
|
|
51
|
-
message: 'for and for-in loops are not allowed in
|
|
51
|
+
message: 'for and for-in loops are not allowed in DSL workflows. Use for-of instead.',
|
|
52
52
|
node,
|
|
53
53
|
});
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
// Check for inline workflow.do
|
|
57
|
-
if (ts.isCallExpression(node)) {
|
|
57
|
+
if (!options?.allowInline && ts.isCallExpression(node)) {
|
|
58
58
|
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
59
59
|
const propAccess = node.expression;
|
|
60
60
|
if (propAccess.name.text === 'do' &&
|
|
@@ -65,7 +65,7 @@ export function validateNoDisallowedPatterns(node) {
|
|
|
65
65
|
(ts.isArrowFunction(secondArg) ||
|
|
66
66
|
ts.isFunctionExpression(secondArg))) {
|
|
67
67
|
errors.push({
|
|
68
|
-
message: 'Inline workflow.do with function argument is not allowed in
|
|
68
|
+
message: 'Inline workflow.do with function argument is not allowed in DSL workflows. Use pikkuWorkflowComplexFunc instead.',
|
|
69
69
|
node,
|
|
70
70
|
});
|
|
71
71
|
return;
|
|
@@ -97,11 +97,19 @@ export function validateAwaitedCalls(node) {
|
|
|
97
97
|
if (propAccess.name.text === 'all' &&
|
|
98
98
|
ts.isIdentifier(propAccess.expression) &&
|
|
99
99
|
propAccess.expression.text === 'Promise') {
|
|
100
|
-
// console.log('[DEBUG] Found Promise.all, setting insidePromiseAll=true')
|
|
101
|
-
// Visit children with insidePromiseAll = true
|
|
102
100
|
ts.forEachChild(node, (child) => visit(child, parentIsAwait, true));
|
|
103
101
|
return;
|
|
104
102
|
}
|
|
103
|
+
// .push() on an array — workflow.do() inside is collecting promises
|
|
104
|
+
if (propAccess.name.text === 'push') {
|
|
105
|
+
ts.forEachChild(node, (child) => visit(child, parentIsAwait, true));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Array literal — workflow.do() inside is collecting promises
|
|
110
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
111
|
+
ts.forEachChild(node, (child) => visit(child, parentIsAwait, true));
|
|
112
|
+
return;
|
|
105
113
|
}
|
|
106
114
|
// Now check for workflow calls
|
|
107
115
|
if (ts.isCallExpression(node)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.11",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
|
|
38
|
-
"@pikku/core": "^0.12.
|
|
38
|
+
"@pikku/core": "^0.12.19",
|
|
39
39
|
"path-to-regexp": "^8.3.0",
|
|
40
40
|
"ts-json-schema-generator": "^2.5.0",
|
|
41
41
|
"tsx": "^4.21.0",
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
describe('addFunctions duplicate name handling', () => {
|
|
11
|
+
test('logs a critical error when function name is duplicated across files', async () => {
|
|
12
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-duplicate-function-'))
|
|
13
|
+
const fileA = join(rootDir, 'a.ts')
|
|
14
|
+
const fileB = join(rootDir, 'b.ts')
|
|
15
|
+
|
|
16
|
+
await writeFile(
|
|
17
|
+
fileA,
|
|
18
|
+
[
|
|
19
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
20
|
+
'export const createUser = pikkuFunc({',
|
|
21
|
+
' func: async () => ({ ok: true })',
|
|
22
|
+
'})',
|
|
23
|
+
].join('\n')
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
await writeFile(
|
|
27
|
+
fileB,
|
|
28
|
+
[
|
|
29
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
30
|
+
'export const createUser = pikkuFunc({',
|
|
31
|
+
' func: async () => ({ ok: true })',
|
|
32
|
+
'})',
|
|
33
|
+
].join('\n')
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
37
|
+
const logger: InspectorLogger = {
|
|
38
|
+
debug: () => {},
|
|
39
|
+
info: () => {},
|
|
40
|
+
warn: () => {},
|
|
41
|
+
error: () => {},
|
|
42
|
+
critical: (code: ErrorCode, message: string) => {
|
|
43
|
+
criticals.push({ code, message })
|
|
44
|
+
},
|
|
45
|
+
hasCriticalErrors: () => criticals.length > 0,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const state = await inspect(logger, [fileA, fileB], { rootDir })
|
|
50
|
+
const nameCollision = criticals.find(
|
|
51
|
+
(entry) => entry.code === ErrorCode.DUPLICATE_FUNCTION_NAME
|
|
52
|
+
)
|
|
53
|
+
assert.ok(nameCollision)
|
|
54
|
+
assert.match(nameCollision!.message, /createUser/)
|
|
55
|
+
assert.strictEqual(state.rpc.internalMeta['createUser'], 'createUser')
|
|
56
|
+
} finally {
|
|
57
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('allows same base function name across files when versions differ', async () => {
|
|
62
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-versioned-function-'))
|
|
63
|
+
const fileA = join(rootDir, 'a.ts')
|
|
64
|
+
const fileB = join(rootDir, 'b.ts')
|
|
65
|
+
|
|
66
|
+
await writeFile(
|
|
67
|
+
fileA,
|
|
68
|
+
[
|
|
69
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
70
|
+
'export const createUser = pikkuFunc({',
|
|
71
|
+
' version: 1,',
|
|
72
|
+
' func: async () => ({ ok: true })',
|
|
73
|
+
'})',
|
|
74
|
+
].join('\n')
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
await writeFile(
|
|
78
|
+
fileB,
|
|
79
|
+
[
|
|
80
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
81
|
+
'export const createUser = pikkuFunc({',
|
|
82
|
+
' version: 2,',
|
|
83
|
+
' func: async () => ({ ok: true })',
|
|
84
|
+
'})',
|
|
85
|
+
].join('\n')
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
89
|
+
const logger: InspectorLogger = {
|
|
90
|
+
debug: () => {},
|
|
91
|
+
info: () => {},
|
|
92
|
+
warn: () => {},
|
|
93
|
+
error: () => {},
|
|
94
|
+
critical: (code: ErrorCode, message: string) => {
|
|
95
|
+
criticals.push({ code, message })
|
|
96
|
+
},
|
|
97
|
+
hasCriticalErrors: () => criticals.length > 0,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const state = await inspect(logger, [fileA, fileB], { rootDir })
|
|
102
|
+
const nameCollision = criticals.find(
|
|
103
|
+
(entry) => entry.code === ErrorCode.DUPLICATE_FUNCTION_NAME
|
|
104
|
+
)
|
|
105
|
+
assert.equal(nameCollision, undefined)
|
|
106
|
+
assert.strictEqual(state.rpc.internalMeta['createUser'], 'createUser@v2')
|
|
107
|
+
assert.ok(state.functions.meta['createUser@v1'])
|
|
108
|
+
assert.ok(state.functions.meta['createUser@v2'])
|
|
109
|
+
} finally {
|
|
110
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('logs a critical error when exposed function name is duplicated across files', async () => {
|
|
115
|
+
const rootDir = await mkdtemp(
|
|
116
|
+
join(tmpdir(), 'pikku-exposed-duplicate-function-')
|
|
117
|
+
)
|
|
118
|
+
const fileA = join(rootDir, 'a.ts')
|
|
119
|
+
const fileB = join(rootDir, 'b.ts')
|
|
120
|
+
|
|
121
|
+
await writeFile(
|
|
122
|
+
fileA,
|
|
123
|
+
[
|
|
124
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
125
|
+
'export const createUser = pikkuFunc({',
|
|
126
|
+
' expose: true,',
|
|
127
|
+
' func: async () => ({ ok: true })',
|
|
128
|
+
'})',
|
|
129
|
+
].join('\n')
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
await writeFile(
|
|
133
|
+
fileB,
|
|
134
|
+
[
|
|
135
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
136
|
+
'export const createUser = pikkuFunc({',
|
|
137
|
+
' expose: true,',
|
|
138
|
+
' func: async () => ({ ok: true })',
|
|
139
|
+
'})',
|
|
140
|
+
].join('\n')
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
144
|
+
const logger: InspectorLogger = {
|
|
145
|
+
debug: () => {},
|
|
146
|
+
info: () => {},
|
|
147
|
+
warn: () => {},
|
|
148
|
+
error: () => {},
|
|
149
|
+
critical: (code: ErrorCode, message: string) => {
|
|
150
|
+
criticals.push({ code, message })
|
|
151
|
+
},
|
|
152
|
+
hasCriticalErrors: () => criticals.length > 0,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await inspect(logger, [fileA, fileB], { rootDir })
|
|
157
|
+
const nameCollision = criticals.find(
|
|
158
|
+
(entry) => entry.code === ErrorCode.DUPLICATE_FUNCTION_NAME
|
|
159
|
+
)
|
|
160
|
+
assert.ok(nameCollision)
|
|
161
|
+
assert.match(
|
|
162
|
+
nameCollision!.message,
|
|
163
|
+
/Function name 'createUser' is not unique/
|
|
164
|
+
)
|
|
165
|
+
} finally {
|
|
166
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
})
|
package/src/add/add-functions.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
-
import type { AddWiring, SchemaRef } from '../types.js'
|
|
2
|
+
import type { AddWiring, InspectorState, SchemaRef } from '../types.js'
|
|
3
3
|
import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
|
|
4
4
|
import type { TypesMap } from '../types-map.js'
|
|
5
5
|
import {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { extractFunctionNode } from '../utils/extract-function-node.js'
|
|
10
10
|
import { extractUsedWires } from '../utils/extract-services.js'
|
|
11
11
|
import type { FunctionServicesMeta } from '@pikku/core'
|
|
12
|
-
import { formatVersionedId } from '@pikku/core'
|
|
12
|
+
import { formatVersionedId, parseVersionedId } from '@pikku/core'
|
|
13
13
|
import {
|
|
14
14
|
getPropertyValue,
|
|
15
15
|
getCommonWireMetaData,
|
|
@@ -287,6 +287,31 @@ function unwrapPromise(checker: ts.TypeChecker, type: ts.Type): ts.Type {
|
|
|
287
287
|
return type
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
const resolveExistingFunctionSource = (
|
|
291
|
+
state: InspectorState,
|
|
292
|
+
pikkuFuncId: string
|
|
293
|
+
): string | null => {
|
|
294
|
+
return (
|
|
295
|
+
state.functions.meta[pikkuFuncId]?.sourceFile ||
|
|
296
|
+
state.rpc.internalFiles.get(pikkuFuncId)?.path ||
|
|
297
|
+
null
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const areCompatibleFunctionIds = (
|
|
302
|
+
existingId: string,
|
|
303
|
+
incomingId: string
|
|
304
|
+
): boolean => {
|
|
305
|
+
if (existingId === incomingId) {
|
|
306
|
+
return true
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const existingParsed = parseVersionedId(existingId)
|
|
310
|
+
const incomingParsed = parseVersionedId(incomingId)
|
|
311
|
+
|
|
312
|
+
return existingParsed.baseName === incomingParsed.baseName
|
|
313
|
+
}
|
|
314
|
+
|
|
290
315
|
/**
|
|
291
316
|
* Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
|
|
292
317
|
* then push into state.functions.meta.
|
|
@@ -339,6 +364,7 @@ export const addFunctions: AddWiring = (
|
|
|
339
364
|
let remote: boolean | undefined
|
|
340
365
|
let mcp: boolean | undefined
|
|
341
366
|
let readonly_: boolean | undefined
|
|
367
|
+
let deploy: 'serverless' | 'server' | 'auto' | undefined
|
|
342
368
|
let approvalRequired: boolean | undefined
|
|
343
369
|
let approvalDescription: string | undefined
|
|
344
370
|
let version: number | undefined
|
|
@@ -422,6 +448,11 @@ export const addFunctions: AddWiring = (
|
|
|
422
448
|
remote = getPropertyValue(firstArg, 'remote') as boolean | undefined
|
|
423
449
|
mcp = getPropertyValue(firstArg, 'mcp') as boolean | undefined
|
|
424
450
|
readonly_ = getPropertyValue(firstArg, 'readonly') as boolean | undefined
|
|
451
|
+
deploy = getPropertyValue(firstArg, 'deploy') as
|
|
452
|
+
| 'serverless'
|
|
453
|
+
| 'server'
|
|
454
|
+
| 'auto'
|
|
455
|
+
| undefined
|
|
425
456
|
approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
|
|
426
457
|
| boolean
|
|
427
458
|
| undefined
|
|
@@ -540,6 +571,7 @@ export const addFunctions: AddWiring = (
|
|
|
540
571
|
}
|
|
541
572
|
|
|
542
573
|
const isMCPToolFunc = expression.text === 'pikkuMCPToolFunc'
|
|
574
|
+
const isListFunc = expression.text === 'pikkuListFunc'
|
|
543
575
|
const mcpEnabled = mcp || isMCPToolFunc
|
|
544
576
|
|
|
545
577
|
// Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
|
|
@@ -612,7 +644,33 @@ export const addFunctions: AddWiring = (
|
|
|
612
644
|
inputNames = [schemaName]
|
|
613
645
|
state.schemaLookup.set(schemaName, inputSchemaRef)
|
|
614
646
|
state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
|
|
615
|
-
} else if (genericTypes.length >= 1 && genericTypes[0]) {
|
|
647
|
+
} else if (isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
|
|
648
|
+
const inputAliasName = `${capitalizedName}Input`
|
|
649
|
+
const filterType = genericTypes[0]
|
|
650
|
+
const filterTypeText = checker.typeToString(
|
|
651
|
+
filterType,
|
|
652
|
+
undefined,
|
|
653
|
+
ts.TypeFormatFlags.NoTruncation
|
|
654
|
+
)
|
|
655
|
+
const refs = resolveTypeImports(
|
|
656
|
+
filterType,
|
|
657
|
+
state.functions.typesMap,
|
|
658
|
+
true,
|
|
659
|
+
checker
|
|
660
|
+
)
|
|
661
|
+
state.functions.typesMap.addCustomType(
|
|
662
|
+
inputAliasName,
|
|
663
|
+
`{ cursor?: string; limit?: number; sort?: Array<{ column: string; direction: 'asc' | 'desc' }>; filter?: unknown; search?: string; } & ${filterTypeText}`,
|
|
664
|
+
[...new Set(refs)]
|
|
665
|
+
)
|
|
666
|
+
inputNames = [inputAliasName]
|
|
667
|
+
const secondParam = handler.parameters[1]
|
|
668
|
+
if (secondParam) {
|
|
669
|
+
inputTypes = [checker.getTypeAtLocation(secondParam)]
|
|
670
|
+
} else {
|
|
671
|
+
inputTypes = [filterType]
|
|
672
|
+
}
|
|
673
|
+
} else if (!isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
|
|
616
674
|
// Fall back to extracting from generic type arguments
|
|
617
675
|
const result = getNamesAndTypes(
|
|
618
676
|
checker,
|
|
@@ -648,7 +706,27 @@ export const addFunctions: AddWiring = (
|
|
|
648
706
|
outputNames = [schemaName]
|
|
649
707
|
state.schemaLookup.set(schemaName, outputSchemaRef)
|
|
650
708
|
state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
|
|
651
|
-
} else if (genericTypes.length >= 2) {
|
|
709
|
+
} else if (isListFunc && genericTypes.length >= 2 && genericTypes[1]) {
|
|
710
|
+
const outputAliasName = `${capitalizedName}Output`
|
|
711
|
+
const rowType = genericTypes[1]
|
|
712
|
+
const rowTypeText = checker.typeToString(
|
|
713
|
+
rowType,
|
|
714
|
+
undefined,
|
|
715
|
+
ts.TypeFormatFlags.NoTruncation
|
|
716
|
+
)
|
|
717
|
+
const refs = resolveTypeImports(
|
|
718
|
+
rowType,
|
|
719
|
+
state.functions.typesMap,
|
|
720
|
+
true,
|
|
721
|
+
checker
|
|
722
|
+
)
|
|
723
|
+
state.functions.typesMap.addCustomType(
|
|
724
|
+
outputAliasName,
|
|
725
|
+
`{ rows: Array<${rowTypeText}>; nextCursor: string | null; totalCount?: number; }`,
|
|
726
|
+
[...new Set(refs)]
|
|
727
|
+
)
|
|
728
|
+
outputNames = [outputAliasName]
|
|
729
|
+
} else if (!isListFunc && genericTypes.length >= 2) {
|
|
652
730
|
outputNames = getNamesAndTypes(
|
|
653
731
|
checker,
|
|
654
732
|
state.functions.typesMap,
|
|
@@ -725,6 +803,56 @@ export const addFunctions: AddWiring = (
|
|
|
725
803
|
state.typesLookup.set(pikkuFuncId, inputTypes)
|
|
726
804
|
}
|
|
727
805
|
|
|
806
|
+
const sourceFile = node.getSourceFile().fileName
|
|
807
|
+
const existingFunction = state.functions.meta[pikkuFuncId]
|
|
808
|
+
if (
|
|
809
|
+
existingFunction &&
|
|
810
|
+
existingFunction.sourceFile &&
|
|
811
|
+
existingFunction.sourceFile !== sourceFile
|
|
812
|
+
) {
|
|
813
|
+
logger.critical(
|
|
814
|
+
ErrorCode.DUPLICATE_FUNCTION_NAME,
|
|
815
|
+
`Function name '${name}' is not unique. ` +
|
|
816
|
+
`'${pikkuFuncId}' is already defined in '${existingFunction.sourceFile}' and cannot be redefined in '${sourceFile}'.`
|
|
817
|
+
)
|
|
818
|
+
return
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (exportedName || explicitName) {
|
|
822
|
+
const existingInternal = state.rpc.internalMeta[name]
|
|
823
|
+
if (
|
|
824
|
+
existingInternal &&
|
|
825
|
+
!areCompatibleFunctionIds(existingInternal, pikkuFuncId)
|
|
826
|
+
) {
|
|
827
|
+
const existingSource =
|
|
828
|
+
resolveExistingFunctionSource(state, existingInternal) || 'unknown file'
|
|
829
|
+
logger.critical(
|
|
830
|
+
ErrorCode.DUPLICATE_FUNCTION_NAME,
|
|
831
|
+
`Function name '${name}' is not unique. ` +
|
|
832
|
+
`It already points to '${existingInternal}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`
|
|
833
|
+
)
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (expose) {
|
|
838
|
+
const existingExposed = state.rpc.exposedMeta[name]
|
|
839
|
+
if (
|
|
840
|
+
existingExposed &&
|
|
841
|
+
!areCompatibleFunctionIds(existingExposed, pikkuFuncId)
|
|
842
|
+
) {
|
|
843
|
+
const existingSource =
|
|
844
|
+
resolveExistingFunctionSource(state, existingExposed) ||
|
|
845
|
+
'unknown file'
|
|
846
|
+
logger.critical(
|
|
847
|
+
ErrorCode.DUPLICATE_FUNCTION_NAME,
|
|
848
|
+
`Exposed function name '${name}' is not unique. ` +
|
|
849
|
+
`It already points to '${existingExposed}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`
|
|
850
|
+
)
|
|
851
|
+
return
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
728
856
|
// --- resolve middleware ---
|
|
729
857
|
let middleware = objectNode
|
|
730
858
|
? resolveMiddleware(state, objectNode, tags, checker)
|
|
@@ -783,6 +911,7 @@ export const addFunctions: AddWiring = (
|
|
|
783
911
|
remote: remote || undefined,
|
|
784
912
|
mcp: mcpEnabled || undefined,
|
|
785
913
|
readonly: readonly_ || undefined,
|
|
914
|
+
deploy: deploy || undefined,
|
|
786
915
|
approvalRequired: approvalRequired || undefined,
|
|
787
916
|
approvalDescription: approvalDescription || undefined,
|
|
788
917
|
version,
|
|
@@ -794,7 +923,7 @@ export const addFunctions: AddWiring = (
|
|
|
794
923
|
middleware,
|
|
795
924
|
permissions,
|
|
796
925
|
isDirectFunction,
|
|
797
|
-
sourceFile
|
|
926
|
+
sourceFile,
|
|
798
927
|
exportedName: exportedName || undefined,
|
|
799
928
|
}
|
|
800
929
|
|
|
@@ -817,9 +946,7 @@ export const addFunctions: AddWiring = (
|
|
|
817
946
|
|
|
818
947
|
if (mcpEnabled) {
|
|
819
948
|
if (!description) {
|
|
820
|
-
logger.warn(
|
|
821
|
-
`MCP tool '${name}' is missing a description.`
|
|
822
|
-
)
|
|
949
|
+
logger.warn(`MCP tool '${name}' is missing a description.`)
|
|
823
950
|
}
|
|
824
951
|
state.mcpEndpoints.files.add(node.getSourceFile().fileName)
|
|
825
952
|
state.mcpEndpoints.toolsMeta[name] = {
|
|
@@ -18,6 +18,7 @@ import { resolveHTTPMiddlewareFromObject } from '../utils/middleware.js'
|
|
|
18
18
|
import { resolveHTTPPermissionsFromObject } from '../utils/permissions.js'
|
|
19
19
|
import { extractWireNames } from '../utils/post-process.js'
|
|
20
20
|
import { ensureFunctionMetadata } from '../utils/ensure-function-metadata.js'
|
|
21
|
+
import { resolveFunctionMeta } from '../utils/resolve-function-meta.js'
|
|
21
22
|
import { ErrorCode } from '../error-codes.js'
|
|
22
23
|
import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js'
|
|
23
24
|
import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
|
|
@@ -204,6 +205,22 @@ export function registerHTTPRoute({
|
|
|
204
205
|
funcName = makeContextBasedId('http', method, fullRoute)
|
|
205
206
|
}
|
|
206
207
|
|
|
208
|
+
let refAddonTarget: string | null = null
|
|
209
|
+
if (
|
|
210
|
+
ts.isCallExpression(funcInitializer) &&
|
|
211
|
+
ts.isIdentifier(funcInitializer.expression) &&
|
|
212
|
+
funcInitializer.expression.text === 'ref'
|
|
213
|
+
) {
|
|
214
|
+
const [firstArg] = funcInitializer.arguments
|
|
215
|
+
if (
|
|
216
|
+
firstArg &&
|
|
217
|
+
ts.isStringLiteral(firstArg) &&
|
|
218
|
+
firstArg.text.includes(':')
|
|
219
|
+
) {
|
|
220
|
+
refAddonTarget = firstArg.text
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
207
224
|
const packageName = ts.isIdentifier(funcInitializer)
|
|
208
225
|
? resolveAddonName(
|
|
209
226
|
funcInitializer,
|
|
@@ -212,6 +229,16 @@ export function registerHTTPRoute({
|
|
|
212
229
|
)
|
|
213
230
|
: null
|
|
214
231
|
|
|
232
|
+
if (refAddonTarget) {
|
|
233
|
+
const targetMeta = resolveFunctionMeta(state, refAddonTarget)
|
|
234
|
+
if (!targetMeta) {
|
|
235
|
+
logger.warn(
|
|
236
|
+
`Skipping route '${fullRoute}': addon function metadata for '${refAddonTarget}' is not available yet.`
|
|
237
|
+
)
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
215
242
|
ensureFunctionMetadata(
|
|
216
243
|
state,
|
|
217
244
|
funcName,
|
|
@@ -222,7 +249,7 @@ export function registerHTTPRoute({
|
|
|
222
249
|
)
|
|
223
250
|
|
|
224
251
|
// Lookup existing function metadata
|
|
225
|
-
const fnMeta = state
|
|
252
|
+
const fnMeta = resolveFunctionMeta(state, funcName)
|
|
226
253
|
if (!fnMeta) {
|
|
227
254
|
logger.critical(
|
|
228
255
|
ErrorCode.FUNCTION_METADATA_NOT_FOUND,
|
package/src/add/add-workflow.ts
CHANGED
|
@@ -155,7 +155,7 @@ function getWorkflowInvocations(
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
|
-
* Inspector for
|
|
158
|
+
* Inspector for pikkuWorkflowFunc() and pikkuWorkflowComplexFunc() calls
|
|
159
159
|
* Detects workflow registration and extracts metadata
|
|
160
160
|
*/
|
|
161
161
|
export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
@@ -171,11 +171,11 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
171
171
|
return
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
let wrapperType: 'dsl' | '
|
|
174
|
+
let wrapperType: 'dsl' | 'complex' | null = null
|
|
175
175
|
if (expression.text === 'pikkuWorkflowFunc') {
|
|
176
176
|
wrapperType = 'dsl'
|
|
177
177
|
} else if (expression.text === 'pikkuWorkflowComplexFunc') {
|
|
178
|
-
wrapperType = '
|
|
178
|
+
wrapperType = 'complex'
|
|
179
179
|
} else {
|
|
180
180
|
return
|
|
181
181
|
}
|
|
@@ -267,7 +267,9 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
267
267
|
|
|
268
268
|
// Try DSL workflow extraction first
|
|
269
269
|
// Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
|
|
270
|
-
const result = extractDSLWorkflow(node, checker
|
|
270
|
+
const result = extractDSLWorkflow(node, checker, {
|
|
271
|
+
allowInline: wrapperType === 'complex',
|
|
272
|
+
})
|
|
271
273
|
|
|
272
274
|
if (result.status === 'ok' && result.steps) {
|
|
273
275
|
// Extraction succeeded
|
|
@@ -305,7 +307,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
305
307
|
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
|
|
306
308
|
logger.critical(
|
|
307
309
|
ErrorCode.INVALID_DSL_WORKFLOW,
|
|
308
|
-
`Workflow '${workflowName}'
|
|
310
|
+
`Workflow '${workflowName}' does not conform to DSL workflow rules:\n${result.reason || 'Unknown error'}`
|
|
309
311
|
)
|
|
310
312
|
return
|
|
311
313
|
} else {
|
|
@@ -317,12 +319,10 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
317
319
|
}
|
|
318
320
|
}
|
|
319
321
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
*/
|
|
325
|
-
if (!dsl || wrapperType === 'regular') {
|
|
322
|
+
// For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
|
|
323
|
+
// patterns the DSL extractor doesn't handle (array+push, nested Promise.all
|
|
324
|
+
// with identifier args, etc.) are still registered as invoked functions.
|
|
325
|
+
if (wrapperType === 'complex') {
|
|
326
326
|
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
|
|
327
327
|
}
|
|
328
328
|
|
package/src/error-codes.ts
CHANGED
|
@@ -27,6 +27,11 @@ export function extractStringLiteral(
|
|
|
27
27
|
return result
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Unwrap type assertions: `expr as Type` or `<Type>expr`
|
|
31
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
32
|
+
return extractStringLiteral(node.expression, checker)
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
// Try to evaluate constant identifiers
|
|
31
36
|
if (ts.isIdentifier(node)) {
|
|
32
37
|
const symbol = checker.getSymbolAtLocation(node)
|
|
@@ -375,7 +375,7 @@ async function generateZodSchemas(
|
|
|
375
375
|
const uniqueSourceFiles = [
|
|
376
376
|
...new Set([...schemaLookup.values()].map((ref) => ref.sourceFile)),
|
|
377
377
|
]
|
|
378
|
-
|
|
378
|
+
logger.info(
|
|
379
379
|
`[TIMING] Zod schemas: ${schemaLookup.size} schemas from ${uniqueSourceFiles.length} files`
|
|
380
380
|
)
|
|
381
381
|
|
|
@@ -384,7 +384,7 @@ async function generateZodSchemas(
|
|
|
384
384
|
logger,
|
|
385
385
|
uniqueSourceFiles
|
|
386
386
|
)
|
|
387
|
-
|
|
387
|
+
logger.info(
|
|
388
388
|
`[TIMING] Batch import: ${(performance.now() - importStart).toFixed(0)}ms`
|
|
389
389
|
)
|
|
390
390
|
|
|
@@ -456,7 +456,7 @@ async function generateZodSchemas(
|
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
-
|
|
459
|
+
logger.info(
|
|
460
460
|
`[TIMING] Process schemas: ${(performance.now() - processStart).toFixed(0)}ms (${Object.keys(schemas).length} generated)`
|
|
461
461
|
)
|
|
462
462
|
return schemas
|