@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.
Files changed (94) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/channel/channel-runner.js +1 -1
  3. package/dist/channel/serverless/serverless-channel-runner.js +1 -1
  4. package/dist/http/http-runner.js +4 -2
  5. package/dist/http/log-http-routes.js +1 -1
  6. package/lcov.info +4585 -0
  7. package/package.json +1 -1
  8. package/src/channel/channel-handler.ts +165 -0
  9. package/src/channel/channel-runner.ts +153 -0
  10. package/src/channel/channel-store.ts +29 -0
  11. package/src/channel/channel.types.ts +133 -0
  12. package/src/channel/eventhub-service.ts +30 -0
  13. package/src/channel/eventhub-store.ts +8 -0
  14. package/src/channel/index.ts +8 -0
  15. package/src/channel/local/index.ts +3 -0
  16. package/src/channel/local/local-channel-handler.ts +50 -0
  17. package/src/channel/local/local-channel-runner.test.ts +157 -0
  18. package/src/channel/local/local-channel-runner.ts +139 -0
  19. package/src/channel/local/local-eventhub-service.test.ts +95 -0
  20. package/src/channel/local/local-eventhub-service.ts +95 -0
  21. package/src/channel/log-channels.ts +20 -0
  22. package/src/channel/pikku-abstract-channel-handler.test.ts +52 -0
  23. package/src/channel/pikku-abstract-channel-handler.ts +38 -0
  24. package/src/channel/serverless/index.ts +1 -0
  25. package/src/channel/serverless/serverless-channel-runner.ts +220 -0
  26. package/src/errors/error-handler.ts +61 -0
  27. package/src/errors/error.test.ts +54 -0
  28. package/src/errors/errors.ts +339 -0
  29. package/src/errors/index.ts +2 -0
  30. package/src/function/function-runner.ts +91 -0
  31. package/src/function/functions.types.ts +77 -0
  32. package/src/function/index.ts +2 -0
  33. package/src/handle-error.ts +73 -0
  34. package/src/http/http-runner.test.ts +140 -0
  35. package/src/http/http-runner.ts +493 -0
  36. package/src/http/http.types.ts +250 -0
  37. package/src/http/incomingmessage-to-request-convertor.ts +0 -0
  38. package/src/http/index.ts +8 -0
  39. package/src/http/log-http-routes.ts +22 -0
  40. package/src/http/pikku-fetch-http-request.test.ts +237 -0
  41. package/src/http/pikku-fetch-http-request.ts +163 -0
  42. package/src/http/pikku-fetch-http-response.test.ts +82 -0
  43. package/src/http/pikku-fetch-http-response.ts +138 -0
  44. package/src/index.ts +20 -0
  45. package/src/middleware/auth-apikey.ts +66 -0
  46. package/src/middleware/auth-bearer.ts +66 -0
  47. package/src/middleware/auth-cookie.ts +103 -0
  48. package/src/middleware/index.ts +3 -0
  49. package/src/middleware/timeout.ts +13 -0
  50. package/src/middleware-runner.ts +43 -0
  51. package/src/permissions.test.ts +58 -0
  52. package/src/permissions.ts +43 -0
  53. package/src/pikku-function.ts +1 -0
  54. package/src/pikku-request.ts +23 -0
  55. package/src/pikku-response.ts +5 -0
  56. package/src/pikku-state.ts +87 -0
  57. package/src/scheduler/index.ts +5 -0
  58. package/src/scheduler/log-schedulers.ts +20 -0
  59. package/src/scheduler/scheduler-runner.ts +105 -0
  60. package/src/scheduler/scheduler.types.ts +33 -0
  61. package/src/schema.test.ts +57 -0
  62. package/src/schema.ts +99 -0
  63. package/src/services/content-service.ts +68 -0
  64. package/src/services/index.ts +18 -0
  65. package/src/services/jwt-service.ts +30 -0
  66. package/src/services/local-content.ts +105 -0
  67. package/src/services/local-secrets.ts +43 -0
  68. package/src/services/local-variables.ts +17 -0
  69. package/src/services/logger-console.ts +97 -0
  70. package/src/services/logger.ts +57 -0
  71. package/src/services/schema-service.ts +26 -0
  72. package/src/services/secret-service.ts +17 -0
  73. package/src/services/user-session-service.ts +53 -0
  74. package/src/services/variables-service.ts +6 -0
  75. package/src/time-utils.test.ts +56 -0
  76. package/src/time-utils.ts +32 -0
  77. package/src/types/core.types.ts +178 -0
  78. package/src/utils.ts +40 -0
  79. package/tsconfig.json +13 -0
  80. package/tsconfig.tsbuildinfo +1 -0
  81. package/dist/http/http-route-runner.d.ts +0 -90
  82. package/dist/http/http-route-runner.js +0 -325
  83. package/dist/http/http-routes.types.d.ts +0 -179
  84. package/dist/http/http-routes.types.js +0 -1
  85. package/dist/http/pikku-http-request.d.ts +0 -56
  86. package/dist/http/pikku-http-request.js +0 -95
  87. package/dist/http/pikku-http-response.d.ts +0 -13
  88. package/dist/http/pikku-http-response.js +0 -60
  89. package/dist/parse-relative-time-offset.d.ts +0 -12
  90. package/dist/parse-relative-time-offset.js +0 -20
  91. package/dist/pikku-func.d.ts +0 -12
  92. package/dist/pikku-func.js +0 -39
  93. package/dist/types/functions.types.d.ts +0 -37
  94. 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,2 @@
1
+ export * from './function-runner.js'
2
+ export * from './functions.types.js'
@@ -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
+ })