@pikku/core 0.7.0 → 0.7.1
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 +6 -0
- package/dist/channel/channel-runner.js +1 -1
- package/dist/channel/serverless/serverless-channel-runner.js +1 -1
- package/dist/http/http-runner.js +4 -2
- package/dist/http/log-http-routes.js +1 -1
- package/lcov.info +4585 -0
- package/package.json +1 -1
- package/src/channel/channel-handler.ts +165 -0
- package/src/channel/channel-runner.ts +153 -0
- package/src/channel/channel-store.ts +29 -0
- package/src/channel/channel.types.ts +133 -0
- package/src/channel/eventhub-service.ts +30 -0
- package/src/channel/eventhub-store.ts +8 -0
- package/src/channel/index.ts +8 -0
- package/src/channel/local/index.ts +3 -0
- package/src/channel/local/local-channel-handler.ts +50 -0
- package/src/channel/local/local-channel-runner.test.ts +157 -0
- package/src/channel/local/local-channel-runner.ts +139 -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 +52 -0
- package/src/channel/pikku-abstract-channel-handler.ts +38 -0
- package/src/channel/serverless/index.ts +1 -0
- package/src/channel/serverless/serverless-channel-runner.ts +220 -0
- package/src/errors/error-handler.ts +61 -0
- package/src/errors/error.test.ts +54 -0
- package/src/errors/errors.ts +339 -0
- package/src/errors/index.ts +2 -0
- package/src/function/function-runner.ts +91 -0
- package/src/function/functions.types.ts +77 -0
- package/src/function/index.ts +2 -0
- package/src/handle-error.ts +73 -0
- package/src/http/http-runner.test.ts +140 -0
- package/src/http/http-runner.ts +493 -0
- package/src/http/http.types.ts +250 -0
- package/src/http/incomingmessage-to-request-convertor.ts +0 -0
- package/src/http/index.ts +8 -0
- package/src/http/log-http-routes.ts +22 -0
- package/src/http/pikku-fetch-http-request.test.ts +237 -0
- package/src/http/pikku-fetch-http-request.ts +163 -0
- package/src/http/pikku-fetch-http-response.test.ts +82 -0
- package/src/http/pikku-fetch-http-response.ts +138 -0
- package/src/index.ts +20 -0
- package/src/middleware/auth-apikey.ts +66 -0
- package/src/middleware/auth-bearer.ts +66 -0
- package/src/middleware/auth-cookie.ts +103 -0
- package/src/middleware/index.ts +3 -0
- package/src/middleware/timeout.ts +13 -0
- package/src/middleware-runner.ts +43 -0
- package/src/permissions.test.ts +58 -0
- package/src/permissions.ts +43 -0
- package/src/pikku-function.ts +1 -0
- package/src/pikku-request.ts +23 -0
- package/src/pikku-response.ts +5 -0
- package/src/pikku-state.ts +87 -0
- package/src/scheduler/index.ts +5 -0
- package/src/scheduler/log-schedulers.ts +20 -0
- package/src/scheduler/scheduler-runner.ts +105 -0
- package/src/scheduler/scheduler.types.ts +33 -0
- package/src/schema.test.ts +57 -0
- package/src/schema.ts +99 -0
- package/src/services/content-service.ts +68 -0
- package/src/services/index.ts +18 -0
- package/src/services/jwt-service.ts +30 -0
- package/src/services/local-content.ts +105 -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 +53 -0
- package/src/services/variables-service.ts +6 -0
- package/src/time-utils.test.ts +56 -0
- package/src/time-utils.ts +32 -0
- package/src/types/core.types.ts +178 -0
- package/src/utils.ts +40 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/http/http-route-runner.d.ts +0 -90
- package/dist/http/http-route-runner.js +0 -325
- package/dist/http/http-routes.types.d.ts +0 -179
- package/dist/http/http-routes.types.js +0 -1
- package/dist/http/pikku-http-request.d.ts +0 -56
- package/dist/http/pikku-http-request.js +0 -95
- package/dist/http/pikku-http-response.d.ts +0 -13
- package/dist/http/pikku-http-response.js +0 -60
- package/dist/parse-relative-time-offset.d.ts +0 -12
- package/dist/parse-relative-time-offset.js +0 -20
- package/dist/pikku-func.d.ts +0 -12
- package/dist/pikku-func.js +0 -39
- package/dist/types/functions.types.d.ts +0 -37
- package/dist/types/functions.types.js +0 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ForbiddenError } from '../errors/errors.js'
|
|
2
|
+
import { verifyPermissions } from '../permissions.js'
|
|
3
|
+
import { pikkuState } from '../pikku-state.js'
|
|
4
|
+
import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js'
|
|
5
|
+
import {
|
|
6
|
+
CoreServices,
|
|
7
|
+
CoreSingletonServices,
|
|
8
|
+
CoreUserSession,
|
|
9
|
+
} from '../types/core.types.js'
|
|
10
|
+
import {
|
|
11
|
+
CoreAPIFunction,
|
|
12
|
+
CoreAPIFunctionSessionless,
|
|
13
|
+
CorePermissionGroup,
|
|
14
|
+
} from './functions.types.js'
|
|
15
|
+
|
|
16
|
+
export const addFunction = (
|
|
17
|
+
funcName: string,
|
|
18
|
+
func: CoreAPIFunction<any, any> | CoreAPIFunctionSessionless<any, any>
|
|
19
|
+
) => {
|
|
20
|
+
pikkuState('functions', 'nameToFunction').set(funcName, func)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const runPikkuFuncDirectly = async <In, Out>(
|
|
24
|
+
funcName: string,
|
|
25
|
+
allServices: CoreServices,
|
|
26
|
+
data: In,
|
|
27
|
+
session?: CoreUserSession
|
|
28
|
+
) => {
|
|
29
|
+
const func = pikkuState('functions', 'nameToFunction').get(funcName)
|
|
30
|
+
if (!func) {
|
|
31
|
+
throw new Error(`Function not found: ${funcName}`)
|
|
32
|
+
}
|
|
33
|
+
return (await func(allServices, data, session!)) as Out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const runPikkuFunc = async <In = any, Out = any>(
|
|
37
|
+
funcName: string,
|
|
38
|
+
{
|
|
39
|
+
singletonServices,
|
|
40
|
+
getAllServices,
|
|
41
|
+
data,
|
|
42
|
+
session,
|
|
43
|
+
permissions,
|
|
44
|
+
coerceDataFromSchema,
|
|
45
|
+
}: {
|
|
46
|
+
singletonServices: CoreSingletonServices
|
|
47
|
+
getAllServices: () => Promise<CoreServices> | CoreServices
|
|
48
|
+
data: In
|
|
49
|
+
session?: CoreUserSession
|
|
50
|
+
permissions?: CorePermissionGroup
|
|
51
|
+
coerceDataFromSchema?: boolean
|
|
52
|
+
}
|
|
53
|
+
): Promise<Out> => {
|
|
54
|
+
const func = pikkuState('functions', 'nameToFunction').get(funcName)
|
|
55
|
+
if (!func) {
|
|
56
|
+
throw new Error(`Function not found: ${funcName}`)
|
|
57
|
+
}
|
|
58
|
+
const funcMeta = pikkuState('functions', 'meta')[funcName]
|
|
59
|
+
if (!funcMeta) {
|
|
60
|
+
throw new Error(`Function meta not found: ${funcName}`)
|
|
61
|
+
}
|
|
62
|
+
const schemaName = funcMeta.schemaName
|
|
63
|
+
// Validate request data against the defined schema, if any
|
|
64
|
+
await validateSchema(
|
|
65
|
+
singletonServices.logger,
|
|
66
|
+
singletonServices.schema,
|
|
67
|
+
schemaName,
|
|
68
|
+
data
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// Coerce (top level) query string parameters or date objects if specified by the schema
|
|
72
|
+
if (coerceDataFromSchema && schemaName) {
|
|
73
|
+
coerceTopLevelDataFromSchema(schemaName, data)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const allServices = await getAllServices()
|
|
77
|
+
|
|
78
|
+
// Execute permission checks
|
|
79
|
+
const permissioned = await verifyPermissions(
|
|
80
|
+
permissions,
|
|
81
|
+
allServices,
|
|
82
|
+
data,
|
|
83
|
+
session
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if (permissioned === false) {
|
|
87
|
+
throw new ForbiddenError('Permission denied')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (await func(allServices, data, session!)) as Out
|
|
91
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { PikkuChannel } from '../channel/channel.types.js'
|
|
2
|
+
import type {
|
|
3
|
+
CoreServices,
|
|
4
|
+
CoreSingletonServices,
|
|
5
|
+
CoreUserSession,
|
|
6
|
+
} from '../types/core.types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents a core API function that performs an operation using core services and a user session.
|
|
10
|
+
*
|
|
11
|
+
* @template In - The input type.
|
|
12
|
+
* @template Out - The output type.
|
|
13
|
+
* @template Services - The services type, defaults to `CoreServices`.
|
|
14
|
+
* @template Session - The session type, defaults to `CoreUserSession`.
|
|
15
|
+
*/
|
|
16
|
+
export type CoreAPIFunction<
|
|
17
|
+
In,
|
|
18
|
+
Out,
|
|
19
|
+
ChannelData extends unknown | null = null,
|
|
20
|
+
Services extends CoreSingletonServices = CoreServices &
|
|
21
|
+
(ChannelData extends null
|
|
22
|
+
? {
|
|
23
|
+
channel?: PikkuChannel<unknown, Out> | undefined
|
|
24
|
+
}
|
|
25
|
+
: {
|
|
26
|
+
channel: PikkuChannel<ChannelData, Out>
|
|
27
|
+
}),
|
|
28
|
+
Session extends CoreUserSession = CoreUserSession,
|
|
29
|
+
> = (
|
|
30
|
+
services: Services,
|
|
31
|
+
data: In,
|
|
32
|
+
session: Session
|
|
33
|
+
) => ChannelData extends null ? Promise<Out> : Promise<Out> | Promise<void>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Represents a core API function that can be used without a session.
|
|
37
|
+
*
|
|
38
|
+
* @template In - The input type.
|
|
39
|
+
* @template Out - The output type.
|
|
40
|
+
* @template Services - The services type, defaults to `CoreServices`.
|
|
41
|
+
* @template Session - The session type, defaults to `CoreUserSession`.
|
|
42
|
+
*/
|
|
43
|
+
export type CoreAPIFunctionSessionless<
|
|
44
|
+
In,
|
|
45
|
+
Out,
|
|
46
|
+
ChannelData extends unknown | null = null,
|
|
47
|
+
Services extends CoreSingletonServices = CoreServices &
|
|
48
|
+
(ChannelData extends null
|
|
49
|
+
? {
|
|
50
|
+
channel?: PikkuChannel<unknown, Out> | undefined
|
|
51
|
+
}
|
|
52
|
+
: {
|
|
53
|
+
channel: PikkuChannel<ChannelData, Out>
|
|
54
|
+
}),
|
|
55
|
+
Session extends CoreUserSession = CoreUserSession,
|
|
56
|
+
> = (
|
|
57
|
+
services: Services,
|
|
58
|
+
data: In,
|
|
59
|
+
session?: Session
|
|
60
|
+
) => ChannelData extends null ? Promise<Out> : Promise<Out> | Promise<void>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Represents a function that checks permissions for a given API operation.
|
|
64
|
+
*
|
|
65
|
+
* @template In - The input type.
|
|
66
|
+
* @template Services - The services type, defaults to `CoreServices`.
|
|
67
|
+
* @template Session - The session type, defaults to `CoreUserSession`.
|
|
68
|
+
*/
|
|
69
|
+
export type CoreAPIPermission<
|
|
70
|
+
In = any,
|
|
71
|
+
Services extends CoreSingletonServices = CoreServices,
|
|
72
|
+
Session extends CoreUserSession = CoreUserSession,
|
|
73
|
+
> = (services: Services, data: In, session?: Session) => Promise<boolean>
|
|
74
|
+
|
|
75
|
+
export type CorePermissionGroup<APIPermission = CoreAPIPermission<any>> =
|
|
76
|
+
| Record<string, APIPermission | APIPermission[]>
|
|
77
|
+
| undefined
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getErrorResponse } from './errors/error-handler.js'
|
|
2
|
+
import { NotFoundError } from './errors/errors.js'
|
|
3
|
+
import { Logger } from './services/logger.js'
|
|
4
|
+
import { PikkuHTTP } from './http/http.types.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handle errors that occur during route processing
|
|
8
|
+
*
|
|
9
|
+
* @param {any} e - The error that occurred
|
|
10
|
+
* @param {PikkuHTTP | undefined} http - HTTP interaction object
|
|
11
|
+
* @param {string} trackerId - Unique ID for tracking this error
|
|
12
|
+
* @param {Logger} logger - Logger service
|
|
13
|
+
* @param {number[]} logWarningsForStatusCodes - HTTP status codes to log as warnings
|
|
14
|
+
* @param {boolean} respondWith404 - Whether to respond with 404 for NotFoundError
|
|
15
|
+
* @param {boolean} bubbleError - Whether to throw the error after handling
|
|
16
|
+
*/
|
|
17
|
+
export const handleError = (
|
|
18
|
+
e: any,
|
|
19
|
+
http: PikkuHTTP | undefined,
|
|
20
|
+
trackerId: string | undefined,
|
|
21
|
+
logger: Logger,
|
|
22
|
+
logWarningsForStatusCodes: number[],
|
|
23
|
+
respondWith404: boolean,
|
|
24
|
+
bubbleError: boolean
|
|
25
|
+
) => {
|
|
26
|
+
// Skip 404 handling if configured to do so
|
|
27
|
+
if (e instanceof NotFoundError && !respondWith404) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get appropriate error response
|
|
32
|
+
const errorResponse = getErrorResponse(e)
|
|
33
|
+
if (errorResponse != null) {
|
|
34
|
+
// Set status and response body
|
|
35
|
+
http?.response?.status(errorResponse.status)
|
|
36
|
+
http?.response?.json({
|
|
37
|
+
message: errorResponse.message,
|
|
38
|
+
payload: (e as any).payload,
|
|
39
|
+
traceId: trackerId,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Log certain status codes as warnings
|
|
43
|
+
if (logWarningsForStatusCodes.includes(errorResponse.status)) {
|
|
44
|
+
if (trackerId) {
|
|
45
|
+
logger.warn(`Warning id: ${trackerId}`)
|
|
46
|
+
}
|
|
47
|
+
logger.warn(e)
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
// Handle unexpected errors
|
|
51
|
+
logger.error(e)
|
|
52
|
+
http?.response?.status(500)
|
|
53
|
+
|
|
54
|
+
if (trackerId) {
|
|
55
|
+
logger.warn(`Error id: ${trackerId}`)
|
|
56
|
+
http?.response?.json({ errorId: trackerId })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle 404 errors specifically
|
|
61
|
+
if (e instanceof NotFoundError) {
|
|
62
|
+
// TODO
|
|
63
|
+
// http?.response?.end()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Either bubble up or end the response
|
|
67
|
+
if (bubbleError) {
|
|
68
|
+
throw e
|
|
69
|
+
} else {
|
|
70
|
+
// TODO
|
|
71
|
+
// http?.response?.end()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach } from 'node:test'
|
|
2
|
+
import * as assert from 'assert'
|
|
3
|
+
import { NotFoundError } from '../errors/errors.js'
|
|
4
|
+
import { JSONValue, PikkuMiddleware } from '../types/core.types.js'
|
|
5
|
+
import { fetch, addHTTPRoute } from './http-runner.js'
|
|
6
|
+
import { pikkuState, resetPikkuState } from '../pikku-state.js'
|
|
7
|
+
import {
|
|
8
|
+
PikkuMockRequest,
|
|
9
|
+
PikkuMockResponse,
|
|
10
|
+
} from '../channel/local/local-channel-runner.test.js'
|
|
11
|
+
import { addFunction } from '../function/function-runner.js'
|
|
12
|
+
|
|
13
|
+
const sessionMiddleware: PikkuMiddleware = async (services, _, next) => {
|
|
14
|
+
services.userSession.set({ userId: 'test' } as any)
|
|
15
|
+
await next()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const setHTTPFunctionMap = (fun: any) => {
|
|
19
|
+
pikkuState('functions', 'meta', {
|
|
20
|
+
pikku_func_name: {
|
|
21
|
+
pikkuFuncName: 'pikku_func_name',
|
|
22
|
+
services: ['userSession'],
|
|
23
|
+
},
|
|
24
|
+
} as any)
|
|
25
|
+
pikkuState('http', 'meta', [
|
|
26
|
+
{
|
|
27
|
+
pikkuFuncName: 'pikku_func_name',
|
|
28
|
+
route: 'test',
|
|
29
|
+
method: 'get',
|
|
30
|
+
input: null,
|
|
31
|
+
output: null,
|
|
32
|
+
},
|
|
33
|
+
])
|
|
34
|
+
addFunction('pikku_func_name', fun)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('fetch', () => {
|
|
38
|
+
let singletonServices: any
|
|
39
|
+
let createSessionServices: any
|
|
40
|
+
let request: any
|
|
41
|
+
let response: any
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
resetPikkuState()
|
|
45
|
+
|
|
46
|
+
singletonServices = {
|
|
47
|
+
logger: {
|
|
48
|
+
info: () => {},
|
|
49
|
+
warn: () => {},
|
|
50
|
+
error: () => {},
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
createSessionServices = async () => ({})
|
|
55
|
+
request = new PikkuMockRequest('/test', 'get')
|
|
56
|
+
response = new PikkuMockResponse()
|
|
57
|
+
|
|
58
|
+
request.getData = async () => ({})
|
|
59
|
+
request.getHeader = () => 'application/json'
|
|
60
|
+
response.setStatus = (status: number) => {}
|
|
61
|
+
response.setJson = (json: JSONValue) => {}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
afterEach(() => {})
|
|
65
|
+
|
|
66
|
+
test('should throw RouteNotFoundError when no matching route is found', async () => {
|
|
67
|
+
await assert.rejects(
|
|
68
|
+
async () =>
|
|
69
|
+
fetch(request, {
|
|
70
|
+
singletonServices,
|
|
71
|
+
createSessionServices,
|
|
72
|
+
bubbleErrors: true,
|
|
73
|
+
}),
|
|
74
|
+
NotFoundError
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('should call the route function and return its result when a matching route is found', async () => {
|
|
79
|
+
const routeFunc = async () => ({ success: true })
|
|
80
|
+
setHTTPFunctionMap(routeFunc)
|
|
81
|
+
|
|
82
|
+
addHTTPRoute({
|
|
83
|
+
route: 'test',
|
|
84
|
+
method: 'get',
|
|
85
|
+
func: routeFunc,
|
|
86
|
+
middleware: [sessionMiddleware],
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const result = await fetch(request, {
|
|
90
|
+
singletonServices,
|
|
91
|
+
createSessionServices,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
assert.deepStrictEqual(await result.json(), { success: true })
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('should verify permissions if provided', async () => {
|
|
98
|
+
const permissions = { test: async () => true }
|
|
99
|
+
const routeFunc = async () => ({ success: true })
|
|
100
|
+
setHTTPFunctionMap(routeFunc)
|
|
101
|
+
|
|
102
|
+
addHTTPRoute({
|
|
103
|
+
route: 'test',
|
|
104
|
+
method: 'get',
|
|
105
|
+
func: routeFunc,
|
|
106
|
+
permissions,
|
|
107
|
+
middleware: [sessionMiddleware],
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
await fetch(request, {
|
|
111
|
+
singletonServices,
|
|
112
|
+
createSessionServices,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
assert.strictEqual(await permissions.test(), true)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('should handle errors and set appropriate response', async () => {
|
|
119
|
+
const error = new Error('Test error')
|
|
120
|
+
const routeFunc = async () => {
|
|
121
|
+
throw error
|
|
122
|
+
}
|
|
123
|
+
setHTTPFunctionMap(routeFunc)
|
|
124
|
+
addHTTPRoute({
|
|
125
|
+
route: 'test',
|
|
126
|
+
method: 'get',
|
|
127
|
+
func: routeFunc,
|
|
128
|
+
middleware: [sessionMiddleware],
|
|
129
|
+
})
|
|
130
|
+
await assert.rejects(
|
|
131
|
+
async () =>
|
|
132
|
+
fetch(request, {
|
|
133
|
+
singletonServices,
|
|
134
|
+
createSessionServices,
|
|
135
|
+
bubbleErrors: true,
|
|
136
|
+
}),
|
|
137
|
+
error
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
})
|