@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
|
@@ -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,45 @@
|
|
|
1
|
+
import { CoreServices, CoreUserSession } from './types/core.types.js'
|
|
2
|
+
import { CoreAPIPermission } from './types/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:
|
|
13
|
+
| Record<string, CoreAPIPermission<any>[] | CoreAPIPermission<any>>
|
|
14
|
+
| undefined,
|
|
15
|
+
services: CoreServices,
|
|
16
|
+
data: any,
|
|
17
|
+
session?: CoreUserSession
|
|
18
|
+
): Promise<boolean> => {
|
|
19
|
+
if (!permissions) {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let valid = false
|
|
24
|
+
const permissionGroups = Object.values(permissions)
|
|
25
|
+
if (permissionGroups.length === 0) {
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const funcs of permissionGroups) {
|
|
30
|
+
if (funcs instanceof Array) {
|
|
31
|
+
const permissioned = await Promise.all(
|
|
32
|
+
funcs.map((func) => func(services, data, session))
|
|
33
|
+
)
|
|
34
|
+
if (permissioned.every((result) => result)) {
|
|
35
|
+
valid = true
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
valid = await funcs(services, data, session)
|
|
39
|
+
}
|
|
40
|
+
if (valid) {
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
constructor(private data?: In) {}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Retrieves the data
|
|
11
|
+
* @returns A promise that resolves to an object containing the combined data.
|
|
12
|
+
*/
|
|
13
|
+
public async getData(): Promise<In> {
|
|
14
|
+
if (!this.data) {
|
|
15
|
+
throw new Error('Data not found')
|
|
16
|
+
}
|
|
17
|
+
return this.data
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Logger } from '../services/index.js'
|
|
2
|
+
import { getScheduledTasks } from './scheduler-runner.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Logs all the loaded scheduled tasks.
|
|
6
|
+
* @param logger - A logger for logging information.
|
|
7
|
+
*/
|
|
8
|
+
export const logSchedulers = (logger: Logger) => {
|
|
9
|
+
const { scheduledTasks } = getScheduledTasks()
|
|
10
|
+
if (scheduledTasks.size === 0) {
|
|
11
|
+
logger.info('No scheduled tasks added')
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let scheduledTasksMessage = 'Scheduled tasks:'
|
|
16
|
+
scheduledTasks.forEach(({ schedule }, name) => {
|
|
17
|
+
scheduledTasksMessage += `\n\t- ${name} -> ${schedule}`
|
|
18
|
+
})
|
|
19
|
+
logger.info(scheduledTasksMessage)
|
|
20
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CoreServices,
|
|
3
|
+
CoreSingletonServices,
|
|
4
|
+
CoreUserSession,
|
|
5
|
+
CreateSessionServices,
|
|
6
|
+
} from '../types/core.types.js'
|
|
7
|
+
import type {
|
|
8
|
+
CoreScheduledTask,
|
|
9
|
+
ScheduledTasksMeta,
|
|
10
|
+
} from './scheduler.types.js'
|
|
11
|
+
import type { CoreAPIFunctionSessionless } from '../types/functions.types.js'
|
|
12
|
+
|
|
13
|
+
import { getErrorResponse } from '../errors/error-handler.js'
|
|
14
|
+
import { closeSessionServices } from '../utils.js'
|
|
15
|
+
|
|
16
|
+
export type RunScheduledTasksParams = {
|
|
17
|
+
name: string
|
|
18
|
+
session?: CoreUserSession
|
|
19
|
+
singletonServices: CoreSingletonServices
|
|
20
|
+
createSessionServices?: CreateSessionServices<
|
|
21
|
+
CoreSingletonServices,
|
|
22
|
+
CoreServices<CoreSingletonServices>,
|
|
23
|
+
CoreUserSession
|
|
24
|
+
>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!global.pikku?.scheduledTasks) {
|
|
28
|
+
global.pikku = global.pikku || {}
|
|
29
|
+
global.pikku.scheduledTasks = new Map<string, CoreScheduledTask>()
|
|
30
|
+
global.pikku.scheduledTasksMeta = []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const scheduledTasks = (): Map<string, CoreScheduledTask> => {
|
|
34
|
+
return global.pikku.scheduledTasks
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const scheduledTasksMeta = (data?: ScheduledTasksMeta) => {
|
|
38
|
+
if (data) {
|
|
39
|
+
global.pikku.scheduledTasksMeta = data
|
|
40
|
+
}
|
|
41
|
+
return global.pikku.scheduledTasksMeta
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const addScheduledTask = <
|
|
45
|
+
APIFunction extends CoreAPIFunctionSessionless<void, void>,
|
|
46
|
+
>(
|
|
47
|
+
scheduledTask: CoreScheduledTask<APIFunction>
|
|
48
|
+
) => {
|
|
49
|
+
if (scheduledTasks().has(scheduledTask.name)) {
|
|
50
|
+
throw new Error(`Scheduled task already exists: ${scheduledTask.name}`)
|
|
51
|
+
}
|
|
52
|
+
scheduledTasks().set(scheduledTask.name, scheduledTask)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const clearScheduledTasks = () => {
|
|
56
|
+
scheduledTasks().clear()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const setScheduledTasksMeta = (
|
|
60
|
+
_scheduledTasksMeta: ScheduledTasksMeta
|
|
61
|
+
) => {
|
|
62
|
+
scheduledTasksMeta(_scheduledTasksMeta)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns all the cron jobs
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
export const getScheduledTasks = () => {
|
|
70
|
+
return {
|
|
71
|
+
scheduledTasks: scheduledTasks(),
|
|
72
|
+
scheduledTasksMeta: scheduledTasksMeta(),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class ScheduledTaskNotFoundError extends Error {
|
|
77
|
+
constructor(title: string) {
|
|
78
|
+
super(`Scheduled task not found: ${title}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function runScheduledTask<
|
|
83
|
+
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
84
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
85
|
+
Services extends
|
|
86
|
+
CoreServices<SingletonServices> = CoreServices<SingletonServices>,
|
|
87
|
+
>({
|
|
88
|
+
name,
|
|
89
|
+
session,
|
|
90
|
+
singletonServices,
|
|
91
|
+
createSessionServices,
|
|
92
|
+
}: RunScheduledTasksParams): Promise<void> {
|
|
93
|
+
let sessionServices: CoreServices | undefined
|
|
94
|
+
const trackerId: string = crypto.randomUUID().toString()
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const task = scheduledTasks().get(name)
|
|
98
|
+
|
|
99
|
+
if (!task) {
|
|
100
|
+
throw new ScheduledTaskNotFoundError(`Scheduled task not found: ${name}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
singletonServices.logger.info(
|
|
104
|
+
`Running schedule task: ${name} | schedule: ${task.schedule}}`
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
let allServices = singletonServices
|
|
108
|
+
if (createSessionServices) {
|
|
109
|
+
const sessionServices = await createSessionServices(
|
|
110
|
+
singletonServices,
|
|
111
|
+
{},
|
|
112
|
+
session
|
|
113
|
+
)
|
|
114
|
+
allServices = { ...singletonServices, ...sessionServices }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await task.func(allServices, undefined, session!)
|
|
118
|
+
} catch (e: any) {
|
|
119
|
+
const errorResponse = getErrorResponse(e)
|
|
120
|
+
|
|
121
|
+
if (errorResponse != null) {
|
|
122
|
+
singletonServices.logger.warn(`Error id: ${trackerId}`)
|
|
123
|
+
singletonServices.logger.error(e)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
throw e
|
|
127
|
+
} finally {
|
|
128
|
+
if (sessionServices) {
|
|
129
|
+
await closeSessionServices(singletonServices.logger, sessionServices)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { APIDocs, CoreUserSession } from '../types/core.types.js'
|
|
2
|
+
import { CoreAPIFunctionSessionless } from '../types/functions.types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents metadata for scheduled tasks, including title, schedule, and documentation.
|
|
6
|
+
*/
|
|
7
|
+
export type ScheduledTasksMeta<UserSession extends CoreUserSession = any> =
|
|
8
|
+
Array<{
|
|
9
|
+
name: string
|
|
10
|
+
schedule: string
|
|
11
|
+
session?: UserSession
|
|
12
|
+
docs?: APIDocs
|
|
13
|
+
}>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Represents a core scheduled task.
|
|
17
|
+
*/
|
|
18
|
+
export type CoreScheduledTask<
|
|
19
|
+
APIFunction = CoreAPIFunctionSessionless<void, void>,
|
|
20
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
21
|
+
> = {
|
|
22
|
+
name: string
|
|
23
|
+
schedule: string
|
|
24
|
+
func: APIFunction
|
|
25
|
+
docs?: APIDocs
|
|
26
|
+
session?: UserSession
|
|
27
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { test, describe, before } from 'node:test'
|
|
2
|
+
import * as assert from 'assert'
|
|
3
|
+
import { addSchema, coerceQueryStringToArray } from './schema.js'
|
|
4
|
+
|
|
5
|
+
describe('Schema', () => {
|
|
6
|
+
describe('coerceQueryStringToArray', () => {
|
|
7
|
+
before(() => {
|
|
8
|
+
// Add schemas dynamically using addSchema
|
|
9
|
+
addSchema('testSchema', {
|
|
10
|
+
properties: {
|
|
11
|
+
tags: { type: 'array' },
|
|
12
|
+
count: { type: 'number' },
|
|
13
|
+
name: { type: 'string' },
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
addSchema('booleanSchema', {
|
|
18
|
+
properties: {
|
|
19
|
+
isActive: true, // Invalid schema definition, but the function should handle it
|
|
20
|
+
tags: { type: 'array' },
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('should split a string into an array for properties of type array', () => {
|
|
26
|
+
const data = { tags: 'a,b,c' }
|
|
27
|
+
coerceQueryStringToArray('testSchema', data)
|
|
28
|
+
assert.deepStrictEqual(data.tags, ['a', 'b', 'c'])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('should not modify properties of type array if they are already arrays', () => {
|
|
32
|
+
const data = { tags: ['a', 'b', 'c'] }
|
|
33
|
+
coerceQueryStringToArray('testSchema', data)
|
|
34
|
+
assert.deepStrictEqual(data.tags, ['a', 'b', 'c'])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('should not modify properties that are not type array', () => {
|
|
38
|
+
const data = { count: 5, name: 'example' }
|
|
39
|
+
coerceQueryStringToArray('testSchema', data)
|
|
40
|
+
assert.strictEqual(data.count, 5)
|
|
41
|
+
assert.strictEqual(data.name, 'example')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('should handle cases where the data object does not have a key present in the schema', () => {
|
|
45
|
+
const data = { unknownKey: 'shouldRemain' }
|
|
46
|
+
coerceQueryStringToArray('testSchema', data)
|
|
47
|
+
assert.strictEqual(data.unknownKey, 'shouldRemain')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('should handle cases where schema properties contain boolean values', () => {
|
|
51
|
+
const data = { tags: 'a,b,c', isActive: 'true' }
|
|
52
|
+
coerceQueryStringToArray('booleanSchema', data)
|
|
53
|
+
assert.deepStrictEqual(data.tags, ['a', 'b', 'c'])
|
|
54
|
+
assert.strictEqual(data.isActive, 'true') // No coercion should happen
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
})
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Logger } from './services/logger.js'
|
|
2
|
+
import { getRoutes } from './http/http-route-runner.js'
|
|
3
|
+
import { SchemaService } from './services/schema-service.js'
|
|
4
|
+
import { BadRequestError } from './errors/errors.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Retrieves the global schemas map.
|
|
8
|
+
* @returns A map of schemas.
|
|
9
|
+
*/
|
|
10
|
+
export const getSchemas = () => {
|
|
11
|
+
if (!global.pikkuSchemas) {
|
|
12
|
+
global.pikkuSchemas = new Map<string, any>()
|
|
13
|
+
}
|
|
14
|
+
return global.pikkuSchemas
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Retrieves a schema from the schemas map.
|
|
19
|
+
* @returns A schema.
|
|
20
|
+
*/
|
|
21
|
+
export const getSchema = (schemaName: string): any => {
|
|
22
|
+
const schemas = getSchemas()
|
|
23
|
+
const schema = schemas.get(schemaName)
|
|
24
|
+
if (!schema) {
|
|
25
|
+
throw new Error(`Schema not found: ${schemaName}`)
|
|
26
|
+
}
|
|
27
|
+
return schema
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Adds a schema to the global schemas map.
|
|
32
|
+
* @param name - The name of the schema.
|
|
33
|
+
* @param value - The schema value.
|
|
34
|
+
* @ignore
|
|
35
|
+
*/
|
|
36
|
+
export const addSchema = (name: string, value: any) => {
|
|
37
|
+
getSchemas().set(name, value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Loads a schema and compiles it into a validator.
|
|
42
|
+
* @param logger - A logger for logging information.
|
|
43
|
+
*/
|
|
44
|
+
export const compileAllSchemas = (
|
|
45
|
+
logger: Logger,
|
|
46
|
+
schemaService?: SchemaService
|
|
47
|
+
) => {
|
|
48
|
+
if (!schemaService) {
|
|
49
|
+
throw new Error('SchemaService needs to be defined to load schemas')
|
|
50
|
+
}
|
|
51
|
+
for (const [name, schema] of getSchemas()) {
|
|
52
|
+
schemaService.compileSchema(name, schema)
|
|
53
|
+
}
|
|
54
|
+
validateAllSchemasLoaded(logger, schemaService)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const validateAllSchemasLoaded = (
|
|
58
|
+
logger: Logger,
|
|
59
|
+
schemaService: SchemaService
|
|
60
|
+
) => {
|
|
61
|
+
const { routesMeta } = getRoutes()
|
|
62
|
+
const validators = schemaService.getSchemaNames()
|
|
63
|
+
|
|
64
|
+
const missingSchemas: string[] = []
|
|
65
|
+
|
|
66
|
+
for (const route of routesMeta) {
|
|
67
|
+
if (!route.input || validators.has(route.input)) {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
missingSchemas.push(route.input)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (missingSchemas.length > 0) {
|
|
74
|
+
logger.error(
|
|
75
|
+
`Error: Failed to load schemas:\n.${missingSchemas.join('\n')}`
|
|
76
|
+
)
|
|
77
|
+
logger.error('\tHave you run the schema generation?')
|
|
78
|
+
logger.error('\tnpx @pikku/cli schemas')
|
|
79
|
+
} else {
|
|
80
|
+
logger.info('All schemas loaded')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const coerceQueryStringToArray = (schemaName: string, data: any) => {
|
|
85
|
+
const schema = getSchema(schemaName)
|
|
86
|
+
for (const key in schema.properties) {
|
|
87
|
+
const property = schema.properties[key]
|
|
88
|
+
if (typeof property === 'boolean') {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
const type = property.type
|
|
92
|
+
if (typeof type === 'boolean') {
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if (type === 'array' && typeof data[key] === 'string') {
|
|
96
|
+
data[key] = data[key].split(',')
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const validateAndCoerce = (
|
|
102
|
+
logger: Logger,
|
|
103
|
+
schemaService: SchemaService | undefined,
|
|
104
|
+
schemaName: string | undefined | null,
|
|
105
|
+
data: any,
|
|
106
|
+
coerceToArray: boolean
|
|
107
|
+
) => {
|
|
108
|
+
if (schemaService) {
|
|
109
|
+
if (!schemaName) {
|
|
110
|
+
if (data && (data.length > 0 || Object.keys(data).length > 0)) {
|
|
111
|
+
logger.warn('No schema provided, but data was passed')
|
|
112
|
+
throw new BadRequestError('No data expected')
|
|
113
|
+
} else {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const schema = getSchema(schemaName)
|
|
118
|
+
schemaService.compileSchema(schemaName, schema)
|
|
119
|
+
if (coerceToArray) {
|
|
120
|
+
coerceQueryStringToArray(schemaName, data)
|
|
121
|
+
}
|
|
122
|
+
schemaService.validateSchema(schemaName, data)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for handling content operations.
|
|
3
|
+
*/
|
|
4
|
+
export interface ContentService {
|
|
5
|
+
/**
|
|
6
|
+
* Signs a content key to provide secure access.
|
|
7
|
+
* @param contentKey - The key of the content to sign.
|
|
8
|
+
* @returns A promise that resolves to the signed content key URL.
|
|
9
|
+
*/
|
|
10
|
+
signContentKey: (
|
|
11
|
+
contentKey: string,
|
|
12
|
+
dateLessThan: Date,
|
|
13
|
+
dateGreaterThan?: Date
|
|
14
|
+
) => Promise<string>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Signs a URL to provide secure access.
|
|
18
|
+
* @param url - The URL to sign.
|
|
19
|
+
* @returns A promise that resolves to the signed URL.
|
|
20
|
+
*/
|
|
21
|
+
signURL: (
|
|
22
|
+
url: string,
|
|
23
|
+
dateLessThan: Date,
|
|
24
|
+
dateGreaterThan?: Date
|
|
25
|
+
) => Promise<string>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets an upload URL for a file.
|
|
29
|
+
* @param fileKey - The key of the file to upload.
|
|
30
|
+
* @param contentType - The content type of the file.
|
|
31
|
+
* @returns A promise that resolves to an object containing the upload URL and asset key.
|
|
32
|
+
* This method generates an upload URL for the specified file key and content type, allowing the file to be uploaded.
|
|
33
|
+
*/
|
|
34
|
+
getUploadURL: (
|
|
35
|
+
fileKey: string,
|
|
36
|
+
contentType: string
|
|
37
|
+
) => Promise<{ uploadUrl: string; assetKey: string }>
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Deletes a file.
|
|
41
|
+
* @param fileName - The name of the file to delete.
|
|
42
|
+
* @returns A promise that resolves to a boolean indicating success.
|
|
43
|
+
* This method deletes the specified file and returns a boolean indicating whether the deletion was successful.
|
|
44
|
+
*/
|
|
45
|
+
deleteFile?: (fileName: string) => Promise<boolean>
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Writes a file.
|
|
49
|
+
* @param assetKey - The key of the asset to write.
|
|
50
|
+
* @param buffer - The buffer containing the file data.
|
|
51
|
+
* @returns A promise that resolves to a boolean indicating success.
|
|
52
|
+
*/
|
|
53
|
+
writeFile?: (assetKey: string, buffer: Buffer) => Promise<boolean>
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* This method copies the file from the specified absolute path to the location identified by the asset key and returns a boolean indicating success.
|
|
57
|
+
* @param assetKey - The key of the asset to copy.
|
|
58
|
+
* @param fromAbsolutePath - The absolute path of the source file.
|
|
59
|
+
* @returns A promise that resolves to a boolean indicating success.
|
|
60
|
+
*/
|
|
61
|
+
copyFile?: (assetKey: string, fromAbsolutePath: string) => Promise<boolean>
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reads a file.
|
|
65
|
+
* @param assetKey - The key of the asset to read.
|
|
66
|
+
* @returns A promise that resolves to a buffer containing the file data.
|
|
67
|
+
*/
|
|
68
|
+
readFile?: (assetKey: string) => Promise<Buffer>
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @pikku/core
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Interfaces
|
|
6
|
+
export * from './content-service.js'
|
|
7
|
+
export * from './jwt-service.js'
|
|
8
|
+
export * from './logger.js'
|
|
9
|
+
export * from './logger-console.js'
|
|
10
|
+
export * from './secret-service.js'
|
|
11
|
+
export * from './variables-service.js'
|
|
12
|
+
export * from './schema-service.js'
|
|
13
|
+
|
|
14
|
+
// Local implementations
|
|
15
|
+
export * from './local-secrets.js'
|
|
16
|
+
export * from './local-variables.js'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CoreUserSession } from '../types/core.types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interface for handling JSON Web Tokens (JWT).
|
|
5
|
+
* @template UserSession - The type of the user session.
|
|
6
|
+
*/
|
|
7
|
+
export interface JWTService<UserSession = CoreUserSession> {
|
|
8
|
+
/**
|
|
9
|
+
* Encodes a payload into a JWT.
|
|
10
|
+
* @param expiresIn - The expiration time of the token.
|
|
11
|
+
* @param payload - The payload to encode.
|
|
12
|
+
* @returns A promise that resolves to the encoded JWT.
|
|
13
|
+
*/
|
|
14
|
+
encode: <T extends any>(expiresIn: string, payload: T) => Promise<string>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Decodes a JWT into its payload.
|
|
18
|
+
* @param hash - The JWT to decode.
|
|
19
|
+
* @param invalidHashError - An optional error to throw if the hash is invalid.
|
|
20
|
+
* @param debug - An optional flag for debugging.
|
|
21
|
+
* @returns A promise that resolves to the decoded payload.
|
|
22
|
+
*/
|
|
23
|
+
decode: <T>(
|
|
24
|
+
hash: string,
|
|
25
|
+
invalidHashError?: Error,
|
|
26
|
+
debug?: boolean
|
|
27
|
+
) => Promise<T>
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decodes a user session from a JWT.
|
|
31
|
+
* @param jwtToken - The JWT representing the user session.
|
|
32
|
+
* @param debug - An optional flag for debugging.
|
|
33
|
+
* @returns A promise that resolves to the decoded user session.
|
|
34
|
+
*/
|
|
35
|
+
decodeSession: (jwtToken: string, debug?: any) => Promise<UserSession>
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ContentService } from './content-service.js'
|
|
2
|
+
import { Logger } from './logger.js'
|
|
3
|
+
|
|
4
|
+
export interface LocalContentConfig {
|
|
5
|
+
contentDirectory: string
|
|
6
|
+
assetsUrl: string
|
|
7
|
+
uploadUrl: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Service for handling local content operations.
|
|
12
|
+
*/
|
|
13
|
+
export abstract class LocalContent implements ContentService {
|
|
14
|
+
constructor(_config: LocalContentConfig, _logger: Logger) {}
|
|
15
|
+
|
|
16
|
+
public abstract getUploadURL: (
|
|
17
|
+
fileKey: string,
|
|
18
|
+
contentType: string
|
|
19
|
+
) => Promise<{ uploadUrl: string; assetKey: string }>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initializes the local content service.
|
|
23
|
+
*/
|
|
24
|
+
public abstract init(): Promise<void>
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Signs a URL to provide secure access.
|
|
28
|
+
* @param url - The URL to sign.
|
|
29
|
+
* @returns A promise that resolves to the signed URL.
|
|
30
|
+
*/
|
|
31
|
+
public abstract signURL(url: string): Promise<string>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Signs a content key to provide secure access to an asset.
|
|
35
|
+
* @param assetKey - The key of the asset to sign.
|
|
36
|
+
* @returns A promise that resolves to the signed content key URL.
|
|
37
|
+
*/
|
|
38
|
+
public abstract signContentKey(assetKey: string): Promise<string>
|
|
39
|
+
}
|