@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,87 @@
|
|
|
1
|
+
import { ChannelsMeta, CoreAPIChannel } from './channel/channel.types.js'
|
|
2
|
+
import {
|
|
3
|
+
CoreHTTPFunctionRoute,
|
|
4
|
+
HTTPMethod,
|
|
5
|
+
HTTPRoutesMeta,
|
|
6
|
+
} from './http/http.types.js'
|
|
7
|
+
import { FunctionsMeta, PikkuMiddleware } from './types/core.types.js'
|
|
8
|
+
import {
|
|
9
|
+
CoreScheduledTask,
|
|
10
|
+
ScheduledTasksMeta,
|
|
11
|
+
} from './scheduler/scheduler.types.js'
|
|
12
|
+
import { ErrorDetails, PikkuError } from './errors/error-handler.js'
|
|
13
|
+
import {
|
|
14
|
+
CoreAPIFunction,
|
|
15
|
+
CoreAPIFunctionSessionless,
|
|
16
|
+
} from './function/functions.types.js'
|
|
17
|
+
|
|
18
|
+
interface PikkuState {
|
|
19
|
+
functions: {
|
|
20
|
+
meta: FunctionsMeta
|
|
21
|
+
nameToFunction: Map<
|
|
22
|
+
string,
|
|
23
|
+
CoreAPIFunction<any, any> | CoreAPIFunctionSessionless<any, any>
|
|
24
|
+
>
|
|
25
|
+
}
|
|
26
|
+
http: {
|
|
27
|
+
middleware: Array<{ route: string; middleware: PikkuMiddleware[] }>
|
|
28
|
+
routes: Map<HTTPMethod, Map<string, CoreHTTPFunctionRoute<any, any, any>>>
|
|
29
|
+
meta: HTTPRoutesMeta
|
|
30
|
+
}
|
|
31
|
+
channel: {
|
|
32
|
+
channels: Map<string, CoreAPIChannel<any, any>>
|
|
33
|
+
meta: ChannelsMeta
|
|
34
|
+
}
|
|
35
|
+
scheduler: {
|
|
36
|
+
tasks: Map<string, CoreScheduledTask>
|
|
37
|
+
meta: ScheduledTasksMeta
|
|
38
|
+
}
|
|
39
|
+
misc: {
|
|
40
|
+
errors: Map<PikkuError, ErrorDetails>
|
|
41
|
+
schemas: Map<string, any>
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const resetPikkuState = () => {
|
|
46
|
+
globalThis.pikkuState = {
|
|
47
|
+
functions: {
|
|
48
|
+
meta: {},
|
|
49
|
+
nameToFunction: new Map(),
|
|
50
|
+
},
|
|
51
|
+
http: {
|
|
52
|
+
middleware: [],
|
|
53
|
+
routes: new Map(),
|
|
54
|
+
meta: [],
|
|
55
|
+
},
|
|
56
|
+
channel: {
|
|
57
|
+
channels: new Map(),
|
|
58
|
+
meta: {},
|
|
59
|
+
},
|
|
60
|
+
scheduler: {
|
|
61
|
+
tasks: new Map(),
|
|
62
|
+
meta: [],
|
|
63
|
+
},
|
|
64
|
+
misc: {
|
|
65
|
+
errors: globalThis.pikkuState?.misc?.errors || new Map(),
|
|
66
|
+
schemas: globalThis.pikkuState?.misc?.schema || new Map(),
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!globalThis.pikkuState) {
|
|
72
|
+
resetPikkuState()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const pikkuState = <
|
|
76
|
+
Type extends keyof PikkuState,
|
|
77
|
+
Content extends keyof PikkuState[Type],
|
|
78
|
+
>(
|
|
79
|
+
type: Type,
|
|
80
|
+
content: Content,
|
|
81
|
+
value?: PikkuState[Type][Content]
|
|
82
|
+
): PikkuState[Type][Content] => {
|
|
83
|
+
if (value) {
|
|
84
|
+
globalThis.pikkuState[type][content] = value
|
|
85
|
+
}
|
|
86
|
+
return globalThis.pikkuState[type][content]
|
|
87
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { pikkuState } from '../pikku-state.js'
|
|
2
|
+
import { Logger } from '../services/index.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 = pikkuState('scheduler', 'tasks')
|
|
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,105 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CoreServices,
|
|
3
|
+
CoreSingletonServices,
|
|
4
|
+
CoreUserSession,
|
|
5
|
+
CreateSessionServices,
|
|
6
|
+
} from '../types/core.types.js'
|
|
7
|
+
import type { CoreScheduledTask } from './scheduler.types.js'
|
|
8
|
+
import type { CoreAPIFunctionSessionless } from '../function/functions.types.js'
|
|
9
|
+
import { getErrorResponse } from '../errors/error-handler.js'
|
|
10
|
+
import { closeSessionServices } from '../utils.js'
|
|
11
|
+
import { pikkuState } from '../pikku-state.js'
|
|
12
|
+
import { addFunction, runPikkuFunc } from '../function/function-runner.js'
|
|
13
|
+
|
|
14
|
+
export type RunScheduledTasksParams = {
|
|
15
|
+
name: string
|
|
16
|
+
session?: CoreUserSession
|
|
17
|
+
singletonServices: CoreSingletonServices
|
|
18
|
+
createSessionServices?: CreateSessionServices<
|
|
19
|
+
CoreSingletonServices,
|
|
20
|
+
CoreServices<CoreSingletonServices>,
|
|
21
|
+
CoreUserSession
|
|
22
|
+
>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const addScheduledTask = <
|
|
26
|
+
APIFunction extends CoreAPIFunctionSessionless<void, void>,
|
|
27
|
+
>(
|
|
28
|
+
scheduledTask: CoreScheduledTask<APIFunction>
|
|
29
|
+
) => {
|
|
30
|
+
const meta = pikkuState('scheduler', 'meta')
|
|
31
|
+
const taskMeta = meta[scheduledTask.name]
|
|
32
|
+
if (!taskMeta) {
|
|
33
|
+
throw new Error('Task metadata not found')
|
|
34
|
+
}
|
|
35
|
+
addFunction(taskMeta.pikkuFuncName, scheduledTask.func)
|
|
36
|
+
|
|
37
|
+
const tasks = pikkuState('scheduler', 'tasks')
|
|
38
|
+
if (tasks.has(scheduledTask.name)) {
|
|
39
|
+
throw new Error(`Scheduled task already exists: ${scheduledTask.name}`)
|
|
40
|
+
}
|
|
41
|
+
tasks.set(scheduledTask.name, scheduledTask)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class ScheduledTaskNotFoundError extends Error {
|
|
45
|
+
constructor(title: string) {
|
|
46
|
+
super(`Scheduled task not found: ${title}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function runScheduledTask<
|
|
51
|
+
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
52
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
53
|
+
>({
|
|
54
|
+
name,
|
|
55
|
+
session,
|
|
56
|
+
singletonServices,
|
|
57
|
+
createSessionServices,
|
|
58
|
+
}: RunScheduledTasksParams): Promise<void> {
|
|
59
|
+
let sessionServices: CoreServices | undefined
|
|
60
|
+
try {
|
|
61
|
+
const task = pikkuState('scheduler', 'tasks').get(name)
|
|
62
|
+
const meta = pikkuState('scheduler', 'meta')[name]
|
|
63
|
+
if (!task) {
|
|
64
|
+
throw new ScheduledTaskNotFoundError(`Scheduled task not found: ${name}`)
|
|
65
|
+
}
|
|
66
|
+
if (!meta) {
|
|
67
|
+
throw new ScheduledTaskNotFoundError(
|
|
68
|
+
`Scheduled task meta not found: ${name}`
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
singletonServices.logger.info(
|
|
73
|
+
`Running schedule task: ${name} | schedule: ${task.schedule}`
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const getAllServices = async () => {
|
|
77
|
+
if (createSessionServices) {
|
|
78
|
+
const sessionServices = await createSessionServices(
|
|
79
|
+
singletonServices,
|
|
80
|
+
{},
|
|
81
|
+
session
|
|
82
|
+
)
|
|
83
|
+
return { ...singletonServices, ...sessionServices }
|
|
84
|
+
}
|
|
85
|
+
return singletonServices
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return await runPikkuFunc(meta.pikkuFuncName, {
|
|
89
|
+
singletonServices,
|
|
90
|
+
getAllServices,
|
|
91
|
+
session,
|
|
92
|
+
data: undefined,
|
|
93
|
+
})
|
|
94
|
+
} catch (e: any) {
|
|
95
|
+
const errorResponse = getErrorResponse(e)
|
|
96
|
+
if (errorResponse != null) {
|
|
97
|
+
singletonServices.logger.error(e)
|
|
98
|
+
}
|
|
99
|
+
throw e
|
|
100
|
+
} finally {
|
|
101
|
+
if (sessionServices) {
|
|
102
|
+
await closeSessionServices(singletonServices.logger, sessionServices)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { APIDocs, CoreUserSession } from '../types/core.types.js'
|
|
2
|
+
import { CoreAPIFunctionSessionless } from '../function/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
|
+
Record<
|
|
9
|
+
string,
|
|
10
|
+
{
|
|
11
|
+
pikkuFuncName: string
|
|
12
|
+
name: string
|
|
13
|
+
schedule: string
|
|
14
|
+
session?: UserSession
|
|
15
|
+
docs?: APIDocs
|
|
16
|
+
tags?: string[]
|
|
17
|
+
}
|
|
18
|
+
>
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Represents a core scheduled task.
|
|
22
|
+
*/
|
|
23
|
+
export type CoreScheduledTask<
|
|
24
|
+
APIFunction = CoreAPIFunctionSessionless<void, void>,
|
|
25
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
26
|
+
> = {
|
|
27
|
+
name: string
|
|
28
|
+
schedule: string
|
|
29
|
+
func: APIFunction
|
|
30
|
+
docs?: APIDocs
|
|
31
|
+
session?: UserSession
|
|
32
|
+
tags?: string[]
|
|
33
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { test, describe, before } from 'node:test'
|
|
2
|
+
import * as assert from 'assert'
|
|
3
|
+
import { addSchema, coerceTopLevelDataFromSchema } 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
|
+
coerceTopLevelDataFromSchema('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
|
+
coerceTopLevelDataFromSchema('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
|
+
coerceTopLevelDataFromSchema('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
|
+
coerceTopLevelDataFromSchema('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
|
+
coerceTopLevelDataFromSchema('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,99 @@
|
|
|
1
|
+
import { Logger } from './services/logger.js'
|
|
2
|
+
import { SchemaService } from './services/schema-service.js'
|
|
3
|
+
import { UnprocessableContentError } from './errors/errors.js'
|
|
4
|
+
import { pikkuState } from './pikku-state.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Adds a schema to the global schemas map.
|
|
8
|
+
* @param name - The name of the schema.
|
|
9
|
+
* @param value - The schema value.
|
|
10
|
+
* @ignore
|
|
11
|
+
*/
|
|
12
|
+
export const addSchema = (name: string, value: any) => {
|
|
13
|
+
pikkuState('misc', 'schemas').set(name, value)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Loads a schema and compiles it into a validator.
|
|
18
|
+
* @param logger - A logger for logging information.
|
|
19
|
+
*/
|
|
20
|
+
export const compileAllSchemas = (
|
|
21
|
+
logger: Logger,
|
|
22
|
+
schemaService?: SchemaService
|
|
23
|
+
) => {
|
|
24
|
+
if (!schemaService) {
|
|
25
|
+
throw new Error('SchemaService needs to be defined to load schemas')
|
|
26
|
+
}
|
|
27
|
+
const schemas = pikkuState('misc', 'schemas')
|
|
28
|
+
for (const [name, schema] of schemas) {
|
|
29
|
+
schemaService.compileSchema(name, schema)
|
|
30
|
+
}
|
|
31
|
+
validateAllSchemasLoaded(logger, schemaService)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const validateAllSchemasLoaded = (
|
|
35
|
+
logger: Logger,
|
|
36
|
+
schemaService: SchemaService
|
|
37
|
+
) => {
|
|
38
|
+
const routesMeta = pikkuState('http', 'meta')
|
|
39
|
+
const validators = schemaService.getSchemaNames()
|
|
40
|
+
|
|
41
|
+
const missingSchemas: string[] = []
|
|
42
|
+
|
|
43
|
+
for (const route of routesMeta) {
|
|
44
|
+
if (!route.input || validators.has(route.input)) {
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
missingSchemas.push(route.input)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (missingSchemas.length > 0) {
|
|
51
|
+
logger.error(
|
|
52
|
+
`Error: Failed to load schemas:\n.${missingSchemas.join('\n')}`
|
|
53
|
+
)
|
|
54
|
+
logger.error('\tHave you run the schema generation?')
|
|
55
|
+
logger.error('\tnpx @pikku/cli schemas')
|
|
56
|
+
} else {
|
|
57
|
+
logger.info('All schemas loaded')
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const coerceTopLevelDataFromSchema = (schemaName: string, data: any) => {
|
|
62
|
+
const schema = pikkuState('misc', 'schemas').get(schemaName)
|
|
63
|
+
for (const key in schema.properties) {
|
|
64
|
+
const property = schema.properties[key]
|
|
65
|
+
if (typeof property === 'boolean') {
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
const type = property.type
|
|
69
|
+
if (typeof type === 'boolean') {
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (type === 'array' && typeof data[key] === 'string') {
|
|
73
|
+
data[key] = data[key].split(',')
|
|
74
|
+
} else if (type === 'string' && property.format === 'date-time') {
|
|
75
|
+
data[key] = new Date(data[key])
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const validateSchema = async (
|
|
81
|
+
logger: Logger,
|
|
82
|
+
schemaService: SchemaService | undefined,
|
|
83
|
+
schemaName: string | undefined | null,
|
|
84
|
+
data: any
|
|
85
|
+
) => {
|
|
86
|
+
if (schemaService) {
|
|
87
|
+
if (!schemaName) {
|
|
88
|
+
if (data && (data.length > 0 || Object.keys(data).length > 0)) {
|
|
89
|
+
logger.warn('No schema provided, but data was passed')
|
|
90
|
+
throw new UnprocessableContentError('No data expected')
|
|
91
|
+
} else {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const schema = pikkuState('misc', 'schemas').get(schemaName)
|
|
96
|
+
await schemaService.compileSchema(schemaName, schema)
|
|
97
|
+
await schemaService.validateSchema(schemaName, data)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ReadStream } from 'fs'
|
|
2
|
+
|
|
3
|
+
export interface ContentService {
|
|
4
|
+
/**
|
|
5
|
+
* Signs a content key to generate a secure, time-limited access URL.
|
|
6
|
+
* @param contentKey - The key representing the content object.
|
|
7
|
+
* @param dateLessThan - The expiration time for the signed URL.
|
|
8
|
+
* @param dateGreaterThan - (Optional) Start time before which access is denied.
|
|
9
|
+
*/
|
|
10
|
+
signContentKey(
|
|
11
|
+
contentKey: string,
|
|
12
|
+
dateLessThan: Date,
|
|
13
|
+
dateGreaterThan?: Date
|
|
14
|
+
): Promise<string>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Signs an arbitrary URL to generate a secure, time-limited access URL.
|
|
18
|
+
* @param url - The full URL that needs signing.
|
|
19
|
+
* @param dateLessThan - The expiration time for the signed URL.
|
|
20
|
+
* @param dateGreaterThan - (Optional) Start time before which access is denied.
|
|
21
|
+
*/
|
|
22
|
+
signURL(
|
|
23
|
+
url: string,
|
|
24
|
+
dateLessThan: Date,
|
|
25
|
+
dateGreaterThan?: Date
|
|
26
|
+
): Promise<string>
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generates a signed URL for uploading a file directly to storage.
|
|
30
|
+
* @param fileKey - The desired key/location of the uploaded file.
|
|
31
|
+
* @param contentType - The MIME type of the file.
|
|
32
|
+
* @returns A signed upload URL and the finalized asset key.
|
|
33
|
+
*/
|
|
34
|
+
getUploadURL(
|
|
35
|
+
fileKey: string,
|
|
36
|
+
contentType: string
|
|
37
|
+
): Promise<{ uploadUrl: string; assetKey: string }>
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Deletes a file from the storage backend.
|
|
41
|
+
* @param fileName - The name or key of the file to delete.
|
|
42
|
+
* @returns A boolean indicating success.
|
|
43
|
+
*/
|
|
44
|
+
deleteFile(fileName: string): Promise<boolean>
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Uploads a file stream to storage under a specified asset key.
|
|
48
|
+
* @param assetKey - The key where the file will be saved.
|
|
49
|
+
* @param stream - A readable stream of the file contents.
|
|
50
|
+
* @returns A boolean indicating success.
|
|
51
|
+
*/
|
|
52
|
+
writeFile(assetKey: string, stream: ReadStream): Promise<boolean>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Copies a file from a local absolute path into storage under a new asset key.
|
|
56
|
+
* @param assetKey - The destination key.
|
|
57
|
+
* @param fromAbsolutePath - The local absolute file path.
|
|
58
|
+
* @returns A boolean indicating success.
|
|
59
|
+
*/
|
|
60
|
+
copyFile(assetKey: string, fromAbsolutePath: string): Promise<boolean>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reads a file from storage as a readable stream.
|
|
64
|
+
* @param assetKey - The key of the file to read.
|
|
65
|
+
* @returns A readable file stream.
|
|
66
|
+
*/
|
|
67
|
+
readFile(assetKey: string): Promise<ReadStream>
|
|
68
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
export * from './user-session-service.js'
|
|
14
|
+
export * from './local-content.js'
|
|
15
|
+
|
|
16
|
+
// Local implementations
|
|
17
|
+
export * from './local-secrets.js'
|
|
18
|
+
export * from './local-variables.js'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RelativeTimeInput } from '../time-utils.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interface for handling JSON Web Tokens (JWT).
|
|
5
|
+
*/
|
|
6
|
+
export interface JWTService {
|
|
7
|
+
/**
|
|
8
|
+
* Encodes a payload into a JWT.
|
|
9
|
+
* @param expiresIn - The expiration time of the token.
|
|
10
|
+
* @param payload - The payload to encode.
|
|
11
|
+
* @returns A promise that resolves to the encoded JWT.
|
|
12
|
+
*/
|
|
13
|
+
encode: <T extends any>(
|
|
14
|
+
expiresIn: RelativeTimeInput,
|
|
15
|
+
payload: T
|
|
16
|
+
) => Promise<string>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Decodes a JWT into its payload.
|
|
20
|
+
* @param hash - The JWT to decode.
|
|
21
|
+
* @param invalidHashError - An optional error to throw if the hash is invalid.
|
|
22
|
+
* @param debug - An optional flag for debugging.
|
|
23
|
+
* @returns A promise that resolves to the decoded payload.
|
|
24
|
+
*/
|
|
25
|
+
decode: <T>(
|
|
26
|
+
hash: string,
|
|
27
|
+
invalidHashError?: Error,
|
|
28
|
+
debug?: boolean
|
|
29
|
+
) => Promise<T>
|
|
30
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream, promises, ReadStream } from 'fs'
|
|
2
|
+
import { mkdir } from 'fs/promises'
|
|
3
|
+
import { ContentService, Logger } from '@pikku/core/services'
|
|
4
|
+
import { pipeline } from 'stream/promises'
|
|
5
|
+
import { Readable } from 'stream'
|
|
6
|
+
|
|
7
|
+
export interface LocalContentConfig {
|
|
8
|
+
localFileUploadPath: string
|
|
9
|
+
uploadUrlPrefix: string
|
|
10
|
+
assetUrlPrefix: string
|
|
11
|
+
sizeLimit?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class LocalContent implements ContentService {
|
|
15
|
+
constructor(
|
|
16
|
+
private config: LocalContentConfig,
|
|
17
|
+
private logger: Logger
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
public async init() {}
|
|
21
|
+
|
|
22
|
+
public async signURL(url: string): Promise<string> {
|
|
23
|
+
return `${url}?signed=true`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async signContentKey(assetKey: string): Promise<string> {
|
|
27
|
+
return `${this.config.assetUrlPrefix}/${assetKey}?signed=true`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async getUploadURL(assetKey: string) {
|
|
31
|
+
this.logger.debug(`Going to upload with key: ${assetKey}`)
|
|
32
|
+
return {
|
|
33
|
+
uploadUrl: `${this.config.uploadUrlPrefix}/${assetKey}`,
|
|
34
|
+
assetKey,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public async writeFile(assetKey: string, stream: Readable): Promise<boolean> {
|
|
39
|
+
this.logger.debug(`Writing file: ${assetKey}`)
|
|
40
|
+
|
|
41
|
+
const path = `${this.config.localFileUploadPath}/${assetKey}`
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await this.createDirectoryForFile(path)
|
|
45
|
+
const fileStream = createWriteStream(path)
|
|
46
|
+
// Use pipeline to properly manage stream piping and errors
|
|
47
|
+
await pipeline(stream, fileStream)
|
|
48
|
+
return true
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error(e)
|
|
51
|
+
this.logger.error(`Error writing content ${assetKey}`, e)
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public async copyFile(
|
|
57
|
+
assetKey: string,
|
|
58
|
+
fromAbsolutePath: string
|
|
59
|
+
): Promise<boolean> {
|
|
60
|
+
this.logger.debug(`Writing file: ${assetKey}`)
|
|
61
|
+
try {
|
|
62
|
+
const path = `${this.config.localFileUploadPath}/${assetKey}`
|
|
63
|
+
await this.createDirectoryForFile(path)
|
|
64
|
+
await promises.copyFile(fromAbsolutePath, path)
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.error(e)
|
|
67
|
+
this.logger.error(`Error inserting content ${assetKey}`, e)
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public async readFile(assetKey: string): Promise<ReadStream> {
|
|
73
|
+
this.logger.debug(`Getting key: ${assetKey}`)
|
|
74
|
+
|
|
75
|
+
const filePath = `${this.config.localFileUploadPath}/${assetKey}`
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const stream = createReadStream(filePath)
|
|
79
|
+
// Handle early stream errors (like file not found, permission denied, etc.)
|
|
80
|
+
stream.on('error', (err) => {
|
|
81
|
+
this.logger.error(`Error getting content ${assetKey}`, err)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return stream
|
|
85
|
+
} catch (e) {
|
|
86
|
+
this.logger.error(`Error setting up stream for ${assetKey}`, e)
|
|
87
|
+
throw e
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public async deleteFile(assetKey: string): Promise<boolean> {
|
|
92
|
+
this.logger.debug(`deleting key: ${assetKey}`)
|
|
93
|
+
try {
|
|
94
|
+
await promises.unlink(`${this.config.localFileUploadPath}/${assetKey}`)
|
|
95
|
+
} catch (e: any) {
|
|
96
|
+
this.logger.error(`Error deleting content ${assetKey}`, e)
|
|
97
|
+
}
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async createDirectoryForFile(path: string): Promise<void> {
|
|
102
|
+
const dir = path.split('/').slice(0, -1).join('/')
|
|
103
|
+
await mkdir(dir, { recursive: true })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { LocalVariablesService } from './local-variables.js'
|
|
2
|
+
import { SecretService } from './secret-service.js'
|
|
3
|
+
import { VariablesService } from './variables-service.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Service for retrieving secrets from environment variables.
|
|
7
|
+
*/
|
|
8
|
+
export class LocalSecretService implements SecretService {
|
|
9
|
+
/**
|
|
10
|
+
* Creates an instance of LocalSecretService.
|
|
11
|
+
*/
|
|
12
|
+
constructor(
|
|
13
|
+
private variables: VariablesService = new LocalVariablesService()
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Retrieves a secret by key.
|
|
18
|
+
* @param key - The key of the secret to retrieve.
|
|
19
|
+
* @returns A promise that resolves to the secret value.
|
|
20
|
+
* @throws {Error} If the secret is not found.
|
|
21
|
+
*/
|
|
22
|
+
public async getSecretJSON<R>(key: string): Promise<R> {
|
|
23
|
+
const value = await this.variables.get(key)
|
|
24
|
+
if (value) {
|
|
25
|
+
return JSON.parse(value)
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Secret Not Found: ${key}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Retrieves a secret by key.
|
|
32
|
+
* @param key - The key of the secret to retrieve.
|
|
33
|
+
* @returns A promise that resolves to the secret value.
|
|
34
|
+
* @throws {Error} If the secret is not found.
|
|
35
|
+
*/
|
|
36
|
+
public async getSecret(key: string): Promise<string> {
|
|
37
|
+
const value = await this.variables.get(key)
|
|
38
|
+
if (value) {
|
|
39
|
+
return value
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Secret Not Found: ${key}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { VariablesService } from './variables-service.js'
|
|
2
|
+
|
|
3
|
+
export class LocalVariablesService implements VariablesService {
|
|
4
|
+
constructor(
|
|
5
|
+
private variables: Record<string, string | undefined> = process.env
|
|
6
|
+
) {}
|
|
7
|
+
|
|
8
|
+
public getAll():
|
|
9
|
+
| Promise<Record<string, string | undefined>>
|
|
10
|
+
| Record<string, string | undefined> {
|
|
11
|
+
return this.variables || {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public get(name: string): Promise<string | undefined> | string | undefined {
|
|
15
|
+
return this.variables[name]
|
|
16
|
+
}
|
|
17
|
+
}
|