@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,82 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'assert'
3
+ import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js' // Adjust path if needed
4
+
5
+ describe('PikkuFetchHTTPResponse', () => {
6
+ test('sets status code', () => {
7
+ const res = new PikkuFetchHTTPResponse().status(404).toResponse()
8
+ assert.strictEqual(res.status, 404)
9
+ })
10
+
11
+ test('sets headers', async () => {
12
+ const res = new PikkuFetchHTTPResponse()
13
+ .header('X-Test', 'value')
14
+ .header('X-Multi', ['a', 'b'])
15
+ .toResponse()
16
+
17
+ assert.strictEqual(res.headers.get('X-Test'), 'value')
18
+ const multi = res.headers.get('X-Multi')?.split(', ')
19
+ assert.deepEqual(multi, ['a', 'b'])
20
+ })
21
+
22
+ test('sets cookies', () => {
23
+ const res = new PikkuFetchHTTPResponse()
24
+ .cookie('token', 'abc123', { httpOnly: true, path: '/' })
25
+ .cookie('other', 'val', { maxAge: 3600 })
26
+ .toResponse()
27
+
28
+ const setCookieHeader = res.headers.get('Set-Cookie')
29
+ assert.ok(setCookieHeader)
30
+
31
+ const cookies = setCookieHeader!.split(', ')
32
+ assert.ok(cookies.some((c) => c.startsWith('token=abc123')))
33
+ assert.ok(cookies.some((c) => c.startsWith('other=val')))
34
+ })
35
+
36
+ test('json sets correct content type and body', async () => {
37
+ const res = new PikkuFetchHTTPResponse()
38
+ .json({ hello: 'world' })
39
+ .toResponse()
40
+
41
+ assert.strictEqual(res.headers.get('Content-Type'), 'application/json')
42
+ const body = await res.json()
43
+ assert.deepEqual(body, { hello: 'world' })
44
+ })
45
+
46
+ test('text sets correct content type and body', async () => {
47
+ const res = new PikkuFetchHTTPResponse().text('hello').toResponse()
48
+
49
+ assert.strictEqual(res.headers.get('Content-Type'), 'text/plain')
50
+ const text = await res.text()
51
+ assert.strictEqual(text, 'hello')
52
+ })
53
+
54
+ test('html sets correct content type and body', async () => {
55
+ const res = new PikkuFetchHTTPResponse().html('<p>hi</p>').toResponse()
56
+
57
+ assert.strictEqual(res.headers.get('Content-Type'), 'text/html')
58
+ const text = await res.text()
59
+ assert.strictEqual(text, '<p>hi</p>')
60
+ })
61
+
62
+ test('arrayBuffer sets correct content type', async () => {
63
+ const buffer = new Uint8Array([1, 2, 3]).buffer
64
+ const res = new PikkuFetchHTTPResponse().arrayBuffer(buffer).toResponse()
65
+
66
+ assert.strictEqual(
67
+ res.headers.get('Content-Type'),
68
+ 'application/octet-stream'
69
+ )
70
+ const body = await res.arrayBuffer()
71
+ assert.deepEqual(new Uint8Array(body), new Uint8Array([1, 2, 3]))
72
+ })
73
+
74
+ test('redirect sets Location header and status', () => {
75
+ const res = new PikkuFetchHTTPResponse()
76
+ .redirect('/login', 301)
77
+ .toResponse()
78
+
79
+ assert.strictEqual(res.status, 301)
80
+ assert.strictEqual(res.headers.get('Location'), '/login')
81
+ })
82
+ })
@@ -0,0 +1,138 @@
1
+ import { PikkuHTTPResponse } from './http.types.js'
2
+ import {
3
+ SerializeOptions as CookieSerializeOptions,
4
+ serialize as serializeCookie,
5
+ } from 'cookie'
6
+
7
+ export class PikkuFetchHTTPResponse implements PikkuHTTPResponse {
8
+ #statusCode: number = 200
9
+ #headers = new Headers()
10
+ #cookies = new Map<string, { value: string; flags: CookieSerializeOptions }>()
11
+
12
+ #body: BodyInit | null = null
13
+ #responseMode: 'stream' | null = null
14
+ #send: ((data: string) => void) | null = null
15
+ #close: (() => void) | null = null
16
+
17
+ public setMode(mode: 'stream') {
18
+ this.#responseMode = 'stream'
19
+ if (mode === 'stream') {
20
+ this.#body = this.createStream()
21
+ }
22
+ }
23
+
24
+ public status(code: number): this {
25
+ this.#statusCode = code
26
+ return this
27
+ }
28
+
29
+ public cookie(
30
+ name: string,
31
+ value: string,
32
+ flags: CookieSerializeOptions
33
+ ): this {
34
+ this.#cookies.set(name, { value, flags })
35
+ return this
36
+ }
37
+
38
+ public header(name: string, value: string | string[]): this {
39
+ this.#headers.delete(name)
40
+ if (Array.isArray(value)) {
41
+ value.forEach((v) => this.#headers.append(name, v))
42
+ } else {
43
+ this.#headers.set(name, value)
44
+ }
45
+ return this
46
+ }
47
+
48
+ public arrayBuffer(data: XMLHttpRequestBodyInit): this {
49
+ if (this.#responseMode === 'stream') {
50
+ this.#send!(data as string)
51
+ } else {
52
+ this.#body = data
53
+ this.header('Content-Type', 'application/octet-stream')
54
+ }
55
+ return this
56
+ }
57
+
58
+ public json(data: unknown): this {
59
+ if (this.#responseMode === 'stream') {
60
+ this.#send!(JSON.stringify(data))
61
+ } else {
62
+ this.#body = JSON.stringify(data)
63
+ this.header('Content-Type', 'application/json')
64
+ }
65
+ return this
66
+ }
67
+
68
+ public text(content: string): this {
69
+ if (this.#responseMode === 'stream') {
70
+ this.#send!(content)
71
+ } else {
72
+ this.#body = content
73
+ this.header('Content-Type', 'text/plain')
74
+ }
75
+ return this
76
+ }
77
+
78
+ public html(content: string): this {
79
+ if (this.#responseMode === 'stream') {
80
+ this.#send!(content)
81
+ } else {
82
+ this.#body = content
83
+ this.header('Content-Type', 'text/html')
84
+ }
85
+ return this
86
+ }
87
+
88
+ // public body(body: BodyInit): this {
89
+ // this.#body = body
90
+ // return this
91
+ // }
92
+
93
+ public redirect(location: string, status: number = 302): this {
94
+ this.#statusCode = status
95
+ this.header('Location', location)
96
+ return this
97
+ }
98
+
99
+ public close(): this {
100
+ if (this.#close) {
101
+ this.#close()
102
+ }
103
+ return this
104
+ }
105
+
106
+ public toResponse(args?: Record<string, any>): Response {
107
+ const cookieHeader = Array.from(this.#cookies.entries()).map(
108
+ ([name, { value, flags }]) => serializeCookie(name, value, flags)
109
+ )
110
+ this.#headers.set('Set-Cookie', cookieHeader.join(', '))
111
+ return new Response(this.#body, {
112
+ ...args,
113
+ status: this.#statusCode,
114
+ headers: this.#headers,
115
+ })
116
+ }
117
+
118
+ private createStream(): ReadableStream<Uint8Array> {
119
+ const encoder = new TextEncoder()
120
+ return new ReadableStream({
121
+ start: (controller) => {
122
+ const send = (data: string) => {
123
+ controller.enqueue(encoder.encode(`data: ${data}\n\n`))
124
+ }
125
+
126
+ const close = () => {
127
+ controller.close()
128
+ }
129
+
130
+ this.#send = send
131
+ this.#close = close
132
+
133
+ // Force initial flush
134
+ controller.enqueue(encoder.encode(':\n\n'))
135
+ },
136
+ })
137
+ }
138
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @module @pikku/core
3
+ */
4
+ export * from './types/core.types.js'
5
+ export * from './function/index.js'
6
+ export * from './pikku-request.js'
7
+ export * from './pikku-response.js'
8
+ export * from './services/index.js'
9
+ export * from './http/index.js'
10
+ export * from './channel/index.js'
11
+ export * from './scheduler/index.js'
12
+ export * from './errors/index.js'
13
+ export * from './middleware/index.js'
14
+ export * from './time-utils.js'
15
+ export * from './utils.js'
16
+ export { pikkuState } from './pikku-state.js'
17
+ export { runMiddleware } from './middleware-runner.js'
18
+ export { addHTTPRoute, addMiddleware } from './http/http-runner.js'
19
+ export { addChannel } from './channel/channel-runner.js'
20
+ export { addScheduledTask } from './scheduler/scheduler-runner.js'
@@ -0,0 +1,66 @@
1
+ import {
2
+ CoreConfig,
3
+ CoreSingletonServices,
4
+ CoreUserSession,
5
+ PikkuMiddleware,
6
+ } from '../types/core.types.js'
7
+
8
+ /**
9
+ * API key middleware that retrieves a session from the 'x-api-key' header using a provided callback.
10
+ *
11
+ * @param options.getSessionForAPIKey - A function that returns a session when provided an API key.
12
+ */
13
+ export const authAPIKey = <
14
+ SingletonServices extends CoreSingletonServices<CoreConfig>,
15
+ UserSession extends CoreUserSession,
16
+ >({
17
+ source,
18
+ getSessionForAPIKey,
19
+ jwt,
20
+ }: {
21
+ source: 'header' | 'query' | 'all'
22
+ } & (
23
+ | {
24
+ getSessionForAPIKey?: undefined
25
+ jwt?: true
26
+ }
27
+ | {
28
+ getSessionForAPIKey: (
29
+ services: SingletonServices,
30
+ apiKey: string
31
+ ) => Promise<any>
32
+ jwt?: false
33
+ }
34
+ )) => {
35
+ const middleware: PikkuMiddleware = async (services, { http }, next) => {
36
+ const { userSession: userSessionService, jwt: jwtService } = services
37
+ if (!http?.request || userSessionService.get()) {
38
+ return next()
39
+ }
40
+
41
+ let apiKey: string | null = null
42
+ if (source === 'header' || source === 'all') {
43
+ apiKey = http.request.header('x-api-key') as string | null
44
+ }
45
+ if (!apiKey && (source === 'query' || source === 'all')) {
46
+ apiKey = http.request.query().apiKey as string | null
47
+ }
48
+ if (apiKey) {
49
+ let userSession: UserSession | null = null
50
+ if (jwt) {
51
+ if (!jwtService) {
52
+ throw new Error('JWT service is required for JWT decoding.')
53
+ }
54
+ userSession = await jwtService.decode(apiKey)
55
+ } else {
56
+ userSession = await getSessionForAPIKey!(services as any, apiKey)
57
+ }
58
+ if (userSession) {
59
+ userSessionService.setInitial(userSession)
60
+ }
61
+ }
62
+ return next()
63
+ }
64
+
65
+ return middleware
66
+ }
@@ -0,0 +1,66 @@
1
+ import { InvalidSessionError } from '../errors/errors.js'
2
+ import {
3
+ CoreConfig,
4
+ CoreSingletonServices,
5
+ CoreUserSession,
6
+ PikkuMiddleware,
7
+ } from '../types/core.types.js'
8
+
9
+ /**
10
+ * Extracts the Bearer token from the Authorization header
11
+ */
12
+ export const authBearer = <
13
+ SingletonServices extends CoreSingletonServices<CoreConfig>,
14
+ UserSession extends CoreUserSession,
15
+ >({
16
+ token,
17
+ jwt,
18
+ getSession,
19
+ }: {
20
+ token?: {
21
+ value: string
22
+ userSession: UserSession
23
+ }
24
+ jwt?: boolean
25
+ getSession?: (
26
+ services: SingletonServices,
27
+ token: string
28
+ ) => Promise<UserSession> | UserSession
29
+ } = {}): PikkuMiddleware => {
30
+ const middleware: PikkuMiddleware = async (services, { http }, next) => {
31
+ const { userSession: userSessionService, jwt: jwtService } = services
32
+ // Skip if session already exists.
33
+ if (!http?.request || userSessionService.get()) {
34
+ return next()
35
+ }
36
+
37
+ const authHeader =
38
+ http.request.header('authorization') ||
39
+ http.request.header('Authorization')
40
+ if (authHeader) {
41
+ const [scheme, bearerToken] = authHeader.split(' ')
42
+ if (scheme !== 'Bearer' || !token || !bearerToken) {
43
+ throw new InvalidSessionError()
44
+ }
45
+ let userSession: UserSession | null = null
46
+ if (jwt) {
47
+ if (!jwtService) {
48
+ throw new Error('JWT service is required for JWT decoding.')
49
+ }
50
+ userSession = await jwtService.decode(bearerToken)
51
+ } else if (token) {
52
+ if (bearerToken === token.value) {
53
+ userSession = token.userSession
54
+ }
55
+ } else if (getSession) {
56
+ userSession = await getSession(services as any, token)
57
+ }
58
+
59
+ if (userSession) {
60
+ userSessionService.setInitial(userSession)
61
+ }
62
+ }
63
+ return next()
64
+ }
65
+ return middleware
66
+ }
@@ -0,0 +1,103 @@
1
+ import { SerializeOptions } from 'cookie'
2
+ import {
3
+ CoreConfig,
4
+ CoreSingletonServices,
5
+ CoreUserSession,
6
+ PikkuMiddleware,
7
+ } from '../types/core.types.js'
8
+ import {
9
+ getRelativeTimeOffsetFromNow,
10
+ RelativeTimeInput,
11
+ } from '../time-utils.js'
12
+
13
+ /**
14
+ * Cookie middleware that extracts a session from cookies.
15
+ *
16
+ * @param options.name - List of cookie names to check.
17
+ * @param options.getSessionForCookieValue - Function to retrieve a session using a cookie value.
18
+ */
19
+ export const authCookie = <
20
+ SingletonServices extends CoreSingletonServices<CoreConfig>,
21
+ UserSession extends CoreUserSession,
22
+ >({
23
+ name,
24
+ getSessionForCookieValue,
25
+ jwt,
26
+ options,
27
+ expiresIn,
28
+ }: {
29
+ name: string
30
+ options: SerializeOptions
31
+ expiresIn: RelativeTimeInput
32
+ } & (
33
+ | {
34
+ getSessionForCookieValue: (
35
+ services: SingletonServices,
36
+ cookieValue: string,
37
+ cookieName: string
38
+ ) => Promise<UserSession>
39
+ jwt?: false
40
+ }
41
+ | {
42
+ getSessionForCookieValue?: undefined
43
+ jwt: true
44
+ }
45
+ )): PikkuMiddleware => {
46
+ const middleware: PikkuMiddleware = async (services, { http }, next) => {
47
+ const {
48
+ userSession: userSessionService,
49
+ jwt: jwtService,
50
+ logger,
51
+ } = services
52
+ if (!http?.request || userSessionService.get()) {
53
+ return next()
54
+ }
55
+
56
+ let userSession: UserSession | null = null
57
+ const cookieValue = http.request.cookie(name)
58
+ if (cookieValue) {
59
+ if (jwt) {
60
+ if (!jwtService) {
61
+ throw new Error('JWT service is required for JWT decoding.')
62
+ }
63
+ userSession = await jwtService.decode(cookieValue)
64
+ } else if (getSessionForCookieValue) {
65
+ userSession = await getSessionForCookieValue(
66
+ services as any,
67
+ cookieValue,
68
+ name
69
+ )
70
+ }
71
+ }
72
+
73
+ if (userSession) {
74
+ userSessionService.setInitial(userSession)
75
+ }
76
+ await next()
77
+
78
+ // Set the cookie in the response if the session has changed
79
+ if (!http?.response) {
80
+ return
81
+ }
82
+
83
+ if (userSessionService.sessionChanged) {
84
+ const session = userSessionService.get()
85
+ if (jwt) {
86
+ if (!jwtService) {
87
+ throw new Error('JWT service is required for JWT encoding.')
88
+ }
89
+ http.response.cookie(
90
+ name,
91
+ await jwtService.encode(expiresIn, session),
92
+ {
93
+ ...options,
94
+ expires: getRelativeTimeOffsetFromNow(expiresIn),
95
+ }
96
+ )
97
+ } else {
98
+ logger.warn('No JWT service available, unable to set cookie')
99
+ }
100
+ }
101
+ }
102
+ return middleware
103
+ }
@@ -0,0 +1,3 @@
1
+ export * from './auth-apikey.js'
2
+ export * from './auth-cookie.js'
3
+ export * from './auth-bearer.js'
@@ -0,0 +1,13 @@
1
+ import { RequestTimeoutError } from '../errors/errors.js'
2
+ import { PikkuMiddleware } from '../types/core.types.js'
3
+
4
+ export const timeout = (timeout: number) => {
5
+ const middleware: PikkuMiddleware = async (_services, _interaction, next) => {
6
+ setTimeout(() => {
7
+ throw new RequestTimeoutError()
8
+ }, timeout)
9
+
10
+ await next()
11
+ }
12
+ return middleware
13
+ }
@@ -0,0 +1,43 @@
1
+ import { UserSessionService } from './services/user-session-service.js'
2
+ import {
3
+ CoreSingletonServices,
4
+ PikkuInteraction,
5
+ PikkuMiddleware,
6
+ } from './types/core.types.js'
7
+
8
+ /**
9
+ * Runs a chain of middleware functions in sequence before executing the main function.
10
+ *
11
+ * @param services - An object containing services (e.g., singletonServices, userSession, etc.)
12
+ * @param interaction - The interaction context, e.g., { http }.
13
+ * @param middlewares - An array of middleware functions to run.
14
+ * @param main - The main function to execute after all middleware have run.
15
+ * @returns A promise resolving to the result of the main function.
16
+ *
17
+ * @example
18
+ * runMiddleware(
19
+ * { ...services, userSession },
20
+ * { http },
21
+ * [middleware1, middleware2, middleware3],
22
+ * async () => { return await runMain(); }
23
+ * );
24
+ */
25
+ export const runMiddleware = async (
26
+ services: CoreSingletonServices & {
27
+ userSession: UserSessionService<any>
28
+ },
29
+ interaction: PikkuInteraction,
30
+ middlewares: PikkuMiddleware[],
31
+ main?: () => Promise<void>
32
+ ): Promise<void> => {
33
+ const dispatch = async (index: number): Promise<any> => {
34
+ if (middlewares && index < middlewares.length) {
35
+ await middlewares[index]!(services, interaction, () =>
36
+ dispatch(index + 1)
37
+ )
38
+ } else if (main) {
39
+ return await main()
40
+ }
41
+ }
42
+ await dispatch(0)
43
+ }
@@ -0,0 +1,58 @@
1
+ import { test, describe } from 'node:test'
2
+ import assert from 'assert'
3
+ import { getErrorResponse, addError } from './errors/error-handler.js'
4
+ import {
5
+ ForbiddenError,
6
+ BadRequestError,
7
+ NotFoundError,
8
+ } from './errors/errors.js'
9
+
10
+ describe('getErrorResponse', () => {
11
+ test('should return the correct error response for BadRequestError', () => {
12
+ const error = new BadRequestError()
13
+ const response = getErrorResponse(error)
14
+ assert.deepStrictEqual(response, {
15
+ status: 400,
16
+ message:
17
+ 'The server cannot or will not process the request due to client error (e.g., malformed request syntax).',
18
+ })
19
+ })
20
+
21
+ test('should return the correct error response for NotFoundError', () => {
22
+ const error = new NotFoundError()
23
+ const response = getErrorResponse(error)
24
+ assert.deepStrictEqual(response, {
25
+ status: 404,
26
+ message: 'The server cannot find the requested resource.',
27
+ })
28
+ })
29
+
30
+ test('should return the correct error response for ForbiddenError', () => {
31
+ const error = new ForbiddenError()
32
+ const response = getErrorResponse(error)
33
+ assert.deepStrictEqual(response, {
34
+ status: 403,
35
+ message:
36
+ 'The client does not have permission to access the requested resource.',
37
+ })
38
+ })
39
+
40
+ test('should return undefined for an unknown error', () => {
41
+ class UnknownError extends Error {}
42
+ const error = new UnknownError()
43
+ const response = getErrorResponse(error)
44
+ assert.strictEqual(response, undefined)
45
+ })
46
+
47
+ test('should return the correct error response for a custom error added to apiErrors', () => {
48
+ class CustomError extends Error {}
49
+ const customError = new CustomError()
50
+ const customErrorDetails = { status: 400, message: 'Custom Error' }
51
+
52
+ // Add the custom error to the apiErrors map
53
+ addError(CustomError, customErrorDetails)
54
+
55
+ const response = getErrorResponse(customError)
56
+ assert.deepStrictEqual(response, customErrorDetails)
57
+ })
58
+ })
@@ -0,0 +1,43 @@
1
+ import { CoreServices, CoreUserSession } from './types/core.types.js'
2
+ import { CorePermissionGroup } from './function/functions.types.js'
3
+
4
+ /**
5
+ * This function validates permissions by iterating over permission groups and executing the corresponding permission functions. If all functions in at least one group return true, the permission is considered valid.
6
+ * @param services - The core services required for permission validation.
7
+ * @param data - The data to be used in the permission validation functions.
8
+ * @param session - An optional user session for permission validation.
9
+ * @returns A promise that resolves to void.
10
+ */
11
+ export const verifyPermissions = async (
12
+ permissions: CorePermissionGroup,
13
+ services: CoreServices,
14
+ data: any,
15
+ session?: CoreUserSession
16
+ ): Promise<boolean> => {
17
+ if (!permissions) {
18
+ return true
19
+ }
20
+
21
+ let valid = false
22
+ const permissionGroups = Object.values(permissions)
23
+ if (permissionGroups.length === 0) {
24
+ return true
25
+ }
26
+
27
+ for (const funcs of permissionGroups) {
28
+ if (funcs instanceof Array) {
29
+ const permissioned = await Promise.all(
30
+ funcs.map((func) => func(services, data, session))
31
+ )
32
+ if (permissioned.every((result) => result)) {
33
+ valid = true
34
+ }
35
+ } else {
36
+ valid = await funcs(services, data, session)
37
+ }
38
+ if (valid) {
39
+ return true
40
+ }
41
+ }
42
+ return false
43
+ }
@@ -0,0 +1 @@
1
+ export const callFunction = () => {}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Simplest class to represent a pikku request.
3
+ * @template In - The type of the request body.
4
+ * @group RequestResponse
5
+ */
6
+ export abstract class PikkuRequest<In = any> {
7
+ #data: In | undefined
8
+
9
+ constructor(data: In) {
10
+ this.#data = data
11
+ }
12
+
13
+ /**
14
+ * Retrieves the data
15
+ * @returns A promise that resolves to an object containing the combined data.
16
+ */
17
+ public async data(): Promise<In> {
18
+ if (!this.#data) {
19
+ throw new Error('Data not found')
20
+ }
21
+ return this.#data
22
+ }
23
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Abstract class representing a pikku response.
3
+ * @group RequestResponse
4
+ */
5
+ export abstract class PikkuResponse {}