@pikku/core 0.6.9
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 +339 -0
- package/README.md +5 -0
- package/dist/channel/channel-handler.d.ts +3 -0
- package/dist/channel/channel-handler.js +97 -0
- package/dist/channel/channel-runner.d.ts +26 -0
- package/dist/channel/channel-runner.js +86 -0
- package/dist/channel/channel-store.d.ts +14 -0
- package/dist/channel/channel-store.js +2 -0
- package/dist/channel/channel.types.d.ts +106 -0
- package/dist/channel/channel.types.js +1 -0
- package/dist/channel/eventhub-forwarder.d.ts +17 -0
- package/dist/channel/eventhub-forwarder.js +1 -0
- package/dist/channel/eventhub-service.d.ts +23 -0
- package/dist/channel/eventhub-service.js +1 -0
- package/dist/channel/eventhub-store.d.ts +5 -0
- package/dist/channel/eventhub-store.js +2 -0
- package/dist/channel/index.d.ts +9 -0
- package/dist/channel/index.js +9 -0
- package/dist/channel/local/index.d.ts +3 -0
- package/dist/channel/local/index.js +3 -0
- package/dist/channel/local/local-channel-handler.d.ts +17 -0
- package/dist/channel/local/local-channel-handler.js +41 -0
- package/dist/channel/local/local-channel-runner.d.ts +3 -0
- package/dist/channel/local/local-channel-runner.js +46 -0
- package/dist/channel/local/local-eventhub-service.d.ts +44 -0
- package/dist/channel/local/local-eventhub-service.js +81 -0
- package/dist/channel/log-channels.d.ts +6 -0
- package/dist/channel/log-channels.js +17 -0
- package/dist/channel/pikku-abstract-channel-handler.d.ts +15 -0
- package/dist/channel/pikku-abstract-channel-handler.js +33 -0
- package/dist/channel/serverless/index.d.ts +1 -0
- package/dist/channel/serverless/index.js +1 -0
- package/dist/channel/serverless/serverless-channel-runner.d.ts +10 -0
- package/dist/channel/serverless/serverless-channel-runner.js +98 -0
- package/dist/errors/error-handler.d.ts +39 -0
- package/dist/errors/error-handler.js +54 -0
- package/dist/errors/errors.d.ts +190 -0
- package/dist/errors/errors.js +315 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.js +2 -0
- package/dist/http/http-route-runner.d.ts +30 -0
- package/dist/http/http-route-runner.js +208 -0
- package/dist/http/http-routes.types.d.ts +127 -0
- package/dist/http/http-routes.types.js +1 -0
- package/dist/http/http-session-service.d.ts +15 -0
- package/dist/http/http-session-service.js +1 -0
- package/dist/http/index.d.ts +6 -0
- package/dist/http/index.js +5 -0
- package/dist/http/log-http-routes.d.ts +6 -0
- package/dist/http/log-http-routes.js +17 -0
- package/dist/http/pikku-http-abstract-request.d.ts +57 -0
- package/dist/http/pikku-http-abstract-request.js +69 -0
- package/dist/http/pikku-http-abstract-response.d.ts +58 -0
- package/dist/http/pikku-http-abstract-response.js +54 -0
- package/dist/http/pikku-http-session-service.d.ts +45 -0
- package/dist/http/pikku-http-session-service.js +92 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/permissions.d.ts +10 -0
- package/dist/permissions.js +32 -0
- package/dist/pikku-request.d.ts +14 -0
- package/dist/pikku-request.js +21 -0
- package/dist/pikku-response.d.ts +6 -0
- package/dist/pikku-response.js +6 -0
- package/dist/scheduler/index.d.ts +3 -0
- package/dist/scheduler/index.js +3 -0
- package/dist/scheduler/log-schedulers.d.ts +6 -0
- package/dist/scheduler/log-schedulers.js +17 -0
- package/dist/scheduler/scheduler-runner.d.ts +21 -0
- package/dist/scheduler/scheduler-runner.js +73 -0
- package/dist/scheduler/scheduler.types.d.ts +21 -0
- package/dist/scheduler/scheduler.types.js +1 -0
- package/dist/schema.d.ts +26 -0
- package/dist/schema.js +100 -0
- package/dist/services/content-service.d.ts +55 -0
- package/dist/services/content-service.js +1 -0
- package/dist/services/index.d.ts +12 -0
- package/dist/services/index.js +14 -0
- package/dist/services/jwt-service.d.ts +29 -0
- package/dist/services/jwt-service.js +1 -0
- package/dist/services/local-content.d.ts +33 -0
- package/dist/services/local-content.js +6 -0
- package/dist/services/local-secrets.d.ts +26 -0
- package/dist/services/local-secrets.js +39 -0
- package/dist/services/local-variables.d.ts +7 -0
- package/dist/services/local-variables.js +12 -0
- package/dist/services/logger-console.d.ts +52 -0
- package/dist/services/logger-console.js +82 -0
- package/dist/services/logger.d.ts +48 -0
- package/dist/services/logger.js +9 -0
- package/dist/services/schema-service.d.ts +24 -0
- package/dist/services/schema-service.js +1 -0
- package/dist/services/secret-service.d.ts +17 -0
- package/dist/services/secret-service.js +1 -0
- package/dist/services/user-session-service.d.ts +6 -0
- package/dist/services/user-session-service.js +1 -0
- package/dist/services/variables-service.d.ts +4 -0
- package/dist/services/variables-service.js +1 -0
- package/dist/types/core.types.d.ts +109 -0
- package/dist/types/core.types.js +1 -0
- package/dist/types/functions.types.d.ts +27 -0
- package/dist/types/functions.types.js +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +15 -0
- package/package.json +48 -0
- package/run-tests.sh +53 -0
- package/src/channel/channel-handler.ts +179 -0
- package/src/channel/channel-runner.ts +156 -0
- package/src/channel/channel-store.ts +32 -0
- package/src/channel/channel.types.ts +202 -0
- package/src/channel/eventhub-forwarder.ts +25 -0
- package/src/channel/eventhub-service.ts +30 -0
- package/src/channel/eventhub-store.ts +8 -0
- package/src/channel/index.ts +9 -0
- package/src/channel/local/index.ts +3 -0
- package/src/channel/local/local-channel-handler.ts +56 -0
- package/src/channel/local/local-channel-runner.test.ts +128 -0
- package/src/channel/local/local-channel-runner.ts +94 -0
- package/src/channel/local/local-eventhub-service.test.ts +95 -0
- package/src/channel/local/local-eventhub-service.ts +95 -0
- package/src/channel/log-channels.ts +20 -0
- package/src/channel/pikku-abstract-channel-handler.test.ts +77 -0
- package/src/channel/pikku-abstract-channel-handler.ts +44 -0
- package/src/channel/serverless/index.ts +1 -0
- package/src/channel/serverless/serverless-channel-runner.ts +190 -0
- package/src/errors/error-handler.ts +74 -0
- package/src/errors/error.test.ts +54 -0
- package/src/errors/errors.ts +340 -0
- package/src/errors/index.ts +2 -0
- package/src/http/http-route-runner.test.ts +193 -0
- package/src/http/http-route-runner.ts +365 -0
- package/src/http/http-routes.types.ts +189 -0
- package/src/http/http-session-service.ts +19 -0
- package/src/http/index.ts +16 -0
- package/src/http/log-http-routes.ts +20 -0
- package/src/http/pikku-http-abstract-request.ts +95 -0
- package/src/http/pikku-http-abstract-response.ts +79 -0
- package/src/http/pikku-http-session-service.test.ts +106 -0
- package/src/http/pikku-http-session-service.ts +135 -0
- package/src/index.ts +15 -0
- package/src/permissions.test.ts +58 -0
- package/src/permissions.ts +45 -0
- package/src/pikku-request.ts +19 -0
- package/src/pikku-response.ts +5 -0
- package/src/scheduler/index.ts +11 -0
- package/src/scheduler/log-schedulers.ts +20 -0
- package/src/scheduler/scheduler-runner.ts +132 -0
- package/src/scheduler/scheduler.types.ts +27 -0
- package/src/schema.test.ts +57 -0
- package/src/schema.ts +124 -0
- package/src/services/content-service.ts +69 -0
- package/src/services/index.ts +16 -0
- package/src/services/jwt-service.ts +36 -0
- package/src/services/local-content.ts +39 -0
- package/src/services/local-secrets.ts +43 -0
- package/src/services/local-variables.ts +17 -0
- package/src/services/logger-console.ts +97 -0
- package/src/services/logger.ts +57 -0
- package/src/services/schema-service.ts +26 -0
- package/src/services/secret-service.ts +17 -0
- package/src/services/user-session-service.ts +7 -0
- package/src/services/variables-service.ts +6 -0
- package/src/types/core.types.ts +162 -0
- package/src/types/functions.types.ts +44 -0
- package/src/utils.ts +21 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
package/run-tests.sh
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Enable nullglob to handle cases where no files match the pattern
|
|
4
|
+
shopt -s nullglob
|
|
5
|
+
|
|
6
|
+
# Initialize variables for options
|
|
7
|
+
watch_mode=false
|
|
8
|
+
coverage_mode=false
|
|
9
|
+
|
|
10
|
+
# Parse command-line options
|
|
11
|
+
while [[ $# -gt 0 ]]; do
|
|
12
|
+
case $1 in
|
|
13
|
+
--watch)
|
|
14
|
+
watch_mode=true
|
|
15
|
+
shift
|
|
16
|
+
;;
|
|
17
|
+
--coverage)
|
|
18
|
+
coverage_mode=true
|
|
19
|
+
shift
|
|
20
|
+
;;
|
|
21
|
+
*)
|
|
22
|
+
echo "Unknown option: $1"
|
|
23
|
+
exit 1
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
# Define the pattern to match your test files
|
|
29
|
+
pattern="src/*.test.ts"
|
|
30
|
+
|
|
31
|
+
# Expand the pattern into an array of files
|
|
32
|
+
files=($(find src -type f -name "*.test.ts"))
|
|
33
|
+
|
|
34
|
+
# Check if any files matched the pattern
|
|
35
|
+
if [ ${#files[@]} -eq 0 ]; then
|
|
36
|
+
echo "No test files found matching pattern: $pattern"
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Construct the node command
|
|
41
|
+
node_cmd="node --import tsx --test"
|
|
42
|
+
|
|
43
|
+
# Append options based on flags
|
|
44
|
+
if [ "$watch_mode" = true ]; then
|
|
45
|
+
node_cmd="$node_cmd --watch"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
if [ "$coverage_mode" = true ]; then
|
|
49
|
+
node_cmd="$node_cmd --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Execute the node command with the expanded list of files
|
|
53
|
+
$node_cmd "${files[@]}"
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CoreSingletonServices,
|
|
3
|
+
CoreServices,
|
|
4
|
+
CoreUserSession,
|
|
5
|
+
JSONValue,
|
|
6
|
+
} from '../types/core.types.js'
|
|
7
|
+
import { CoreAPIChannel, PikkuChannelHandler } from './channel.types.js'
|
|
8
|
+
import { verifyPermissions } from '../permissions.js'
|
|
9
|
+
import { getChannels } from './channel-runner.js'
|
|
10
|
+
|
|
11
|
+
const validateSchema = (
|
|
12
|
+
logger: CoreSingletonServices['logger'],
|
|
13
|
+
data: JSONValue,
|
|
14
|
+
channelName: string,
|
|
15
|
+
routingProperty?: string,
|
|
16
|
+
routerValue?: string
|
|
17
|
+
) => {
|
|
18
|
+
const { channelsMeta } = getChannels()
|
|
19
|
+
for (const channelMeta of channelsMeta) {
|
|
20
|
+
if (routingProperty && routerValue) {
|
|
21
|
+
const channelRoute =
|
|
22
|
+
channelMeta.messageRoutes[routingProperty]?.[routerValue]
|
|
23
|
+
if (channelRoute) {
|
|
24
|
+
const schemaNames = channelRoute.inputs
|
|
25
|
+
if (schemaNames) {
|
|
26
|
+
// TODO
|
|
27
|
+
// loadSchema(schemaNames, logger)
|
|
28
|
+
// validateJson(schemaNames, data)
|
|
29
|
+
}
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const validateAuth = (
|
|
37
|
+
requiresSession: boolean,
|
|
38
|
+
channelHandler: PikkuChannelHandler,
|
|
39
|
+
onMessage: any
|
|
40
|
+
) => {
|
|
41
|
+
const auth =
|
|
42
|
+
typeof onMessage === 'function'
|
|
43
|
+
? requiresSession
|
|
44
|
+
: onMessage.auth === undefined
|
|
45
|
+
? requiresSession
|
|
46
|
+
: onMessage.auth
|
|
47
|
+
|
|
48
|
+
if (auth && !channelHandler.getChannel().userSession) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const validatePermissions = async (
|
|
55
|
+
services: CoreServices,
|
|
56
|
+
channelHandler: PikkuChannelHandler,
|
|
57
|
+
onMessage: any,
|
|
58
|
+
data: unknown
|
|
59
|
+
) => {
|
|
60
|
+
const permissions =
|
|
61
|
+
typeof onMessage === 'function' ? {} : onMessage.permissions
|
|
62
|
+
return await verifyPermissions(
|
|
63
|
+
permissions,
|
|
64
|
+
services,
|
|
65
|
+
data,
|
|
66
|
+
channelHandler.getChannel().userSession
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const runFunction = async (
|
|
71
|
+
services: CoreServices,
|
|
72
|
+
channelHandler: PikkuChannelHandler,
|
|
73
|
+
onMessage: any,
|
|
74
|
+
data: unknown
|
|
75
|
+
) => {
|
|
76
|
+
const func: any = typeof onMessage === 'function' ? onMessage : onMessage.func
|
|
77
|
+
return await func(services, channelHandler.getChannel(), data)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const processMessageHandlers = (
|
|
81
|
+
services: CoreServices,
|
|
82
|
+
channelConfig: CoreAPIChannel<any, any>,
|
|
83
|
+
channelHandler: PikkuChannelHandler<CoreUserSession, unknown>
|
|
84
|
+
) => {
|
|
85
|
+
const logger = services.logger
|
|
86
|
+
const requiresSession = channelConfig.auth !== false
|
|
87
|
+
|
|
88
|
+
const processMessage = async (
|
|
89
|
+
data: JSONValue,
|
|
90
|
+
onMessage: any,
|
|
91
|
+
routingProperty?: string,
|
|
92
|
+
routerValue?: string
|
|
93
|
+
): Promise<unknown> => {
|
|
94
|
+
if (!validateAuth(requiresSession, channelHandler, onMessage)) {
|
|
95
|
+
const routeMessage = routingProperty
|
|
96
|
+
? `route '${routingProperty}:${routerValue}'`
|
|
97
|
+
: 'the default message route'
|
|
98
|
+
logger.error(
|
|
99
|
+
`Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for ${routeMessage}`
|
|
100
|
+
)
|
|
101
|
+
// TODO: Send error message back breaks typescript, but should be implemented somehow
|
|
102
|
+
channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
validateSchema(
|
|
107
|
+
services.logger,
|
|
108
|
+
data,
|
|
109
|
+
channelConfig.name,
|
|
110
|
+
routingProperty,
|
|
111
|
+
routerValue
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const hasPermission = await validatePermissions(
|
|
115
|
+
services,
|
|
116
|
+
channelHandler,
|
|
117
|
+
onMessage,
|
|
118
|
+
data
|
|
119
|
+
)
|
|
120
|
+
if (!hasPermission) {
|
|
121
|
+
logger.error(
|
|
122
|
+
`Channel ${channelConfig.name} requires permissions for ${routingProperty || 'default message route'}`
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return await runFunction(services, channelHandler, onMessage, data)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const onMessage = async (rawData): Promise<unknown> => {
|
|
130
|
+
let result: unknown
|
|
131
|
+
let processed = false
|
|
132
|
+
|
|
133
|
+
// Route-specific handling
|
|
134
|
+
if (typeof rawData === 'string' && channelConfig.onMessageRoute) {
|
|
135
|
+
try {
|
|
136
|
+
const messageData = JSON.parse(rawData)
|
|
137
|
+
const entries = Object.entries(channelConfig.onMessageRoute)
|
|
138
|
+
for (const [routingProperty, routes] of entries) {
|
|
139
|
+
const routerValue = messageData[routingProperty]
|
|
140
|
+
if (routerValue && routes[routerValue]) {
|
|
141
|
+
processed = true
|
|
142
|
+
result = await processMessage(
|
|
143
|
+
messageData,
|
|
144
|
+
routes[routerValue],
|
|
145
|
+
routingProperty,
|
|
146
|
+
routerValue
|
|
147
|
+
)
|
|
148
|
+
break
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Default handler if no routes matched but json data was parsed
|
|
153
|
+
if (!processed && channelConfig.onMessage) {
|
|
154
|
+
processed = true
|
|
155
|
+
result = await processMessage(messageData, channelConfig.onMessage)
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// Most likely a json error.. ignore
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Default handler if no routes matched and json data wasn't parsed
|
|
163
|
+
if (!processed && channelConfig.onMessage) {
|
|
164
|
+
processed = true
|
|
165
|
+
result = await processMessage(rawData, channelConfig.onMessage)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!processed) {
|
|
169
|
+
logger.error(
|
|
170
|
+
`No handler found for message in channel ${channelConfig.name} for ${rawData}`
|
|
171
|
+
)
|
|
172
|
+
logger.error(`Channel ${channelConfig}`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return onMessage
|
|
179
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { NotFoundError } from '../errors/errors.js'
|
|
2
|
+
import { loadUserSession } from '../http/http-route-runner.js'
|
|
3
|
+
import { validateAndCoerce } from '../schema.js'
|
|
4
|
+
import { CoreUserSession } from '../types/core.types.js'
|
|
5
|
+
import {
|
|
6
|
+
CoreAPIChannel,
|
|
7
|
+
ChannelsMeta,
|
|
8
|
+
CoreAPIChannels,
|
|
9
|
+
RunChannelOptions,
|
|
10
|
+
RunChannelParams,
|
|
11
|
+
} from './channel.types.js'
|
|
12
|
+
import { match } from 'path-to-regexp'
|
|
13
|
+
|
|
14
|
+
if (!globalThis.pikku?.channels) {
|
|
15
|
+
globalThis.pikku = globalThis.pikku || {}
|
|
16
|
+
globalThis.pikku.channels = []
|
|
17
|
+
globalThis.pikku.channelsMeta = []
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const channels = (data?: any): CoreAPIChannels => {
|
|
21
|
+
if (data) {
|
|
22
|
+
globalThis.pikku.channels = data
|
|
23
|
+
}
|
|
24
|
+
return globalThis.pikku.channels
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const channelsMeta = (data?: any): ChannelsMeta => {
|
|
28
|
+
if (data) {
|
|
29
|
+
globalThis.pikku.channelsMeta = data
|
|
30
|
+
}
|
|
31
|
+
return globalThis.pikku.channelsMeta
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns all the registered routes and associated metadata.
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export const getChannels = () => {
|
|
39
|
+
return {
|
|
40
|
+
channels: channels(),
|
|
41
|
+
channelsMeta: channelsMeta(),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const addChannel = <
|
|
46
|
+
In,
|
|
47
|
+
Channel extends string,
|
|
48
|
+
ChannelFunction,
|
|
49
|
+
ChannelFunctionSessionless,
|
|
50
|
+
APIPermission,
|
|
51
|
+
>(
|
|
52
|
+
channel: CoreAPIChannel<
|
|
53
|
+
In,
|
|
54
|
+
Channel,
|
|
55
|
+
ChannelFunction,
|
|
56
|
+
ChannelFunctionSessionless,
|
|
57
|
+
APIPermission
|
|
58
|
+
>
|
|
59
|
+
) => {
|
|
60
|
+
channels().push(channel as any)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @ignore
|
|
65
|
+
*/
|
|
66
|
+
export const setChannelsMeta = (_channelsMeta: ChannelsMeta) => {
|
|
67
|
+
channelsMeta(_channelsMeta)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const getMatchingChannelConfig = (request: string) => {
|
|
71
|
+
const { channels, channelsMeta } = getChannels()
|
|
72
|
+
for (const channelConfig of channels) {
|
|
73
|
+
const cleanedRoute = channelConfig.route.replace(/^\/\//, '/')
|
|
74
|
+
const cleanedRequest = request.replace(/^\/\//, '/')
|
|
75
|
+
const matchFunc = match(cleanedRoute, {
|
|
76
|
+
decode: decodeURIComponent,
|
|
77
|
+
})
|
|
78
|
+
const matchedPath = matchFunc(cleanedRequest)
|
|
79
|
+
if (matchedPath) {
|
|
80
|
+
const schemaName = channelsMeta.find(
|
|
81
|
+
(channelMeta) => channelMeta.route === channelConfig.route
|
|
82
|
+
)?.input
|
|
83
|
+
return {
|
|
84
|
+
matchedPath,
|
|
85
|
+
params: matchedPath.params,
|
|
86
|
+
channelConfig,
|
|
87
|
+
schemaName,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const openChannel = async ({
|
|
96
|
+
route,
|
|
97
|
+
singletonServices,
|
|
98
|
+
skipUserSession = false,
|
|
99
|
+
coerceToArray = false,
|
|
100
|
+
http,
|
|
101
|
+
}: Pick<CoreAPIChannel<unknown, string>, 'route'> &
|
|
102
|
+
Omit<RunChannelParams<unknown>, 'response' | 'request'> &
|
|
103
|
+
RunChannelOptions): Promise<{
|
|
104
|
+
userSession?: CoreUserSession
|
|
105
|
+
openingData: unknown
|
|
106
|
+
channelConfig: CoreAPIChannel<unknown, any>
|
|
107
|
+
}> => {
|
|
108
|
+
const matchingChannel = getMatchingChannelConfig(route)
|
|
109
|
+
if (!matchingChannel) {
|
|
110
|
+
singletonServices.logger.info(`Channel not found: ${route}`)
|
|
111
|
+
throw new NotFoundError(`Channel not found: ${route}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { matchedPath, params, channelConfig, schemaName } = matchingChannel
|
|
115
|
+
|
|
116
|
+
const requiresSession = channelConfig.auth !== false
|
|
117
|
+
http?.request?.setParams(params)
|
|
118
|
+
|
|
119
|
+
singletonServices.logger.info(
|
|
120
|
+
`Matched channel: ${channelConfig.name} | route: ${channelConfig.route} | auth: ${requiresSession.toString()}`
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const userSession = await loadUserSession(
|
|
124
|
+
skipUserSession,
|
|
125
|
+
// We may require a session, but we don't actually need it
|
|
126
|
+
// on connect since channels can authenticate later given
|
|
127
|
+
// how websocket sessions work (cookie or queryParam based)
|
|
128
|
+
false,
|
|
129
|
+
http,
|
|
130
|
+
matchedPath,
|
|
131
|
+
channelConfig,
|
|
132
|
+
singletonServices.logger,
|
|
133
|
+
singletonServices.httpSessionService
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if (singletonServices.enforceChannelAccess) {
|
|
137
|
+
await singletonServices.enforceChannelAccess(
|
|
138
|
+
matchingChannel.channelConfig,
|
|
139
|
+
userSession
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let openingData: any | undefined
|
|
144
|
+
if (http?.request) {
|
|
145
|
+
openingData = await http.request.getData()
|
|
146
|
+
validateAndCoerce(
|
|
147
|
+
singletonServices.logger,
|
|
148
|
+
singletonServices.schemaService,
|
|
149
|
+
schemaName,
|
|
150
|
+
openingData,
|
|
151
|
+
coerceToArray
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { userSession, openingData, channelConfig }
|
|
156
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CoreUserSession } from '../types/core.types.js'
|
|
2
|
+
|
|
3
|
+
export type Channel<
|
|
4
|
+
ChannelType = unknown,
|
|
5
|
+
OpeningData = unknown,
|
|
6
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
7
|
+
> = {
|
|
8
|
+
channelId: string
|
|
9
|
+
channelName: string
|
|
10
|
+
channelObject?: ChannelType
|
|
11
|
+
openingData?: OpeningData
|
|
12
|
+
userSession?: UserSession
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export abstract class ChannelStore<
|
|
16
|
+
ChannelType = unknown,
|
|
17
|
+
OpeningData = unknown,
|
|
18
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
19
|
+
TypedChannel = Channel<ChannelType, OpeningData, UserSession>,
|
|
20
|
+
> {
|
|
21
|
+
public abstract addChannel(
|
|
22
|
+
channel: Channel<ChannelType, OpeningData, UserSession>
|
|
23
|
+
): Promise<void> | void
|
|
24
|
+
public abstract removeChannels(channelId: string[]): Promise<void> | void
|
|
25
|
+
public abstract setUserSession(
|
|
26
|
+
channelId: string,
|
|
27
|
+
userSession: any
|
|
28
|
+
): Promise<void> | void
|
|
29
|
+
public abstract getChannel(
|
|
30
|
+
channelId: string
|
|
31
|
+
): Promise<TypedChannel> | TypedChannel
|
|
32
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { EError } from '../errors/error-handler.js'
|
|
2
|
+
import {
|
|
3
|
+
HTTPFunctionMetaInputTypes,
|
|
4
|
+
PikkuHTTP,
|
|
5
|
+
} from '../http/http-routes.types.js'
|
|
6
|
+
import { PikkuHTTPAbstractRequest } from '../http/pikku-http-abstract-request.js'
|
|
7
|
+
import { PikkuHTTPAbstractResponse } from '../http/pikku-http-abstract-response.js'
|
|
8
|
+
import {
|
|
9
|
+
APIDocs,
|
|
10
|
+
CoreServices,
|
|
11
|
+
CoreSingletonServices,
|
|
12
|
+
CoreUserSession,
|
|
13
|
+
CreateSessionServices,
|
|
14
|
+
MakeRequired,
|
|
15
|
+
} from '../types/core.types.js'
|
|
16
|
+
import { CoreAPIPermission } from '../types/functions.types.js'
|
|
17
|
+
import { PikkuRequest } from '../pikku-request.js'
|
|
18
|
+
import { PikkuResponse } from '../pikku-response.js'
|
|
19
|
+
|
|
20
|
+
export type RunChannelOptions = Partial<{
|
|
21
|
+
skipUserSession: boolean
|
|
22
|
+
respondWith404: boolean
|
|
23
|
+
coerceToArray: boolean
|
|
24
|
+
logWarningsForStatusCodes: number[]
|
|
25
|
+
bubbleErrors: boolean
|
|
26
|
+
}>
|
|
27
|
+
|
|
28
|
+
export type RunChannelParams<ChannelData> = {
|
|
29
|
+
channelId: string
|
|
30
|
+
singletonServices: MakeRequired<CoreSingletonServices, 'eventHub'>
|
|
31
|
+
request?: PikkuRequest<ChannelData> | PikkuHTTPAbstractRequest<ChannelData>
|
|
32
|
+
response?: PikkuResponse | PikkuHTTPAbstractResponse
|
|
33
|
+
http?: PikkuHTTP
|
|
34
|
+
createSessionServices?: CreateSessionServices
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface HandlerMeta {}
|
|
38
|
+
|
|
39
|
+
export interface ChannelMeta {
|
|
40
|
+
name: string
|
|
41
|
+
route: string
|
|
42
|
+
params?: string[]
|
|
43
|
+
query?: string[]
|
|
44
|
+
input: string | null
|
|
45
|
+
inputTypes?: HTTPFunctionMetaInputTypes
|
|
46
|
+
connect: boolean
|
|
47
|
+
disconnect: boolean
|
|
48
|
+
message: { inputs: string[] | null; outputs: string[] | null } | null
|
|
49
|
+
messageRoutes: Record<
|
|
50
|
+
string,
|
|
51
|
+
Record<
|
|
52
|
+
string,
|
|
53
|
+
{
|
|
54
|
+
inputs: string[] | null
|
|
55
|
+
outputs: string[] | null
|
|
56
|
+
}
|
|
57
|
+
>
|
|
58
|
+
>
|
|
59
|
+
docs?: APIDocs
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type ChannelsMeta = ChannelMeta[]
|
|
63
|
+
|
|
64
|
+
export type CoreChannelConnection<
|
|
65
|
+
ChannelData,
|
|
66
|
+
Out = unknown,
|
|
67
|
+
Services extends CoreServices = CoreServices,
|
|
68
|
+
Session extends CoreUserSession = CoreUserSession,
|
|
69
|
+
> = (
|
|
70
|
+
services: MakeRequired<Services, 'eventHub'>,
|
|
71
|
+
channel: PikkuChannel<Session, ChannelData, Out>
|
|
72
|
+
) => Promise<void>
|
|
73
|
+
|
|
74
|
+
export type CoreChannelDisconnection<
|
|
75
|
+
ChannelData,
|
|
76
|
+
Services extends CoreServices = CoreServices,
|
|
77
|
+
Session extends CoreUserSession = CoreUserSession,
|
|
78
|
+
> = (
|
|
79
|
+
services: MakeRequired<Services, 'eventHub'>,
|
|
80
|
+
channel: PikkuChannel<Session, ChannelData, never>
|
|
81
|
+
) => Promise<void>
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Represents a core channel function that performs an operation using core services and a user session.
|
|
85
|
+
*
|
|
86
|
+
* @template In - The input type.
|
|
87
|
+
* @template Services - The services type, defaults to `CoreServices`.
|
|
88
|
+
* @template Session - The session type, defaults to `CoreUserSession`.
|
|
89
|
+
*/
|
|
90
|
+
export type CoreChannelMessage<
|
|
91
|
+
In,
|
|
92
|
+
Out,
|
|
93
|
+
ChannelData,
|
|
94
|
+
Services extends CoreServices = CoreServices,
|
|
95
|
+
Session extends CoreUserSession = CoreUserSession,
|
|
96
|
+
> = (
|
|
97
|
+
services: MakeRequired<Services, 'eventHub'>,
|
|
98
|
+
channel: PikkuChannel<Session, ChannelData, Out>,
|
|
99
|
+
data: In
|
|
100
|
+
) => Promise<void | Out>
|
|
101
|
+
|
|
102
|
+
export type CoreAPIChannelMessage<
|
|
103
|
+
ChannelFunctionMessage = CoreChannelMessage<unknown, unknown, unknown>,
|
|
104
|
+
> = {
|
|
105
|
+
func: ChannelFunctionMessage
|
|
106
|
+
route: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type CoreAPIChannel<
|
|
110
|
+
ChannelData,
|
|
111
|
+
Channel extends string,
|
|
112
|
+
ChannelFunctionConnection = CoreChannelConnection<ChannelData>,
|
|
113
|
+
ChannelFunctionDisconnection = CoreChannelConnection<ChannelData>,
|
|
114
|
+
ChannelFunctionDefaultMessage = CoreChannelMessage<unknown, unknown, unknown>,
|
|
115
|
+
ChannelFunctionMessageRoute = CoreChannelMessage<unknown, unknown, unknown>,
|
|
116
|
+
APIPermission = CoreAPIPermission<ChannelData>,
|
|
117
|
+
> = {
|
|
118
|
+
name: string
|
|
119
|
+
route: Channel
|
|
120
|
+
onConnect?: ChannelFunctionConnection
|
|
121
|
+
onDisconnect?: ChannelFunctionDisconnection
|
|
122
|
+
onMessage?:
|
|
123
|
+
| {
|
|
124
|
+
func: ChannelFunctionDefaultMessage
|
|
125
|
+
permissions?: Record<string, APIPermission[] | APIPermission>
|
|
126
|
+
auth?: boolean
|
|
127
|
+
}
|
|
128
|
+
| ChannelFunctionDefaultMessage
|
|
129
|
+
onMessageRoute?: Record<
|
|
130
|
+
string,
|
|
131
|
+
Record<
|
|
132
|
+
string,
|
|
133
|
+
| ChannelFunctionMessageRoute
|
|
134
|
+
| {
|
|
135
|
+
func: ChannelFunctionMessageRoute
|
|
136
|
+
permissions?: Record<string, APIPermission[] | APIPermission>
|
|
137
|
+
auth?: boolean
|
|
138
|
+
}
|
|
139
|
+
>
|
|
140
|
+
>
|
|
141
|
+
permissions?: Record<string, APIPermission[] | APIPermission>
|
|
142
|
+
auth?: boolean
|
|
143
|
+
docs?: Partial<{
|
|
144
|
+
description: string
|
|
145
|
+
response: string
|
|
146
|
+
errors: Array<typeof EError>
|
|
147
|
+
tags: string[]
|
|
148
|
+
}>
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type CoreAPIChannels = CoreAPIChannel<any, string>[]
|
|
152
|
+
|
|
153
|
+
export interface PikkuChannel<UserSession, OpeningData, Out> {
|
|
154
|
+
// The channel identifier
|
|
155
|
+
channelId: string
|
|
156
|
+
// The user session, if available
|
|
157
|
+
userSession?: UserSession
|
|
158
|
+
// Update the user session, useful if you deal with auth on the
|
|
159
|
+
// stream side
|
|
160
|
+
setUserSession: (userSession: UserSession) => Promise<void> | void
|
|
161
|
+
// The data the channel was created with. This could be query parameters
|
|
162
|
+
// or parameters in the url.
|
|
163
|
+
openingData: OpeningData
|
|
164
|
+
// The data to send. This will fail is the stream has been closed.
|
|
165
|
+
send: (data: Out, isBinary?: boolean) => Promise<void> | void
|
|
166
|
+
// This will close the channel.
|
|
167
|
+
close: () => Promise<void> | void
|
|
168
|
+
// The current state of the channel
|
|
169
|
+
state: 'initial' | 'open' | 'closed'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface PikkuChannelHandler<
|
|
173
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
174
|
+
OpeningData = unknown,
|
|
175
|
+
Out = unknown,
|
|
176
|
+
> {
|
|
177
|
+
setUserSession(session: UserSession): Promise<void> | void
|
|
178
|
+
send(message: Out, isBinary?: boolean): Promise<void> | void
|
|
179
|
+
getChannel(): PikkuChannel<UserSession, OpeningData, Out>
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type PikkuChannelHandlerFactory<
|
|
183
|
+
OpeningData = unknown,
|
|
184
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
185
|
+
Out = unknown,
|
|
186
|
+
> = (
|
|
187
|
+
channelId: string,
|
|
188
|
+
channelName: string,
|
|
189
|
+
openingData: OpeningData,
|
|
190
|
+
userSession: UserSession | undefined
|
|
191
|
+
) => PikkuChannelHandler<UserSession, OpeningData, Out>
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Enfore access to a channel.
|
|
195
|
+
* @param route - The channel to verify access for.
|
|
196
|
+
* @param session - The user session.
|
|
197
|
+
* @returns A promise that resolves if access is granted.
|
|
198
|
+
*/
|
|
199
|
+
export type enforceChannelAccess = (
|
|
200
|
+
channel: CoreAPIChannel<unknown, any>,
|
|
201
|
+
session?: CoreUserSession
|
|
202
|
+
) => Promise<void> | void
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface defining the methods of a EventHub Service.
|
|
3
|
+
*/
|
|
4
|
+
export interface EventHubForwarder {
|
|
5
|
+
/**
|
|
6
|
+
* Sends data to all connections subscribed to a topic.
|
|
7
|
+
* @param topic - The topic to send data to.
|
|
8
|
+
* @param data - The data to send to the subscribers.
|
|
9
|
+
*/
|
|
10
|
+
forwardPublish(
|
|
11
|
+
topic: string,
|
|
12
|
+
channelId: string,
|
|
13
|
+
data: unknown,
|
|
14
|
+
isBinary?: boolean
|
|
15
|
+
): Promise<void>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sends data to all connections subscribed to a topic.
|
|
19
|
+
* @param topic - The topic to send data to.
|
|
20
|
+
* @param data - The data to send to the subscribers.
|
|
21
|
+
*/
|
|
22
|
+
onForwardedPublishMessage(
|
|
23
|
+
callback: (topic: string, data: unknown, isBinary?: boolean) => void
|
|
24
|
+
): void
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface defining the methods of a EventHub Service.
|
|
3
|
+
*/
|
|
4
|
+
export interface EventHubService<Out = unknown> {
|
|
5
|
+
/**
|
|
6
|
+
* Subscribes a connection to a specific topic.
|
|
7
|
+
* @param topic - The topic to subscribe to.
|
|
8
|
+
* @param channelId - The unique ID of the connection to subscribe.
|
|
9
|
+
*/
|
|
10
|
+
subscribe(topic: string, channelId: string): Promise<void> | void
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Unsubscribes a connection from a specific topic.
|
|
14
|
+
* @param topic - The topic to unsubscribe from.
|
|
15
|
+
* @param channelId - The unique ID of the connection to unsubscribe.
|
|
16
|
+
*/
|
|
17
|
+
unsubscribe(topic: string, channelId: string): Promise<void> | void
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sends data to all connections subscribed to a topic.
|
|
21
|
+
* @param topic - The topic to send data to.
|
|
22
|
+
* @param data - The data to send to the subscribers.
|
|
23
|
+
*/
|
|
24
|
+
publish(
|
|
25
|
+
topic: string,
|
|
26
|
+
channelId: string | null,
|
|
27
|
+
data: Out,
|
|
28
|
+
isBinary?: boolean
|
|
29
|
+
): Promise<void> | void
|
|
30
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export abstract class EventHubStore {
|
|
2
|
+
public abstract getChannelIdsForTopic(topic: string): Promise<string[]>
|
|
3
|
+
public abstract subscribe(topic: string, channelId: string): Promise<boolean>
|
|
4
|
+
public abstract unsubscribe(
|
|
5
|
+
topic: string,
|
|
6
|
+
channelId: string
|
|
7
|
+
): Promise<boolean>
|
|
8
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './channel-runner.js'
|
|
2
|
+
export * from './channel.types.js'
|
|
3
|
+
export * from './log-channels.js'
|
|
4
|
+
export * from './pikku-abstract-channel-handler.js'
|
|
5
|
+
export * from './eventhub-service.js'
|
|
6
|
+
export * from './eventhub-forwarder.js'
|
|
7
|
+
export * from './eventhub-service.js'
|
|
8
|
+
export * from './channel-store.js'
|
|
9
|
+
export * from './eventhub-store.js'
|