@pikku/core 0.12.19 → 0.12.21
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 +77 -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/function-runner.js +53 -3
- package/dist/function/functions.types.d.ts +3 -2
- package/dist/index.d.ts +3 -3
- 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/content-service.d.ts +66 -37
- 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/index.d.ts +1 -1
- package/dist/services/local-content.d.ts +10 -12
- package/dist/services/local-content.js +54 -38
- 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.d.ts +3 -2
- package/dist/wirings/rpc/rpc-runner.js +45 -11
- 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/function-runner.ts +68 -3
- package/src/function/functions.types.ts +3 -2
- package/src/index.ts +22 -3
- 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/content-service.ts +79 -51
- 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/index.ts +10 -1
- package/src/services/local-content.test.ts +54 -0
- package/src/services/local-content.ts +80 -53
- 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 +60 -21
- 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
|
@@ -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
|
})
|
|
@@ -72,6 +72,54 @@ async function resolveSession(
|
|
|
72
72
|
* @param parentServices - The parent/caller's singleton services (used as base)
|
|
73
73
|
* @returns The package's singleton services
|
|
74
74
|
*/
|
|
75
|
+
/**
|
|
76
|
+
* Find the consumer-defined namespace (from wireAddon) for a given addon package.
|
|
77
|
+
* Returns null if the package isn't registered as an addon.
|
|
78
|
+
*/
|
|
79
|
+
const findAddonNamespaceForPackage = (packageName: string): string | null => {
|
|
80
|
+
const addons = pikkuState(null, 'addons', 'packages')
|
|
81
|
+
if (!addons) return null
|
|
82
|
+
for (const [namespace, cfg] of addons.entries()) {
|
|
83
|
+
if (cfg?.package === packageName) return namespace
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Wrap a workflow service so that bare workflow names passed from inside an
|
|
90
|
+
* addon function are auto-prefixed with the addon's consumer-facing namespace.
|
|
91
|
+
* Without this, `runToCompletion('myWorkflow')` from inside an addon misses
|
|
92
|
+
* the workflow registered under the addon's package scope and throws
|
|
93
|
+
* WorkflowNotFoundError — forcing addons to hardcode their consumer-defined
|
|
94
|
+
* namespace, which couples the addon to its caller.
|
|
95
|
+
*
|
|
96
|
+
* Explicit `'ns:name'` and bare names that already exist in root meta are
|
|
97
|
+
* unaffected; only bare names that would otherwise miss resolution get
|
|
98
|
+
* prefixed.
|
|
99
|
+
*/
|
|
100
|
+
const wrapWorkflowServiceForPackage = <T extends object>(
|
|
101
|
+
service: T,
|
|
102
|
+
packageName: string
|
|
103
|
+
): T => {
|
|
104
|
+
return new Proxy(service, {
|
|
105
|
+
get(target, prop, receiver) {
|
|
106
|
+
if (prop === 'startWorkflow' || prop === 'runToCompletion') {
|
|
107
|
+
const original = Reflect.get(target, prop, receiver) as Function
|
|
108
|
+
return function (this: any, name: string, ...rest: any[]) {
|
|
109
|
+
if (typeof name === 'string' && !name.includes(':')) {
|
|
110
|
+
const namespace = findAddonNamespaceForPackage(packageName)
|
|
111
|
+
if (namespace) {
|
|
112
|
+
name = `${namespace}:${name}`
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return original.call(this, name, ...rest)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return Reflect.get(target, prop, receiver)
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
75
123
|
const getOrCreatePackageSingletonServices = async (
|
|
76
124
|
packageName: string,
|
|
77
125
|
parentServices: CoreSingletonServices
|
|
@@ -101,6 +149,18 @@ const getOrCreatePackageSingletonServices = async (
|
|
|
101
149
|
parentServices
|
|
102
150
|
)
|
|
103
151
|
|
|
152
|
+
// Wrap workflowService so that bare names used inside the addon's functions
|
|
153
|
+
// resolve to workflows registered under the addon's package scope.
|
|
154
|
+
if (
|
|
155
|
+
packageServices.workflowService &&
|
|
156
|
+
typeof packageServices.workflowService === 'object'
|
|
157
|
+
) {
|
|
158
|
+
packageServices.workflowService = wrapWorkflowServiceForPackage(
|
|
159
|
+
packageServices.workflowService as object,
|
|
160
|
+
packageName
|
|
161
|
+
) as typeof packageServices.workflowService
|
|
162
|
+
}
|
|
163
|
+
|
|
104
164
|
// Cache the services
|
|
105
165
|
pikkuState(packageName, 'package', 'singletonServices', packageServices)
|
|
106
166
|
|
|
@@ -378,11 +438,16 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
378
438
|
wireServices && Object.keys(wireServices).length > 0
|
|
379
439
|
? { ...resolvedSingletonServices, ...wireServices }
|
|
380
440
|
: resolvedSingletonServices
|
|
441
|
+
const callerPackageName = packageName
|
|
381
442
|
Object.defineProperty(resolvedWire, 'rpc', {
|
|
382
443
|
get() {
|
|
383
|
-
const rpc = rpcService.getContextRPCService(
|
|
384
|
-
|
|
385
|
-
|
|
444
|
+
const rpc = rpcService.getContextRPCService(
|
|
445
|
+
services,
|
|
446
|
+
resolvedWire,
|
|
447
|
+
{ sessionService },
|
|
448
|
+
0,
|
|
449
|
+
callerPackageName
|
|
450
|
+
)
|
|
386
451
|
Object.defineProperty(resolvedWire, 'rpc', {
|
|
387
452
|
value: rpc,
|
|
388
453
|
writable: true,
|
|
@@ -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[]
|