@pikku/core 0.12.20 → 0.12.22
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 +17 -0
- package/dist/dev/hot-reload.js +20 -6
- package/dist/errors/error-handler.d.ts +1 -1
- package/dist/errors/error-handler.js +2 -2
- package/dist/function/functions.types.d.ts +3 -2
- package/dist/handle-error.js +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/middleware/index.d.ts +2 -2
- package/dist/middleware/index.js +2 -2
- package/dist/middleware/telemetry.d.ts +2 -2
- package/dist/middleware/telemetry.js +2 -2
- package/dist/middleware-runner.d.ts +15 -27
- package/dist/middleware-runner.js +25 -30
- package/dist/permissions.d.ts +15 -22
- package/dist/permissions.js +30 -25
- package/dist/pikku-request.js +1 -1
- package/dist/pikku-state.js +2 -3
- package/dist/services/ai-agent-runner-service.d.ts +1 -0
- package/dist/services/in-memory-queue-service.js +1 -1
- package/dist/services/in-memory-workflow-service.d.ts +15 -13
- package/dist/services/in-memory-workflow-service.js +31 -13
- package/dist/services/local-content.js +8 -1
- package/dist/services/user-session-service.d.ts +1 -1
- package/dist/testing/service-tests.js +24 -0
- package/dist/types/core.types.d.ts +4 -0
- package/dist/types/state.types.d.ts +2 -18
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
- package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
- package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
- package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
- package/dist/wirings/channel/channel-handler.js +1 -1
- package/dist/wirings/channel/channel-store.d.ts +13 -0
- package/dist/wirings/channel/channel.types.d.ts +3 -0
- package/dist/wirings/channel/local/local-channel-runner.js +23 -5
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
- package/dist/wirings/cli/cli-runner.js +24 -5
- package/dist/wirings/cli/command-parser.js +19 -4
- package/dist/wirings/http/http-runner.js +72 -36
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
- package/dist/wirings/http/web-request.js +32 -0
- package/dist/wirings/rpc/rpc-runner.js +13 -3
- package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
- package/dist/wirings/workflow/graph/graph-node.js +4 -0
- package/dist/wirings/workflow/graph/graph-runner.js +12 -10
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
- package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
- package/dist/wirings/workflow/workflow.types.d.ts +34 -0
- package/package.json +1 -1
- package/run-tests.sh +4 -1
- package/src/dev/hot-reload.test.ts +62 -11
- package/src/dev/hot-reload.ts +28 -7
- package/src/errors/error-handler.ts +8 -2
- package/src/errors/error.test.ts +2 -0
- package/src/function/function-runner.test.ts +500 -10
- package/src/function/functions.types.ts +3 -2
- package/src/handle-error.test.ts +1 -1
- package/src/handle-error.ts +4 -1
- package/src/index.ts +12 -2
- package/src/middleware/index.ts +11 -2
- package/src/middleware/telemetry.ts +2 -2
- package/src/middleware-runner.test.ts +16 -16
- package/src/middleware-runner.ts +42 -30
- package/src/permissions.test.ts +27 -24
- package/src/permissions.ts +41 -25
- package/src/pikku-request.test.ts +35 -0
- package/src/pikku-request.ts +1 -1
- package/src/pikku-state.ts +2 -3
- package/src/run-tests-script.test.ts +18 -0
- package/src/services/ai-agent-runner-service.ts +1 -0
- package/src/services/in-memory-queue-service.ts +1 -1
- package/src/services/in-memory-session-store.ts +1 -2
- package/src/services/in-memory-workflow-service.test.ts +33 -11
- package/src/services/in-memory-workflow-service.ts +49 -13
- package/src/services/local-content.test.ts +54 -0
- package/src/services/local-content.ts +12 -1
- package/src/services/typed-credential-service.ts +3 -3
- package/src/services/typed-secret-service.ts +3 -3
- package/src/services/typed-variables-service.ts +3 -3
- package/src/services/user-session-service.ts +4 -4
- package/src/testing/service-tests.ts +30 -0
- package/src/types/core.types.ts +6 -2
- package/src/types/state.types.ts +2 -13
- package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
- package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
- package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
- package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
- package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
- package/src/wirings/channel/channel-handler.test.ts +272 -0
- package/src/wirings/channel/channel-handler.ts +1 -1
- package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
- package/src/wirings/channel/channel-store.ts +19 -0
- package/src/wirings/channel/channel.types.ts +4 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
- package/src/wirings/channel/local/local-channel-runner.ts +41 -5
- package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
- package/src/wirings/cli/cli-runner.test.ts +255 -2
- package/src/wirings/cli/cli-runner.ts +31 -10
- package/src/wirings/cli/cli.types.ts +4 -8
- package/src/wirings/cli/command-parser.test.ts +83 -0
- package/src/wirings/cli/command-parser.ts +23 -4
- package/src/wirings/http/http-runner.test.ts +296 -1
- package/src/wirings/http/http-runner.ts +87 -57
- package/src/wirings/http/http.types.ts +11 -26
- package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
- package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
- package/src/wirings/http/web-request.test.ts +115 -0
- package/src/wirings/http/web-request.ts +43 -5
- package/src/wirings/mcp/mcp-runner.test.ts +367 -0
- package/src/wirings/queue/queue.types.ts +2 -5
- package/src/wirings/rpc/rpc-runner.test.ts +511 -0
- package/src/wirings/rpc/rpc-runner.ts +15 -3
- package/src/wirings/rpc/wire-addon.test.ts +57 -0
- package/src/wirings/workflow/graph/graph-node.ts +12 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
- package/src/wirings/workflow/graph/graph-runner.ts +28 -10
- package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
- package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
- package/src/wirings/workflow/workflow.types.ts +68 -5
- package/tsconfig.tsbuildinfo +1 -1
package/src/dev/hot-reload.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { watch, type FSWatcher } from 'node:fs'
|
|
2
2
|
import { stat, readFile } from 'node:fs/promises'
|
|
3
3
|
import { join, resolve, relative } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import { register } from 'tsx/esm/api'
|
|
4
7
|
|
|
5
8
|
import { pikkuState } from '../pikku-state.js'
|
|
6
9
|
import { addFunction } from '../function/function-runner.js'
|
|
@@ -54,10 +57,26 @@ const findCompiledFile = async (
|
|
|
54
57
|
// Use data: URLs to import modules. This bypasses TypeScript loaders
|
|
55
58
|
// (e.g. tsx) that intercept file:// imports and break dynamic ESM loading.
|
|
56
59
|
// Each import gets unique content so there's no module cache to worry about.
|
|
60
|
+
let tsxRegistered = false
|
|
61
|
+
|
|
62
|
+
const ensureTsxRegistered = () => {
|
|
63
|
+
if (tsxRegistered) return
|
|
64
|
+
register()
|
|
65
|
+
tsxRegistered = true
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
const reimportModule = async (
|
|
58
|
-
filePath: string
|
|
69
|
+
filePath: string,
|
|
70
|
+
useTsx = false
|
|
59
71
|
): Promise<Record<string, unknown> | null> => {
|
|
60
72
|
try {
|
|
73
|
+
if (useTsx) {
|
|
74
|
+
ensureTsxRegistered()
|
|
75
|
+
return await import(
|
|
76
|
+
`${pathToFileURL(resolve(filePath)).href}?t=${Date.now()}`
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
const content = await readFile(resolve(filePath), 'utf-8')
|
|
62
81
|
const dataUrl =
|
|
63
82
|
'data:text/javascript;base64,' + Buffer.from(content).toString('base64')
|
|
@@ -98,17 +117,19 @@ export async function pikkuDevReloader(
|
|
|
98
117
|
srcDir,
|
|
99
118
|
absPikkuDir
|
|
100
119
|
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
const importPath = compiledFile ?? changedTsFile
|
|
121
|
+
const usedTsxFallback = !compiledFile
|
|
122
|
+
|
|
123
|
+
if (usedTsxFallback) {
|
|
124
|
+
logger.debug(
|
|
125
|
+
`Hot-reload using tsx fallback for: ${relative(process.cwd(), changedTsFile)}`
|
|
104
126
|
)
|
|
105
|
-
return
|
|
106
127
|
}
|
|
107
128
|
|
|
108
|
-
const mod = await reimportModule(
|
|
129
|
+
const mod = await reimportModule(importPath, usedTsxFallback)
|
|
109
130
|
if (!mod) {
|
|
110
131
|
logger.error(
|
|
111
|
-
`Failed to import: ${relative(process.cwd(),
|
|
132
|
+
`Failed to import: ${relative(process.cwd(), importPath)} (keeping old code)`
|
|
112
133
|
)
|
|
113
134
|
return
|
|
114
135
|
}
|
|
@@ -29,8 +29,14 @@ export interface ErrorDetails {
|
|
|
29
29
|
* @param error - The error to add.
|
|
30
30
|
* @param details - The details of the error.
|
|
31
31
|
*/
|
|
32
|
-
export const addError = (
|
|
33
|
-
|
|
32
|
+
export const addError = (
|
|
33
|
+
error: any,
|
|
34
|
+
{ status, message, mcpCode }: ErrorDetails
|
|
35
|
+
) => {
|
|
36
|
+
pikkuState(null, 'misc', 'errors').set(
|
|
37
|
+
error,
|
|
38
|
+
mcpCode === undefined ? { status, message } : { status, message, mcpCode }
|
|
39
|
+
)
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
package/src/errors/error.test.ts
CHANGED
|
@@ -77,6 +77,7 @@ describe('getErrorResponse', () => {
|
|
|
77
77
|
const response = getErrorResponse(error)
|
|
78
78
|
assert.deepStrictEqual(response, {
|
|
79
79
|
status: 400,
|
|
80
|
+
mcpCode: -32600,
|
|
80
81
|
message:
|
|
81
82
|
'The server cannot or will not process the request due to client error (e.g., malformed request syntax).',
|
|
82
83
|
})
|
|
@@ -87,6 +88,7 @@ describe('getErrorResponse', () => {
|
|
|
87
88
|
const response = getErrorResponse(error)
|
|
88
89
|
assert.deepStrictEqual(response, {
|
|
89
90
|
status: 404,
|
|
91
|
+
mcpCode: -32601,
|
|
90
92
|
message: 'The server cannot find the requested resource.',
|
|
91
93
|
})
|
|
92
94
|
})
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { describe, test, beforeEach } from 'node:test'
|
|
2
2
|
import * as assert from 'node:assert'
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
addFunction,
|
|
5
|
+
getAllFunctionNames,
|
|
6
|
+
getFunctionNames,
|
|
7
|
+
runPikkuFunc,
|
|
8
|
+
runPikkuFuncDirectly,
|
|
9
|
+
} from './function-runner.js'
|
|
10
|
+
import { addTagMiddleware, addTagPermission } from '../index.js'
|
|
5
11
|
import { resetPikkuState, pikkuState } from '../pikku-state.js'
|
|
6
12
|
import type { CoreServices, CorePikkuMiddleware } from '../types/core.types.js'
|
|
7
13
|
import type { CorePermissionGroup } from './functions.types.js'
|
|
14
|
+
import { PikkuSessionService } from '../services/user-session-service.js'
|
|
15
|
+
import { ReadonlySessionError } from '../errors/errors.js'
|
|
8
16
|
|
|
9
17
|
beforeEach(() => {
|
|
10
18
|
resetPikkuState()
|
|
@@ -53,8 +61,8 @@ describe('runPikkuFunc - Integration Tests', () => {
|
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
addTagMiddleware('wiringTag', [createMiddleware('wiringTag')])
|
|
65
|
+
addTagMiddleware('funcTag', [createMiddleware('funcTag')])
|
|
58
66
|
|
|
59
67
|
// Register function with middleware and tags
|
|
60
68
|
addTestFunction('testFunc', {
|
|
@@ -105,8 +113,8 @@ describe('runPikkuFunc - Integration Tests', () => {
|
|
|
105
113
|
return true
|
|
106
114
|
}
|
|
107
115
|
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
addTagPermission('wiringTag', [wiringTagPermission])
|
|
117
|
+
addTagPermission('funcTag', [funcTagPermission])
|
|
110
118
|
|
|
111
119
|
// Setup direct permissions
|
|
112
120
|
const wiringPermissions: CorePermissionGroup = {
|
|
@@ -159,7 +167,7 @@ describe('runPikkuFunc - Integration Tests', () => {
|
|
|
159
167
|
test('should throw specific error for wiring tag permission failures', async () => {
|
|
160
168
|
const failingWiringTagPermission = async () => false
|
|
161
169
|
|
|
162
|
-
|
|
170
|
+
addTagPermission('wiringTag', [failingWiringTagPermission])
|
|
163
171
|
|
|
164
172
|
addTestFunction('testFunc', {
|
|
165
173
|
func: async () => 'success',
|
|
@@ -207,7 +215,7 @@ describe('runPikkuFunc - Integration Tests', () => {
|
|
|
207
215
|
test('should throw specific error for function tag permission failures', async () => {
|
|
208
216
|
const failingFuncTagPermission = async () => false
|
|
209
217
|
|
|
210
|
-
|
|
218
|
+
addTagPermission('funcTag', [failingFuncTagPermission])
|
|
211
219
|
|
|
212
220
|
addTestFunction('testFunc', {
|
|
213
221
|
func: async () => 'success',
|
|
@@ -265,7 +273,7 @@ describe('runPikkuFunc - Integration Tests', () => {
|
|
|
265
273
|
}
|
|
266
274
|
|
|
267
275
|
// Add same middleware to tag and directly
|
|
268
|
-
|
|
276
|
+
addTagMiddleware('testTag', [duplicatedMiddleware])
|
|
269
277
|
|
|
270
278
|
addTestFunction('testFunc', {
|
|
271
279
|
func: async () => 'success',
|
|
@@ -309,7 +317,7 @@ describe('runPikkuFunc - Integration Tests', () => {
|
|
|
309
317
|
],
|
|
310
318
|
}
|
|
311
319
|
|
|
312
|
-
|
|
320
|
+
addTagPermission('mixedTag', arrayPermission)
|
|
313
321
|
|
|
314
322
|
addTestFunction('testFunc', {
|
|
315
323
|
func: async () => {
|
|
@@ -533,4 +541,486 @@ describe('runPikkuFunc - Integration Tests', () => {
|
|
|
533
541
|
...wireServices,
|
|
534
542
|
})
|
|
535
543
|
})
|
|
544
|
+
|
|
545
|
+
test('should resolve versioned function ids to the base function and warn once', async () => {
|
|
546
|
+
const warnings: string[] = []
|
|
547
|
+
const singletonServices = {
|
|
548
|
+
...mockSingletonServices,
|
|
549
|
+
logger: {
|
|
550
|
+
...mockSingletonServices.logger,
|
|
551
|
+
warn: (message: string) => {
|
|
552
|
+
warnings.push(message)
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
} as any
|
|
556
|
+
|
|
557
|
+
addTestFunction('versionedBase', {
|
|
558
|
+
func: async () => 'resolved',
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
const result = await runPikkuFunc(
|
|
562
|
+
'rpc',
|
|
563
|
+
Math.random().toString(),
|
|
564
|
+
'versionedBase@v2',
|
|
565
|
+
{
|
|
566
|
+
singletonServices,
|
|
567
|
+
data: () => ({}),
|
|
568
|
+
auth: false,
|
|
569
|
+
wire: {},
|
|
570
|
+
}
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
assert.equal(result, 'resolved')
|
|
574
|
+
assert.equal(warnings.length, 1)
|
|
575
|
+
assert.match(
|
|
576
|
+
warnings[0]!,
|
|
577
|
+
/Version 'versionedBase@v2' not registered, resolved to 'versionedBase'/
|
|
578
|
+
)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
test('should throw when function metadata is missing', async () => {
|
|
582
|
+
addFunction('metaMissing', {
|
|
583
|
+
func: async () => 'ok',
|
|
584
|
+
} as any)
|
|
585
|
+
|
|
586
|
+
await assert.rejects(
|
|
587
|
+
() =>
|
|
588
|
+
runPikkuFunc('rpc', Math.random().toString(), 'metaMissing', {
|
|
589
|
+
singletonServices: mockSingletonServices,
|
|
590
|
+
data: () => ({}),
|
|
591
|
+
auth: false,
|
|
592
|
+
wire: {},
|
|
593
|
+
}),
|
|
594
|
+
{
|
|
595
|
+
message: 'Function meta not found: metaMissing',
|
|
596
|
+
}
|
|
597
|
+
)
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
test('should throw when function config is missing', async () => {
|
|
601
|
+
pikkuState(null, 'function', 'meta').missingConfig = {
|
|
602
|
+
pikkuFuncId: 'missingConfig',
|
|
603
|
+
sessionless: true,
|
|
604
|
+
permissions: [],
|
|
605
|
+
} as any
|
|
606
|
+
|
|
607
|
+
await assert.rejects(
|
|
608
|
+
() =>
|
|
609
|
+
runPikkuFunc('rpc', Math.random().toString(), 'missingConfig', {
|
|
610
|
+
singletonServices: mockSingletonServices,
|
|
611
|
+
data: () => ({}),
|
|
612
|
+
auth: false,
|
|
613
|
+
wire: {},
|
|
614
|
+
}),
|
|
615
|
+
{
|
|
616
|
+
message: 'Function not found: missingConfig',
|
|
617
|
+
}
|
|
618
|
+
)
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
test('should require a session when sessionless metadata is false', async () => {
|
|
622
|
+
addTestFunction('sessionRequired', {
|
|
623
|
+
func: async () => 'ok',
|
|
624
|
+
})
|
|
625
|
+
pikkuState(null, 'function', 'meta').sessionRequired.sessionless = false
|
|
626
|
+
|
|
627
|
+
await assert.rejects(
|
|
628
|
+
() =>
|
|
629
|
+
runPikkuFunc('rpc', Math.random().toString(), 'sessionRequired', {
|
|
630
|
+
singletonServices: mockSingletonServices,
|
|
631
|
+
data: () => ({}),
|
|
632
|
+
auth: true,
|
|
633
|
+
wire: {},
|
|
634
|
+
}),
|
|
635
|
+
{
|
|
636
|
+
message: 'Authentication required',
|
|
637
|
+
}
|
|
638
|
+
)
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
test('should warn when auth is disabled for a session-required function but still enforce session', async () => {
|
|
642
|
+
const warnings: string[] = []
|
|
643
|
+
const singletonServices = {
|
|
644
|
+
...mockSingletonServices,
|
|
645
|
+
logger: {
|
|
646
|
+
...mockSingletonServices.logger,
|
|
647
|
+
warn: (message: string) => {
|
|
648
|
+
warnings.push(message)
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
} as any
|
|
652
|
+
|
|
653
|
+
addTestFunction('sessionRequired', {
|
|
654
|
+
func: async () => 'ok',
|
|
655
|
+
auth: false,
|
|
656
|
+
})
|
|
657
|
+
pikkuState(null, 'function', 'meta').sessionRequired.sessionless = false
|
|
658
|
+
|
|
659
|
+
await assert.rejects(
|
|
660
|
+
() =>
|
|
661
|
+
runPikkuFunc('rpc', Math.random().toString(), 'sessionRequired', {
|
|
662
|
+
singletonServices,
|
|
663
|
+
data: () => ({}),
|
|
664
|
+
auth: false,
|
|
665
|
+
wire: {},
|
|
666
|
+
}),
|
|
667
|
+
{
|
|
668
|
+
message: 'Authentication required',
|
|
669
|
+
}
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
assert.equal(warnings.length, 1)
|
|
673
|
+
assert.match(
|
|
674
|
+
warnings[0]!,
|
|
675
|
+
/requires a session but auth was explicitly disabled/
|
|
676
|
+
)
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
test('should use backward-compatible auth checks when sessionless metadata is absent', async () => {
|
|
680
|
+
addTestFunction('legacyAuth', {
|
|
681
|
+
func: async () => 'ok',
|
|
682
|
+
auth: true,
|
|
683
|
+
})
|
|
684
|
+
delete pikkuState(null, 'function', 'meta').legacyAuth.sessionless
|
|
685
|
+
|
|
686
|
+
await assert.rejects(
|
|
687
|
+
() =>
|
|
688
|
+
runPikkuFunc('rpc', Math.random().toString(), 'legacyAuth', {
|
|
689
|
+
singletonServices: mockSingletonServices,
|
|
690
|
+
data: () => ({}),
|
|
691
|
+
auth: false,
|
|
692
|
+
wire: {},
|
|
693
|
+
}),
|
|
694
|
+
{
|
|
695
|
+
message: 'Authentication required',
|
|
696
|
+
}
|
|
697
|
+
)
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
test('should reject readonly sessions for non-readonly functions', async () => {
|
|
701
|
+
addTestFunction('readonlyBlocked', {
|
|
702
|
+
func: async () => 'ok',
|
|
703
|
+
})
|
|
704
|
+
pikkuState(null, 'function', 'meta').readonlyBlocked.sessionless = true
|
|
705
|
+
|
|
706
|
+
await assert.rejects(
|
|
707
|
+
() =>
|
|
708
|
+
runPikkuFunc('rpc', Math.random().toString(), 'readonlyBlocked', {
|
|
709
|
+
singletonServices: mockSingletonServices,
|
|
710
|
+
data: () => ({}),
|
|
711
|
+
auth: false,
|
|
712
|
+
wire: { session: { readonly: true } as any },
|
|
713
|
+
}),
|
|
714
|
+
ReadonlySessionError
|
|
715
|
+
)
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
test('should set session helpers on the wire when a session service is provided', async () => {
|
|
719
|
+
const sessionStore = {
|
|
720
|
+
get: async () => undefined,
|
|
721
|
+
}
|
|
722
|
+
const sessionService = new PikkuSessionService(sessionStore as any)
|
|
723
|
+
const wire: any = {}
|
|
724
|
+
let receivedWire: any
|
|
725
|
+
|
|
726
|
+
addTestFunction('sessionHelpers', {
|
|
727
|
+
func: async (_services: any, _data: any, innerWire: any) => {
|
|
728
|
+
receivedWire = innerWire
|
|
729
|
+
innerWire.setSession({ userId: 'u1' })
|
|
730
|
+
return innerWire.getSession()
|
|
731
|
+
},
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
const result = await runPikkuFunc(
|
|
735
|
+
'rpc',
|
|
736
|
+
Math.random().toString(),
|
|
737
|
+
'sessionHelpers',
|
|
738
|
+
{
|
|
739
|
+
singletonServices: {
|
|
740
|
+
...mockSingletonServices,
|
|
741
|
+
sessionStore,
|
|
742
|
+
} as any,
|
|
743
|
+
data: () => ({}),
|
|
744
|
+
auth: false,
|
|
745
|
+
wire,
|
|
746
|
+
sessionService,
|
|
747
|
+
}
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
assert.deepEqual(result, { userId: 'u1' })
|
|
751
|
+
assert.equal(typeof receivedWire.setSession, 'function')
|
|
752
|
+
assert.equal(typeof receivedWire.clearSession, 'function')
|
|
753
|
+
assert.equal(typeof receivedWire.getSession, 'function')
|
|
754
|
+
assert.equal(typeof receivedWire.hasSessionChanged, 'function')
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
test('should load session from sessionStore using pikkuUserId from the wire', async () => {
|
|
758
|
+
let sessionLookups = 0
|
|
759
|
+
let receivedSession: any
|
|
760
|
+
|
|
761
|
+
addTestFunction('loadStoredSession', {
|
|
762
|
+
func: async (_services: any, _data: any, wire: any) => {
|
|
763
|
+
receivedSession = wire.session
|
|
764
|
+
return 'ok'
|
|
765
|
+
},
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
await runPikkuFunc('rpc', Math.random().toString(), 'loadStoredSession', {
|
|
769
|
+
singletonServices: {
|
|
770
|
+
...mockSingletonServices,
|
|
771
|
+
sessionStore: {
|
|
772
|
+
get: async (userId: string) => {
|
|
773
|
+
sessionLookups++
|
|
774
|
+
assert.equal(userId, 'user-1')
|
|
775
|
+
return { userId, loaded: true }
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
} as any,
|
|
779
|
+
data: () => ({}),
|
|
780
|
+
auth: false,
|
|
781
|
+
wire: { pikkuUserId: 'user-1' },
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
assert.equal(sessionLookups, 1)
|
|
785
|
+
assert.deepEqual(receivedSession, { userId: 'user-1', loaded: true })
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
test('should pass addon-scoped singleton services and wrap bare workflow names', async () => {
|
|
789
|
+
pikkuState(null, 'addons', 'packages').set('stripe', {
|
|
790
|
+
package: '@addon/stripe',
|
|
791
|
+
} as any)
|
|
792
|
+
|
|
793
|
+
const workflowCalls: unknown[][] = []
|
|
794
|
+
let singletonFactoryCalls = 0
|
|
795
|
+
let configFactoryCalls = 0
|
|
796
|
+
|
|
797
|
+
pikkuState('@addon/stripe', 'package', 'factories', {
|
|
798
|
+
createConfig: async () => {
|
|
799
|
+
configFactoryCalls++
|
|
800
|
+
return { name: 'addon-config' }
|
|
801
|
+
},
|
|
802
|
+
createSingletonServices: async () => {
|
|
803
|
+
singletonFactoryCalls++
|
|
804
|
+
return {
|
|
805
|
+
logger: mockSingletonServices.logger,
|
|
806
|
+
workflowService: {
|
|
807
|
+
startWorkflow: async (...args: unknown[]) => {
|
|
808
|
+
workflowCalls.push(args)
|
|
809
|
+
return { runId: 'wf-1' }
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
} as any)
|
|
815
|
+
|
|
816
|
+
addFunction(
|
|
817
|
+
'addonFunc',
|
|
818
|
+
{
|
|
819
|
+
func: async (services: any, _data: any) => {
|
|
820
|
+
await services.workflowService.startWorkflow('chargeCustomer', {
|
|
821
|
+
amount: 10,
|
|
822
|
+
})
|
|
823
|
+
await services.workflowService.startWorkflow('stripe:alreadyScoped', {
|
|
824
|
+
amount: 20,
|
|
825
|
+
})
|
|
826
|
+
return 'ok'
|
|
827
|
+
},
|
|
828
|
+
} as any,
|
|
829
|
+
'@addon/stripe'
|
|
830
|
+
)
|
|
831
|
+
pikkuState('@addon/stripe', 'function', 'meta').addonFunc = {
|
|
832
|
+
pikkuFuncId: 'addonFunc',
|
|
833
|
+
sessionless: true,
|
|
834
|
+
permissions: [],
|
|
835
|
+
} as any
|
|
836
|
+
|
|
837
|
+
await runPikkuFunc('rpc', Math.random().toString(), 'addonFunc', {
|
|
838
|
+
singletonServices: {
|
|
839
|
+
...mockSingletonServices,
|
|
840
|
+
config: { parent: true },
|
|
841
|
+
variables: {},
|
|
842
|
+
} as any,
|
|
843
|
+
data: () => ({}),
|
|
844
|
+
auth: false,
|
|
845
|
+
wire: {},
|
|
846
|
+
packageName: '@addon/stripe',
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
assert.equal(configFactoryCalls, 1)
|
|
850
|
+
assert.equal(singletonFactoryCalls, 1)
|
|
851
|
+
assert.deepEqual(workflowCalls, [
|
|
852
|
+
['stripe:chargeCustomer', { amount: 10 }],
|
|
853
|
+
['stripe:alreadyScoped', { amount: 20 }],
|
|
854
|
+
])
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
test('should reuse cached addon singleton services on subsequent calls', async () => {
|
|
858
|
+
let singletonFactoryCalls = 0
|
|
859
|
+
pikkuState('@addon/cache', 'package', 'factories', {
|
|
860
|
+
createSingletonServices: async () => {
|
|
861
|
+
singletonFactoryCalls++
|
|
862
|
+
return {
|
|
863
|
+
logger: mockSingletonServices.logger,
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
} as any)
|
|
867
|
+
|
|
868
|
+
addFunction(
|
|
869
|
+
'cachedFunc',
|
|
870
|
+
{
|
|
871
|
+
func: async () => 'ok',
|
|
872
|
+
} as any,
|
|
873
|
+
'@addon/cache'
|
|
874
|
+
)
|
|
875
|
+
pikkuState('@addon/cache', 'function', 'meta').cachedFunc = {
|
|
876
|
+
pikkuFuncId: 'cachedFunc',
|
|
877
|
+
sessionless: true,
|
|
878
|
+
permissions: [],
|
|
879
|
+
} as any
|
|
880
|
+
|
|
881
|
+
await runPikkuFunc('rpc', '1', 'cachedFunc', {
|
|
882
|
+
singletonServices: mockSingletonServices,
|
|
883
|
+
data: () => ({}),
|
|
884
|
+
auth: false,
|
|
885
|
+
wire: {},
|
|
886
|
+
packageName: '@addon/cache',
|
|
887
|
+
})
|
|
888
|
+
await runPikkuFunc('rpc', '2', 'cachedFunc', {
|
|
889
|
+
singletonServices: mockSingletonServices,
|
|
890
|
+
data: () => ({}),
|
|
891
|
+
auth: false,
|
|
892
|
+
wire: {},
|
|
893
|
+
packageName: '@addon/cache',
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
assert.equal(singletonFactoryCalls, 1)
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
test('should use addon createWireServices instead of the caller createWireServices', async () => {
|
|
900
|
+
let callerCreateWireServicesUsed = false
|
|
901
|
+
let addonCreateWireServicesUsed = false
|
|
902
|
+
let receivedServices: any
|
|
903
|
+
|
|
904
|
+
pikkuState('@addon/wires', 'package', 'factories', {
|
|
905
|
+
createSingletonServices: async () => ({
|
|
906
|
+
logger: mockSingletonServices.logger,
|
|
907
|
+
}),
|
|
908
|
+
createWireServices: async () => {
|
|
909
|
+
addonCreateWireServicesUsed = true
|
|
910
|
+
return { addonWire: true }
|
|
911
|
+
},
|
|
912
|
+
} as any)
|
|
913
|
+
|
|
914
|
+
addFunction(
|
|
915
|
+
'wireFunc',
|
|
916
|
+
{
|
|
917
|
+
func: async (services: any) => {
|
|
918
|
+
receivedServices = services
|
|
919
|
+
return 'ok'
|
|
920
|
+
},
|
|
921
|
+
} as any,
|
|
922
|
+
'@addon/wires'
|
|
923
|
+
)
|
|
924
|
+
pikkuState('@addon/wires', 'function', 'meta').wireFunc = {
|
|
925
|
+
pikkuFuncId: 'wireFunc',
|
|
926
|
+
sessionless: true,
|
|
927
|
+
permissions: [],
|
|
928
|
+
} as any
|
|
929
|
+
|
|
930
|
+
await runPikkuFunc('rpc', 'wire', 'wireFunc', {
|
|
931
|
+
singletonServices: mockSingletonServices,
|
|
932
|
+
createWireServices: async () => {
|
|
933
|
+
callerCreateWireServicesUsed = true
|
|
934
|
+
return { callerWire: true }
|
|
935
|
+
},
|
|
936
|
+
data: () => ({}),
|
|
937
|
+
auth: false,
|
|
938
|
+
wire: {},
|
|
939
|
+
packageName: '@addon/wires',
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
assert.equal(callerCreateWireServicesUsed, false)
|
|
943
|
+
assert.equal(addonCreateWireServicesUsed, true)
|
|
944
|
+
assert.deepEqual(receivedServices, {
|
|
945
|
+
logger: mockSingletonServices.logger,
|
|
946
|
+
addonWire: true,
|
|
947
|
+
})
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
test('should lazily memoize the rpc getter on the wire', async () => {
|
|
951
|
+
let receivedWire: any
|
|
952
|
+
|
|
953
|
+
addTestFunction('rpcGetter', {
|
|
954
|
+
func: async (_services: any, _data: any, wire: any) => {
|
|
955
|
+
receivedWire = wire
|
|
956
|
+
const rpcA = wire.rpc
|
|
957
|
+
const rpcB = wire.rpc
|
|
958
|
+
return rpcA === rpcB
|
|
959
|
+
},
|
|
960
|
+
})
|
|
961
|
+
|
|
962
|
+
const result = await runPikkuFunc('rpc', 'rpc-getter', 'rpcGetter', {
|
|
963
|
+
singletonServices: mockSingletonServices,
|
|
964
|
+
data: () => ({}),
|
|
965
|
+
auth: false,
|
|
966
|
+
wire: {},
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
assert.equal(result, true)
|
|
970
|
+
assert.ok(receivedWire.rpc)
|
|
971
|
+
})
|
|
972
|
+
})
|
|
973
|
+
|
|
974
|
+
describe('function-runner helpers', () => {
|
|
975
|
+
test('runPikkuFuncDirectly should pass through the provided wire and session helpers', async () => {
|
|
976
|
+
let receivedWire: any
|
|
977
|
+
const sessionService = new PikkuSessionService({
|
|
978
|
+
get: async () => undefined,
|
|
979
|
+
} as any)
|
|
980
|
+
|
|
981
|
+
addTestFunction('directFunc', {
|
|
982
|
+
func: async (_services: any, _data: any, wire: any) => {
|
|
983
|
+
receivedWire = wire
|
|
984
|
+
return 'ok'
|
|
985
|
+
},
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
const result = await runPikkuFuncDirectly(
|
|
989
|
+
'directFunc',
|
|
990
|
+
mockServices,
|
|
991
|
+
{ traceId: 'trace-1' },
|
|
992
|
+
{ hello: 'world' },
|
|
993
|
+
sessionService
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
assert.equal(result, 'ok')
|
|
997
|
+
assert.equal(receivedWire.traceId, 'trace-1')
|
|
998
|
+
assert.equal(typeof receivedWire.getSession, 'function')
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
test('getFunctionNames and getAllFunctionNames should include addon namespaces', () => {
|
|
1002
|
+
addTestFunction('rootFunc', { func: async () => 'ok' })
|
|
1003
|
+
pikkuState(null, 'addons', 'packages').set('stripe', {
|
|
1004
|
+
package: '@addon/stripe',
|
|
1005
|
+
} as any)
|
|
1006
|
+
addFunction(
|
|
1007
|
+
'addonFunc',
|
|
1008
|
+
{
|
|
1009
|
+
func: async () => 'ok',
|
|
1010
|
+
} as any,
|
|
1011
|
+
'@addon/stripe'
|
|
1012
|
+
)
|
|
1013
|
+
pikkuState('@addon/stripe', 'function', 'meta').addonFunc = {
|
|
1014
|
+
pikkuFuncId: 'addonFunc',
|
|
1015
|
+
sessionless: true,
|
|
1016
|
+
permissions: [],
|
|
1017
|
+
} as any
|
|
1018
|
+
|
|
1019
|
+
assert.deepEqual(getFunctionNames().sort(), ['rootFunc'])
|
|
1020
|
+
assert.deepEqual(getFunctionNames('@addon/stripe').sort(), ['addonFunc'])
|
|
1021
|
+
assert.deepEqual(getAllFunctionNames().sort(), [
|
|
1022
|
+
'rootFunc',
|
|
1023
|
+
'stripe:addonFunc',
|
|
1024
|
+
])
|
|
1025
|
+
})
|
|
536
1026
|
})
|
|
@@ -271,10 +271,11 @@ export type CorePikkuFunctionConfig<
|
|
|
271
271
|
InputSchema extends StandardSchemaV1 | undefined = undefined,
|
|
272
272
|
OutputSchema extends StandardSchemaV1 | undefined = undefined,
|
|
273
273
|
> = {
|
|
274
|
-
/**
|
|
274
|
+
/** Short human-readable name (e.g. "Create Todo") */
|
|
275
275
|
title?: string
|
|
276
|
-
/**
|
|
276
|
+
/** Longer-form description of what the function does */
|
|
277
277
|
description?: string
|
|
278
|
+
/** Explicit logical name override; lets multiple exports share a versioned base */
|
|
278
279
|
override?: string
|
|
279
280
|
version?: number
|
|
280
281
|
tags?: string[]
|
package/src/handle-error.test.ts
CHANGED
|
@@ -111,7 +111,7 @@ describe('handleHTTPError', () => {
|
|
|
111
111
|
)
|
|
112
112
|
|
|
113
113
|
assert.strictEqual(http._state.statusCode, 400)
|
|
114
|
-
assert.
|
|
114
|
+
assert.strictEqual(http._state.jsonBody.message, 'Invalid input')
|
|
115
115
|
assert.strictEqual(http._state.jsonBody.errorId, 'tracker-2')
|
|
116
116
|
})
|
|
117
117
|
|
package/src/handle-error.ts
CHANGED
|
@@ -36,7 +36,10 @@ export const handleHTTPError = (
|
|
|
36
36
|
// Set status and response body
|
|
37
37
|
http?.response?.status(errorResponse.status)
|
|
38
38
|
http?.response?.json({
|
|
39
|
-
message:
|
|
39
|
+
message:
|
|
40
|
+
e instanceof Error && e.message && e.message !== 'An error occurred'
|
|
41
|
+
? e.message
|
|
42
|
+
: errorResponse.message,
|
|
40
43
|
payload: (e as any).payload,
|
|
41
44
|
errorId: traceId,
|
|
42
45
|
})
|