@pikku/core 0.12.4 → 0.12.6
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 +21 -1
- package/dist/dev/hot-reload.d.ts +10 -0
- package/dist/dev/hot-reload.js +158 -0
- package/dist/middleware-runner.d.ts +1 -0
- package/dist/middleware-runner.js +5 -0
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.js +5 -0
- package/dist/services/content-service.d.ts +2 -0
- package/dist/services/in-memory-workflow-service.d.ts +1 -0
- package/dist/services/in-memory-workflow-service.js +16 -11
- package/dist/services/workflow-service.d.ts +1 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.js +16 -1
- package/dist/wirings/channel/channel-middleware-runner.d.ts +1 -0
- package/dist/wirings/channel/channel-middleware-runner.js +5 -0
- package/dist/wirings/http/http-runner.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +6 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +51 -16
- package/dist/wirings/workflow/workflow.types.d.ts +2 -0
- package/package.json +5 -3
- package/src/crypto-utils.test.ts +214 -0
- package/src/crypto-utils.ts +213 -0
- package/src/dev/hot-reload.test.ts +484 -0
- package/src/dev/hot-reload.ts +212 -0
- package/src/errors/error-handler.ts +62 -0
- package/src/errors/error.test.ts +195 -0
- package/src/errors/errors.ts +374 -0
- package/src/errors/index.ts +2 -0
- package/src/factory-functions.test.ts +109 -0
- package/src/function/function-runner.test.ts +536 -0
- package/src/function/function-runner.ts +365 -0
- package/src/function/functions.types.ts +304 -0
- package/src/function/index.ts +5 -0
- package/src/handle-error.test.ts +424 -0
- package/src/handle-error.ts +69 -0
- package/src/index.ts +118 -0
- package/src/internal.ts +6 -0
- package/src/middleware/auth-apikey.test.ts +363 -0
- package/src/middleware/auth-apikey.ts +47 -0
- package/src/middleware/auth-bearer.test.ts +450 -0
- package/src/middleware/auth-bearer.ts +72 -0
- package/src/middleware/auth-cookie.test.ts +528 -0
- package/src/middleware/auth-cookie.ts +80 -0
- package/src/middleware/cors.test.ts +424 -0
- package/src/middleware/cors.ts +104 -0
- package/src/middleware/index.ts +5 -0
- package/src/middleware/remote-auth.test.ts +488 -0
- package/src/middleware/remote-auth.ts +68 -0
- package/src/middleware/timeout.ts +15 -0
- package/src/middleware-runner.test.ts +418 -0
- package/src/middleware-runner.ts +240 -0
- package/src/permissions.test.ts +434 -0
- package/src/permissions.ts +327 -0
- package/src/pikku-request.ts +23 -0
- package/src/pikku-response.ts +5 -0
- package/src/pikku-state.test.ts +224 -0
- package/src/pikku-state.ts +216 -0
- package/src/run-tests-script.test.ts +49 -0
- package/src/schema.test.ts +249 -0
- package/src/schema.ts +151 -0
- package/src/services/ai-agent-runner-service.ts +41 -0
- package/src/services/ai-run-state-service.ts +20 -0
- package/src/services/ai-storage-service.ts +39 -0
- package/src/services/content-service.ts +81 -0
- package/src/services/deployment-service.ts +22 -0
- package/src/services/gateway-service.ts +20 -0
- package/src/services/gopass-secrets.ts +98 -0
- package/src/services/in-memory-ai-run-state-service.ts +73 -0
- package/src/services/in-memory-trigger-service.ts +64 -0
- package/src/services/in-memory-workflow-service.test.ts +351 -0
- package/src/services/in-memory-workflow-service.ts +512 -0
- package/src/services/index.ts +56 -0
- package/src/services/jwt-service.ts +30 -0
- package/src/services/local-content.ts +120 -0
- package/src/services/local-gateway-service.ts +62 -0
- package/src/services/local-secrets.test.ts +80 -0
- package/src/services/local-secrets.ts +94 -0
- package/src/services/local-variables.test.ts +61 -0
- package/src/services/local-variables.ts +39 -0
- package/src/services/logger-console.test.ts +118 -0
- package/src/services/logger-console.ts +98 -0
- package/src/services/logger.ts +57 -0
- package/src/services/scheduler-service.ts +86 -0
- package/src/services/schema-service.ts +33 -0
- package/src/services/scoped-secret-service.test.ts +74 -0
- package/src/services/scoped-secret-service.ts +41 -0
- package/src/services/secret-service.ts +38 -0
- package/src/services/trigger-service.ts +17 -0
- package/src/services/typed-secret-service.test.ts +93 -0
- package/src/services/typed-secret-service.ts +70 -0
- package/src/services/typed-variables-service.test.ts +73 -0
- package/src/services/typed-variables-service.ts +81 -0
- package/src/services/user-session-service.test.ts +113 -0
- package/src/services/user-session-service.ts +86 -0
- package/src/services/variables-service.ts +11 -0
- package/src/services/workflow-service.ts +96 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/service-tests.ts +873 -0
- package/src/time-utils.test.ts +56 -0
- package/src/time-utils.ts +107 -0
- package/src/types/core.types.ts +478 -0
- package/src/types/state.types.ts +188 -0
- package/src/utils/hash.test.ts +68 -0
- package/src/utils/hash.ts +26 -0
- package/src/utils.test.ts +350 -0
- package/src/utils.ts +129 -0
- package/src/version.test.ts +80 -0
- package/src/version.ts +25 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
- package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
- package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +310 -0
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
- package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +659 -0
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
- package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +319 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +773 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +859 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +1153 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +382 -0
- package/src/wirings/ai-agent/index.ts +37 -0
- package/src/wirings/channel/channel-common.ts +90 -0
- package/src/wirings/channel/channel-handler.ts +250 -0
- package/src/wirings/channel/channel-middleware-runner.ts +126 -0
- package/src/wirings/channel/channel-runner.ts +166 -0
- package/src/wirings/channel/channel-store.ts +29 -0
- package/src/wirings/channel/channel.types.ts +172 -0
- package/src/wirings/channel/define-channel-routes.ts +25 -0
- package/src/wirings/channel/eventhub-service.ts +34 -0
- package/src/wirings/channel/eventhub-store.ts +13 -0
- package/src/wirings/channel/index.ts +24 -0
- package/src/wirings/channel/local/index.ts +3 -0
- package/src/wirings/channel/local/local-channel-handler.ts +81 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +215 -0
- package/src/wirings/channel/local/local-channel-runner.ts +210 -0
- package/src/wirings/channel/local/local-eventhub-service.test.ts +95 -0
- package/src/wirings/channel/local/local-eventhub-service.ts +101 -0
- package/src/wirings/channel/log-channels.ts +20 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +54 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +45 -0
- package/src/wirings/channel/serverless/index.ts +5 -0
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +289 -0
- package/src/wirings/cli/channel/cli-channel-runner.ts +136 -0
- package/src/wirings/cli/channel/index.ts +1 -0
- package/src/wirings/cli/cli-runner.test.ts +403 -0
- package/src/wirings/cli/cli-runner.ts +529 -0
- package/src/wirings/cli/cli.types.ts +358 -0
- package/src/wirings/cli/command-parser.test.ts +445 -0
- package/src/wirings/cli/command-parser.ts +504 -0
- package/src/wirings/cli/define-cli-commands.ts +24 -0
- package/src/wirings/cli/index.ts +19 -0
- package/src/wirings/gateway/gateway-runner.test.ts +474 -0
- package/src/wirings/gateway/gateway-runner.ts +411 -0
- package/src/wirings/gateway/gateway.types.ts +149 -0
- package/src/wirings/gateway/index.ts +13 -0
- package/src/wirings/http/http-routes.test.ts +322 -0
- package/src/wirings/http/http-routes.ts +197 -0
- package/src/wirings/http/http-runner.test.ts +144 -0
- package/src/wirings/http/http-runner.ts +588 -0
- package/src/wirings/http/http.types.ts +369 -0
- package/src/wirings/http/index.ts +28 -0
- package/src/wirings/http/log-http-routes.ts +22 -0
- package/src/wirings/http/pikku-fetch-http-request.test.ts +237 -0
- package/src/wirings/http/pikku-fetch-http-request.ts +170 -0
- package/src/wirings/http/pikku-fetch-http-response.test.ts +82 -0
- package/src/wirings/http/pikku-fetch-http-response.ts +147 -0
- package/src/wirings/http/routers/http-router.ts +13 -0
- package/src/wirings/http/routers/path-to-regex.test.ts +319 -0
- package/src/wirings/http/routers/path-to-regex.ts +126 -0
- package/src/wirings/http/web-request.test.ts +236 -0
- package/src/wirings/http/web-request.ts +104 -0
- package/src/wirings/mcp/index.ts +27 -0
- package/src/wirings/mcp/mcp-endpoint-registry.test.ts +389 -0
- package/src/wirings/mcp/mcp-endpoint-registry.ts +159 -0
- package/src/wirings/mcp/mcp-runner.ts +323 -0
- package/src/wirings/mcp/mcp.types.ts +216 -0
- package/src/wirings/node/index.ts +2 -0
- package/src/wirings/node/node.types.ts +23 -0
- package/src/wirings/oauth2/index.ts +3 -0
- package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
- package/src/wirings/oauth2/oauth2-client.ts +335 -0
- package/src/wirings/oauth2/oauth2.types.ts +69 -0
- package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
- package/src/wirings/queue/index.ts +31 -0
- package/src/wirings/queue/queue-runner.test.ts +710 -0
- package/src/wirings/queue/queue-runner.ts +192 -0
- package/src/wirings/queue/queue.types.ts +189 -0
- package/src/wirings/queue/register-queue-helper.ts +60 -0
- package/src/wirings/queue/validate-worker-config.test.ts +108 -0
- package/src/wirings/queue/validate-worker-config.ts +116 -0
- package/src/wirings/rpc/index.ts +4 -0
- package/src/wirings/rpc/rpc-runner.ts +460 -0
- package/src/wirings/rpc/rpc-types.ts +56 -0
- package/src/wirings/rpc/wire-addon.ts +21 -0
- package/src/wirings/scheduler/index.ts +11 -0
- package/src/wirings/scheduler/log-schedulers.ts +20 -0
- package/src/wirings/scheduler/scheduler-runner.test.ts +660 -0
- package/src/wirings/scheduler/scheduler-runner.ts +133 -0
- package/src/wirings/scheduler/scheduler.types.ts +53 -0
- package/src/wirings/secret/index.ts +9 -0
- package/src/wirings/secret/secret.types.ts +32 -0
- package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
- package/src/wirings/secret/validate-secret-definitions.ts +82 -0
- package/src/wirings/trigger/index.ts +10 -0
- package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
- package/src/wirings/trigger/trigger-runner.test.ts +79 -0
- package/src/wirings/trigger/trigger-runner.ts +135 -0
- package/src/wirings/trigger/trigger.types.ts +178 -0
- package/src/wirings/variable/index.ts +8 -0
- package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
- package/src/wirings/variable/validate-variable-definitions.ts +69 -0
- package/src/wirings/variable/variable.types.ts +22 -0
- package/src/wirings/workflow/dsl/index.ts +31 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +320 -0
- package/src/wirings/workflow/dsl/workflow-runner.ts +28 -0
- package/src/wirings/workflow/graph/graph-node.ts +222 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
- package/src/wirings/workflow/graph/graph-runner.ts +816 -0
- package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
- package/src/wirings/workflow/graph/graph-validation.ts +237 -0
- package/src/wirings/workflow/graph/index.ts +19 -0
- package/src/wirings/workflow/graph/template.test.ts +49 -0
- package/src/wirings/workflow/graph/template.ts +42 -0
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +104 -0
- package/src/wirings/workflow/index.ts +82 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +1229 -0
- package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
- package/src/wirings/workflow/workflow-helpers.ts +79 -0
- package/src/wirings/workflow/workflow.types.ts +276 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/lcov.info +0 -18891
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import type { RelativeTimeInput } from './time-utils'
|
|
5
|
+
import {
|
|
6
|
+
getRelativeTimeOffset,
|
|
7
|
+
getRelativeTimeOffsetFromNow,
|
|
8
|
+
} from './time-utils'
|
|
9
|
+
|
|
10
|
+
const approxEqual = (a: number, b: number, delta: number = 10): boolean =>
|
|
11
|
+
Math.abs(a - b) <= delta
|
|
12
|
+
|
|
13
|
+
describe('Time Utils', () => {
|
|
14
|
+
const offsetCases: Array<{ input: RelativeTimeInput; expected: number }> = [
|
|
15
|
+
{ input: { value: 1, unit: 'second' }, expected: 1000 },
|
|
16
|
+
{ input: { value: 2, unit: 'minute' }, expected: 120000 },
|
|
17
|
+
{ input: { value: 1.5, unit: 'hour' }, expected: 5400000 },
|
|
18
|
+
{ input: { value: -1, unit: 'day' }, expected: -86400000 },
|
|
19
|
+
{ input: { value: 1, unit: 'week' }, expected: 604800000 },
|
|
20
|
+
{
|
|
21
|
+
input: { value: 2, unit: 'year' },
|
|
22
|
+
expected: Math.round(2 * 365.25 * 86400 * 1000),
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
describe('getRelativeTimeOffset', () => {
|
|
27
|
+
for (const { input, expected } of offsetCases) {
|
|
28
|
+
test(`returns ${expected} ms for ${input.value} ${input.unit}`, () => {
|
|
29
|
+
const actual = getRelativeTimeOffset(input)
|
|
30
|
+
assert.strictEqual(actual, expected)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('getRelativeTimeOffsetFromNow', () => {
|
|
36
|
+
test('returns a Date ~1 minute in the future', () => {
|
|
37
|
+
const now = Date.now()
|
|
38
|
+
const result = getRelativeTimeOffsetFromNow({ value: 1, unit: 'minute' })
|
|
39
|
+
const delta = result.getTime() - now
|
|
40
|
+
assert.ok(
|
|
41
|
+
approxEqual(delta, 60000),
|
|
42
|
+
`Expected ~60000ms ahead, got ${delta}ms`
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('returns a Date ~2 hours in the past', () => {
|
|
47
|
+
const now = Date.now()
|
|
48
|
+
const result = getRelativeTimeOffsetFromNow({ value: -2, unit: 'hour' })
|
|
49
|
+
const delta = now - result.getTime()
|
|
50
|
+
assert.ok(
|
|
51
|
+
approxEqual(delta, 7200000),
|
|
52
|
+
`Expected ~7200000ms ago, got ${delta}ms`
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
type TimeUnit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'year'
|
|
2
|
+
|
|
3
|
+
export interface RelativeTimeInput {
|
|
4
|
+
value: number // negative = in the past
|
|
5
|
+
unit: TimeUnit
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const multipliers: Record<TimeUnit, number> = {
|
|
9
|
+
second: 1,
|
|
10
|
+
minute: 60,
|
|
11
|
+
hour: 60 * 60,
|
|
12
|
+
day: 60 * 60 * 24,
|
|
13
|
+
week: 60 * 60 * 24 * 7,
|
|
14
|
+
year: Math.round(365.25 * 60 * 60 * 24),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const getRelativeTimeOffset = ({
|
|
18
|
+
value,
|
|
19
|
+
unit,
|
|
20
|
+
}: RelativeTimeInput): number => {
|
|
21
|
+
return Math.round(value * multipliers[unit]) * 1000 // convert to milliseconds
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns a Unix timestamp (in seconds) offset from now by the given duration.
|
|
26
|
+
* e.g. value = -1 and unit = 'day' => 1 day ago => now - 86400 seconds
|
|
27
|
+
*/
|
|
28
|
+
export const getRelativeTimeOffsetFromNow = (
|
|
29
|
+
relativeTime: RelativeTimeInput
|
|
30
|
+
): Date => {
|
|
31
|
+
return new Date(Date.now() + getRelativeTimeOffset(relativeTime))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get duration in milliseconds from a string or number
|
|
36
|
+
* @param duration
|
|
37
|
+
* @returns
|
|
38
|
+
*/
|
|
39
|
+
export const getDurationInMilliseconds = (
|
|
40
|
+
duration: string | number
|
|
41
|
+
): number => {
|
|
42
|
+
if (typeof duration === 'number') {
|
|
43
|
+
return duration
|
|
44
|
+
}
|
|
45
|
+
return parseDurationString(duration)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse a duration string to milliseconds
|
|
50
|
+
* Supports formats like: '2s', '5s', '5sec', '5seconds', '5m', '5min', '5minutes', '1h', '1hour', '2d', '2day', '1w', '1week'
|
|
51
|
+
*
|
|
52
|
+
* @param duration - Duration string (e.g., '2s', '5min', '2hours', '1day')
|
|
53
|
+
* @returns Duration in milliseconds
|
|
54
|
+
*/
|
|
55
|
+
export const parseDurationString = (duration: string): number => {
|
|
56
|
+
const match = duration.match(
|
|
57
|
+
/^(\d+)(ms|milliseconds?|s|sec|seconds?|m|min|minutes?|h|hour|hours?|d|day|days?|w|week|weeks?|y|year|years?)$/
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if (!match) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Invalid duration format: ${duration}. Use formats like '2s', '5s', '5min', '1hour', '2days', '1week'`
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const value = parseInt(match[1], 10)
|
|
67
|
+
const unitStr = match[2]
|
|
68
|
+
|
|
69
|
+
// Handle milliseconds specially
|
|
70
|
+
if (
|
|
71
|
+
unitStr === 'ms' ||
|
|
72
|
+
unitStr === 'millisecond' ||
|
|
73
|
+
unitStr === 'milliseconds'
|
|
74
|
+
) {
|
|
75
|
+
return value
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Map string variations to TimeUnit
|
|
79
|
+
let unit: TimeUnit
|
|
80
|
+
if (
|
|
81
|
+
unitStr === 's' ||
|
|
82
|
+
unitStr === 'sec' ||
|
|
83
|
+
unitStr === 'second' ||
|
|
84
|
+
unitStr === 'seconds'
|
|
85
|
+
) {
|
|
86
|
+
unit = 'second'
|
|
87
|
+
} else if (
|
|
88
|
+
unitStr === 'm' ||
|
|
89
|
+
unitStr === 'min' ||
|
|
90
|
+
unitStr === 'minute' ||
|
|
91
|
+
unitStr === 'minutes'
|
|
92
|
+
) {
|
|
93
|
+
unit = 'minute'
|
|
94
|
+
} else if (unitStr === 'h' || unitStr === 'hour' || unitStr === 'hours') {
|
|
95
|
+
unit = 'hour'
|
|
96
|
+
} else if (unitStr === 'd' || unitStr === 'day' || unitStr === 'days') {
|
|
97
|
+
unit = 'day'
|
|
98
|
+
} else if (unitStr === 'w' || unitStr === 'week' || unitStr === 'weeks') {
|
|
99
|
+
unit = 'week'
|
|
100
|
+
} else if (unitStr === 'y' || unitStr === 'year' || unitStr === 'years') {
|
|
101
|
+
unit = 'year'
|
|
102
|
+
} else {
|
|
103
|
+
throw new Error(`Unknown time unit: ${unitStr}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return getRelativeTimeOffset({ value, unit })
|
|
107
|
+
}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import type { Logger, LogLevel } from '../services/logger.js'
|
|
2
|
+
import type { VariablesService } from '../services/variables-service.js'
|
|
3
|
+
import type { SecretService } from '../services/secret-service.js'
|
|
4
|
+
import type { SchemaService } from '../services/schema-service.js'
|
|
5
|
+
import type { JWTService } from '../services/jwt-service.js'
|
|
6
|
+
import type { PikkuHTTP } from '../wirings/http/http.types.js'
|
|
7
|
+
import type {
|
|
8
|
+
PikkuChannel,
|
|
9
|
+
CorePikkuChannelMiddleware,
|
|
10
|
+
CorePikkuChannelMiddlewareFactory,
|
|
11
|
+
} from '../wirings/channel/channel.types.js'
|
|
12
|
+
import type { PikkuRPC } from '../wirings/rpc/rpc-types.js'
|
|
13
|
+
import type { PikkuMCP } from '../wirings/mcp/mcp.types.js'
|
|
14
|
+
import type { PikkuScheduledTask } from '../wirings/scheduler/scheduler.types.js'
|
|
15
|
+
import type { PikkuQueue, QueueService } from '../wirings/queue/queue.types.js'
|
|
16
|
+
import type { PikkuCLI } from '../wirings/cli/cli.types.js'
|
|
17
|
+
import type {
|
|
18
|
+
PikkuWorkflowWire,
|
|
19
|
+
WorkflowService,
|
|
20
|
+
WorkflowServiceConfig,
|
|
21
|
+
WorkflowStepWire,
|
|
22
|
+
} from '../wirings/workflow/workflow.types.js'
|
|
23
|
+
import type { PikkuGraphWire } from '../wirings/workflow/graph/workflow-graph.types.js'
|
|
24
|
+
import type { PikkuTrigger } from '../wirings/trigger/trigger.types.js'
|
|
25
|
+
import type { PikkuGateway } from '../wirings/gateway/gateway.types.js'
|
|
26
|
+
import type { SchedulerService } from '../services/scheduler-service.js'
|
|
27
|
+
import type { DeploymentService } from '../services/deployment-service.js'
|
|
28
|
+
import type { AIStorageService } from '../services/ai-storage-service.js'
|
|
29
|
+
|
|
30
|
+
import type { ContentService } from '../services/content-service.js'
|
|
31
|
+
import type { AIAgentRunnerService } from '../services/ai-agent-runner-service.js'
|
|
32
|
+
import type { AIRunStateService } from '../services/ai-run-state-service.js'
|
|
33
|
+
import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js'
|
|
34
|
+
import type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.js'
|
|
35
|
+
import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js'
|
|
36
|
+
|
|
37
|
+
export type PikkuWiringTypes =
|
|
38
|
+
| 'http'
|
|
39
|
+
| 'scheduler'
|
|
40
|
+
| 'trigger'
|
|
41
|
+
| 'channel'
|
|
42
|
+
| 'rpc'
|
|
43
|
+
| 'queue'
|
|
44
|
+
| 'mcp'
|
|
45
|
+
| 'cli'
|
|
46
|
+
| 'workflow'
|
|
47
|
+
| 'agent'
|
|
48
|
+
| 'gateway'
|
|
49
|
+
|
|
50
|
+
export interface FunctionServicesMeta {
|
|
51
|
+
optimized: boolean
|
|
52
|
+
services: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface FunctionWiresMeta {
|
|
56
|
+
optimized: boolean
|
|
57
|
+
wires: string[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Metadata for middleware at any level
|
|
62
|
+
* - type: 'http' = HTTP route middleware group (references httpGroup in pikkuState)
|
|
63
|
+
* - type: 'tag' = Tag-based middleware group (references tagGroup in pikkuState)
|
|
64
|
+
* - type: 'wire' = Wire-level individual middleware
|
|
65
|
+
*/
|
|
66
|
+
export type MiddlewareMetadata =
|
|
67
|
+
| {
|
|
68
|
+
type: 'http'
|
|
69
|
+
route: string // Route pattern (e.g., '*' for all, '/api/*' for specific)
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
type: 'tag'
|
|
73
|
+
tag: string // Tag name
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
type: 'wire'
|
|
77
|
+
name: string
|
|
78
|
+
inline?: boolean // true if inline middleware
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Metadata for permissions at any level
|
|
83
|
+
* - type: 'http' = HTTP route permission group (references httpGroup in pikkuState)
|
|
84
|
+
* - type: 'tag' = Tag-based permission group (references tagGroup in pikkuState)
|
|
85
|
+
* - type: 'wire' = Wire-level individual permission
|
|
86
|
+
*/
|
|
87
|
+
export type PermissionMetadata =
|
|
88
|
+
| {
|
|
89
|
+
type: 'http'
|
|
90
|
+
route: string // Route pattern (e.g., '*' for all, '/api/*' for specific)
|
|
91
|
+
}
|
|
92
|
+
| {
|
|
93
|
+
type: 'tag'
|
|
94
|
+
tag: string // Tag name
|
|
95
|
+
}
|
|
96
|
+
| {
|
|
97
|
+
type: 'wire'
|
|
98
|
+
name: string
|
|
99
|
+
inline?: boolean // true if inline permission
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type FunctionRuntimeMeta = {
|
|
103
|
+
pikkuFuncId: string
|
|
104
|
+
inputSchemaName: string | null
|
|
105
|
+
outputSchemaName: string | null
|
|
106
|
+
expose?: boolean
|
|
107
|
+
remote?: boolean
|
|
108
|
+
mcp?: boolean
|
|
109
|
+
readonly?: boolean
|
|
110
|
+
sessionless?: boolean
|
|
111
|
+
version?: number
|
|
112
|
+
approvalRequired?: boolean
|
|
113
|
+
approvalDescription?: string
|
|
114
|
+
contractHash?: string
|
|
115
|
+
inputHash?: string
|
|
116
|
+
outputHash?: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type FunctionMeta = FunctionRuntimeMeta &
|
|
120
|
+
Partial<
|
|
121
|
+
{
|
|
122
|
+
name: string
|
|
123
|
+
functionType: 'user' | 'inline' | 'helper'
|
|
124
|
+
funcWrapper: string
|
|
125
|
+
services: FunctionServicesMeta
|
|
126
|
+
wires: FunctionWiresMeta
|
|
127
|
+
inputs: string[] | null
|
|
128
|
+
outputs: string[] | null
|
|
129
|
+
middleware: MiddlewareMetadata[]
|
|
130
|
+
permissions: PermissionMetadata[]
|
|
131
|
+
isDirectFunction: boolean
|
|
132
|
+
} & CommonWireMeta
|
|
133
|
+
>
|
|
134
|
+
|
|
135
|
+
export type FunctionsRuntimeMeta = Record<string, FunctionRuntimeMeta>
|
|
136
|
+
export type FunctionsMeta = Record<string, FunctionMeta>
|
|
137
|
+
|
|
138
|
+
// Optimized runtime metadata with only essential fields
|
|
139
|
+
|
|
140
|
+
export type MakeRequired<T, K extends keyof T> = Omit<T, K> &
|
|
141
|
+
Required<Pick<T, K>>
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Represents a JSON primitive type which can be a string, number, boolean, null, or undefined.
|
|
145
|
+
*/
|
|
146
|
+
export type JSONPrimitive = string | number | boolean | null | undefined
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Represents a JSON value which can be a primitive, an array, or an object.
|
|
150
|
+
*/
|
|
151
|
+
export type JSONValue =
|
|
152
|
+
| JSONPrimitive
|
|
153
|
+
| JSONValue[]
|
|
154
|
+
| {
|
|
155
|
+
[key: string]: JSONValue
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Utility type for making certain keys required and leaving the rest as optional.
|
|
160
|
+
*/
|
|
161
|
+
export type PickRequired<T, K extends keyof T> = Required<Pick<T, K>> &
|
|
162
|
+
Partial<T>
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Utility type for making certain keys optional while keeping the rest required.
|
|
166
|
+
*/
|
|
167
|
+
export type PickOptional<T, K extends keyof T> = Partial<T> & Pick<T, K>
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Utility type that ensures at least one key in the given type `T` is required.
|
|
171
|
+
*/
|
|
172
|
+
export type RequireAtLeastOne<T> = {
|
|
173
|
+
[K in keyof T]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>>
|
|
174
|
+
}[keyof T]
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Interface for the core configuration settings of Pikku.
|
|
178
|
+
*/
|
|
179
|
+
export type CoreConfig<Config extends Record<string, unknown> = {}> = {
|
|
180
|
+
/** The log level for the application. */
|
|
181
|
+
logLevel?: LogLevel
|
|
182
|
+
/** Secrets used by the application (optional). */
|
|
183
|
+
secrets?: {}
|
|
184
|
+
|
|
185
|
+
workflow?: WorkflowServiceConfig
|
|
186
|
+
} & Config
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Represents a core user session, which can be extended for more specific session information.
|
|
190
|
+
*/
|
|
191
|
+
export interface CoreUserSession {}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Interface for core singleton services provided by Pikku.
|
|
195
|
+
*/
|
|
196
|
+
export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
|
|
197
|
+
/** The schema library used to validate data */
|
|
198
|
+
schema?: SchemaService
|
|
199
|
+
/** The JWT service used to encode and decode tokens */
|
|
200
|
+
jwt?: JWTService
|
|
201
|
+
/** The core configuration for the application. */
|
|
202
|
+
config: Config
|
|
203
|
+
/** The logger used by the application. */
|
|
204
|
+
logger: Logger
|
|
205
|
+
/** The variable service to be used */
|
|
206
|
+
variables: VariablesService
|
|
207
|
+
/** The secrets service to retrieve secrets */
|
|
208
|
+
secrets: SecretService
|
|
209
|
+
/** The workflow orchestrator service */
|
|
210
|
+
workflowService?: WorkflowService
|
|
211
|
+
/** The queue service */
|
|
212
|
+
queueService?: QueueService
|
|
213
|
+
/** The scheduler service */
|
|
214
|
+
schedulerService?: SchedulerService
|
|
215
|
+
/** The deployment service for service discovery */
|
|
216
|
+
deploymentService?: DeploymentService
|
|
217
|
+
/** AI agent storage service (threads, messages, working memory) */
|
|
218
|
+
aiStorage?: AIStorageService
|
|
219
|
+
|
|
220
|
+
/** Content service for file storage and retrieval */
|
|
221
|
+
content?: ContentService
|
|
222
|
+
/** AI agent runner service (model calls + tool loop) */
|
|
223
|
+
aiAgentRunner?: AIAgentRunnerService
|
|
224
|
+
/** AI run state service (run lifecycle + approval persistence) */
|
|
225
|
+
aiRunState?: AIRunStateService
|
|
226
|
+
/** Agent run service (listing threads, runs, steps) */
|
|
227
|
+
agentRunService?: AgentRunService
|
|
228
|
+
/** Workflow run service (listing workflow runs) */
|
|
229
|
+
workflowRunService?: WorkflowRunService
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Represents different forms of wire within Pikku and the outside world.
|
|
234
|
+
*/
|
|
235
|
+
export type PikkuWire<
|
|
236
|
+
In = unknown,
|
|
237
|
+
Out = unknown,
|
|
238
|
+
HasInitialSession extends boolean = false,
|
|
239
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
240
|
+
TypedRPC extends PikkuRPC = PikkuRPC,
|
|
241
|
+
IsChannel extends true | null = null,
|
|
242
|
+
MCPTools extends string | never = never,
|
|
243
|
+
TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire,
|
|
244
|
+
TriggerOutput = unknown,
|
|
245
|
+
> = Partial<{
|
|
246
|
+
wireType: PikkuWiringTypes
|
|
247
|
+
wireId: string
|
|
248
|
+
http: PikkuHTTP<In>
|
|
249
|
+
mcp: PikkuMCP<MCPTools>
|
|
250
|
+
rpc: TypedRPC
|
|
251
|
+
channel: [IsChannel] extends [null]
|
|
252
|
+
? PikkuChannel<unknown, Out>
|
|
253
|
+
: PikkuChannel<unknown, Out> | undefined
|
|
254
|
+
scheduledTask: PikkuScheduledTask
|
|
255
|
+
queue: PikkuQueue
|
|
256
|
+
cli: PikkuCLI
|
|
257
|
+
workflow: TypedWorkflow
|
|
258
|
+
workflowStep: WorkflowStepWire
|
|
259
|
+
graph: PikkuGraphWire
|
|
260
|
+
trigger: PikkuTrigger<TriggerOutput>
|
|
261
|
+
gateway: PikkuGateway
|
|
262
|
+
session: HasInitialSession extends true
|
|
263
|
+
? UserSession
|
|
264
|
+
: UserSession | undefined
|
|
265
|
+
/** Update and persist the current session */
|
|
266
|
+
setSession: (session: CoreUserSession) => Promise<void> | void
|
|
267
|
+
/** Clear and persist the current session */
|
|
268
|
+
clearSession: () => Promise<void> | void
|
|
269
|
+
/** Fetch the latest session (may read from backing store) */
|
|
270
|
+
getSession: () => Promise<UserSession> | UserSession | undefined
|
|
271
|
+
/** Whether the session was modified during this run */
|
|
272
|
+
hasSessionChanged: () => boolean
|
|
273
|
+
}>
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* A function that can wrap an wire and be called before or after
|
|
277
|
+
*/
|
|
278
|
+
export type CorePikkuMiddleware<
|
|
279
|
+
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
280
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
281
|
+
> = (
|
|
282
|
+
services: SingletonServices,
|
|
283
|
+
wires: PikkuWire<unknown, unknown, false, UserSession>,
|
|
284
|
+
next: () => Promise<void>
|
|
285
|
+
) => Promise<void>
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Configuration object for creating middleware with metadata
|
|
289
|
+
*
|
|
290
|
+
* @template SingletonServices - The singleton services type
|
|
291
|
+
* @template UserSession - The user session type
|
|
292
|
+
*/
|
|
293
|
+
export type CorePikkuMiddlewareConfig<
|
|
294
|
+
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
295
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
296
|
+
> = {
|
|
297
|
+
/** The middleware function */
|
|
298
|
+
func: CorePikkuMiddleware<SingletonServices, UserSession>
|
|
299
|
+
/** Optional human-readable name for the middleware */
|
|
300
|
+
name?: string
|
|
301
|
+
/** Optional description of what the middleware does */
|
|
302
|
+
description?: string
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* A factory function that takes input and returns middleware
|
|
307
|
+
* Used when middleware needs configuration/input parameters
|
|
308
|
+
*/
|
|
309
|
+
export type CorePikkuMiddlewareFactory<
|
|
310
|
+
In = any,
|
|
311
|
+
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
312
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
313
|
+
> = (input: In) => CorePikkuMiddleware<SingletonServices, UserSession>
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* A group of middleware (combination of regular middleware and factories)
|
|
317
|
+
* Used with addMiddleware() and addHTTPMiddleware() to group related middleware together
|
|
318
|
+
*/
|
|
319
|
+
export type CorePikkuMiddlewareGroup<
|
|
320
|
+
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
321
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
322
|
+
> = Array<
|
|
323
|
+
| CorePikkuMiddleware<SingletonServices, UserSession>
|
|
324
|
+
| CorePikkuMiddlewareFactory<any, SingletonServices, UserSession>
|
|
325
|
+
>
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Factory function for creating middleware with tree-shaking support
|
|
329
|
+
* Supports both direct function and configuration object syntax
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```typescript
|
|
333
|
+
* // Direct function syntax
|
|
334
|
+
* export const logMiddleware = pikkuMiddleware(
|
|
335
|
+
* async ({ logger }, next) => {
|
|
336
|
+
* logger.info('Request started')
|
|
337
|
+
* await next()
|
|
338
|
+
* }
|
|
339
|
+
* )
|
|
340
|
+
*
|
|
341
|
+
* // Configuration object syntax with metadata
|
|
342
|
+
* export const logMiddleware = pikkuMiddleware({
|
|
343
|
+
* name: 'Request Logger',
|
|
344
|
+
* description: 'Logs request information',
|
|
345
|
+
* func: async ({ logger }, next) => {
|
|
346
|
+
* logger.info('Request started')
|
|
347
|
+
* await next()
|
|
348
|
+
* }
|
|
349
|
+
* })
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
352
|
+
export const pikkuMiddleware = <
|
|
353
|
+
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
354
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
355
|
+
>(
|
|
356
|
+
middleware:
|
|
357
|
+
| CorePikkuMiddleware<SingletonServices, UserSession>
|
|
358
|
+
| CorePikkuMiddlewareConfig<SingletonServices, UserSession>
|
|
359
|
+
): CorePikkuMiddleware<SingletonServices, UserSession> => {
|
|
360
|
+
return typeof middleware === 'function' ? middleware : middleware.func
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Factory function for creating middleware factories
|
|
365
|
+
* Use this when your middleware needs configuration/input parameters
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* export const logMiddleware = pikkuMiddlewareFactory<LogOptions>(({
|
|
370
|
+
* message,
|
|
371
|
+
* level = 'info'
|
|
372
|
+
* }) => {
|
|
373
|
+
* return pikkuMiddleware(async ({ logger }, next) => {
|
|
374
|
+
* logger[level](message)
|
|
375
|
+
* await next()
|
|
376
|
+
* })
|
|
377
|
+
* })
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
export const pikkuMiddlewareFactory = <In = any>(
|
|
381
|
+
factory: CorePikkuMiddlewareFactory<In>
|
|
382
|
+
): CorePikkuMiddlewareFactory<In> => {
|
|
383
|
+
return factory
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export const pikkuChannelMiddleware = <
|
|
387
|
+
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
388
|
+
Event = unknown,
|
|
389
|
+
>(
|
|
390
|
+
middleware: CorePikkuChannelMiddleware<SingletonServices, Event>
|
|
391
|
+
): CorePikkuChannelMiddleware<SingletonServices, Event> => {
|
|
392
|
+
return middleware
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export const pikkuChannelMiddlewareFactory = <In = any>(
|
|
396
|
+
factory: CorePikkuChannelMiddlewareFactory<In>
|
|
397
|
+
): CorePikkuChannelMiddlewareFactory<In> => {
|
|
398
|
+
return factory
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.js'
|
|
402
|
+
|
|
403
|
+
export const pikkuAIMiddleware = <
|
|
404
|
+
State extends Record<string, unknown> = Record<string, unknown>,
|
|
405
|
+
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
406
|
+
>(
|
|
407
|
+
hooks: PikkuAIMiddlewareHooks<State, SingletonServices>
|
|
408
|
+
): PikkuAIMiddlewareHooks<State, SingletonServices> => hooks
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Represents the core services used by Pikku, including singleton services.
|
|
412
|
+
*/
|
|
413
|
+
export type CoreServices<SingletonServices = CoreSingletonServices> =
|
|
414
|
+
SingletonServices
|
|
415
|
+
|
|
416
|
+
export type WireServices<
|
|
417
|
+
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
418
|
+
Services = CoreServices<SingletonServices>,
|
|
419
|
+
> = Omit<Services, keyof SingletonServices | 'session'>
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Defines a function type for creating singleton services from the given configuration.
|
|
423
|
+
*/
|
|
424
|
+
export type CreateSingletonServices<
|
|
425
|
+
Config extends CoreConfig,
|
|
426
|
+
SingletonServices extends CoreSingletonServices,
|
|
427
|
+
> = (
|
|
428
|
+
config: Config,
|
|
429
|
+
existingServices?: Partial<SingletonServices>
|
|
430
|
+
) => Promise<SingletonServices>
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Defines a function type for creating session-specific services.
|
|
434
|
+
*/
|
|
435
|
+
export type CreateWireServices<
|
|
436
|
+
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
437
|
+
Services extends
|
|
438
|
+
CoreServices<SingletonServices> = CoreServices<SingletonServices>,
|
|
439
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
440
|
+
> = (
|
|
441
|
+
services: SingletonServices,
|
|
442
|
+
wire: PikkuWire<unknown, unknown, false, UserSession>
|
|
443
|
+
) => Promise<WireServices<Services, SingletonServices>>
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Defines a function type for creating config.
|
|
447
|
+
*/
|
|
448
|
+
export type CreateConfig<
|
|
449
|
+
Config extends CoreConfig,
|
|
450
|
+
RemainingArgs extends any[] = unknown[],
|
|
451
|
+
> = (variables?: VariablesService, ...args: RemainingArgs) => Promise<Config>
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Represents the documentation for a route, including summary, description, tags, and errors.
|
|
455
|
+
*/
|
|
456
|
+
export type CommonWireMeta = {
|
|
457
|
+
pikkuFuncId: string
|
|
458
|
+
packageName?: string
|
|
459
|
+
|
|
460
|
+
title?: string
|
|
461
|
+
tags?: string[]
|
|
462
|
+
summary?: string
|
|
463
|
+
description?: string
|
|
464
|
+
errors?: string[]
|
|
465
|
+
|
|
466
|
+
middleware?: MiddlewareMetadata[]
|
|
467
|
+
permissions?: PermissionMetadata[]
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Serialized error for storage
|
|
472
|
+
*/
|
|
473
|
+
export interface SerializedError {
|
|
474
|
+
message: string
|
|
475
|
+
stack?: string
|
|
476
|
+
code?: string
|
|
477
|
+
[key: string]: any
|
|
478
|
+
}
|