@owlmeans/server-api 0.1.0
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/LICENSE +21 -0
- package/README.md +613 -0
- package/build/.gitkeep +0 -0
- package/build/consts.d.ts +5 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +5 -0
- package/build/consts.js.map +1 -0
- package/build/errors.d.ts +14 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +27 -0
- package/build/errors.js.map +1 -0
- package/build/helper.d.ts +13 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +53 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/build/server.d.ts +7 -0
- package/build/server.d.ts.map +1 -0
- package/build/server.js +146 -0
- package/build/server.js.map +1 -0
- package/build/types.d.ts +21 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/context.d.ts +4 -0
- package/build/utils/context.d.ts.map +1 -0
- package/build/utils/context.js +5 -0
- package/build/utils/context.js.map +1 -0
- package/build/utils/error.d.ts +3 -0
- package/build/utils/error.d.ts.map +1 -0
- package/build/utils/error.js +16 -0
- package/build/utils/error.js.map +1 -0
- package/build/utils/guards.d.ts +9 -0
- package/build/utils/guards.d.ts.map +1 -0
- package/build/utils/guards.js +57 -0
- package/build/utils/guards.js.map +1 -0
- package/build/utils/index.d.ts +7 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +7 -0
- package/build/utils/index.js.map +1 -0
- package/build/utils/payload.d.ts +10 -0
- package/build/utils/payload.d.ts.map +1 -0
- package/build/utils/payload.js +55 -0
- package/build/utils/payload.js.map +1 -0
- package/build/utils/server.d.ts +10 -0
- package/build/utils/server.d.ts.map +1 -0
- package/build/utils/server.js +56 -0
- package/build/utils/server.js.map +1 -0
- package/package.json +62 -0
- package/src/consts.ts +7 -0
- package/src/errors.ts +35 -0
- package/src/helper.ts +89 -0
- package/src/index.ts +6 -0
- package/src/server.ts +183 -0
- package/src/types.ts +26 -0
- package/src/utils/context.ts +9 -0
- package/src/utils/error.ts +18 -0
- package/src/utils/guards.ts +75 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/payload.ts +62 -0
- package/src/utils/server.ts +70 -0
- package/tsconfig.json +15 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/utils/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/server-api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc -b",
|
|
7
|
+
"dev": "sleep 264 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
8
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty"
|
|
9
|
+
},
|
|
10
|
+
"main": "build/index.js",
|
|
11
|
+
"module": "build/index.js",
|
|
12
|
+
"types": "build/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./build/index.js",
|
|
16
|
+
"require": "./build/index.js",
|
|
17
|
+
"default": "./build/index.js",
|
|
18
|
+
"module": "./build/index.js",
|
|
19
|
+
"types": "./build/index.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./utils": {
|
|
22
|
+
"import": "./build/utils/index.js",
|
|
23
|
+
"require": "./build/utils/index.js",
|
|
24
|
+
"default": "./build/utils/index.js",
|
|
25
|
+
"module": "./build/utils/index.js",
|
|
26
|
+
"types": "./build/utils/index.d.ts"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@fastify/cors": "^10.0.1",
|
|
31
|
+
"@fastify/helmet": "^12.0.1",
|
|
32
|
+
"@fastify/middie": "^9.0.2",
|
|
33
|
+
"@fastify/multipart": "^9.0.1",
|
|
34
|
+
"@owlmeans/api": "^0.1.0",
|
|
35
|
+
"@owlmeans/auth": "^0.1.0",
|
|
36
|
+
"@owlmeans/auth-common": "^0.1.0",
|
|
37
|
+
"@owlmeans/context": "^0.1.0",
|
|
38
|
+
"@owlmeans/error": "^0.1.0",
|
|
39
|
+
"@owlmeans/module": "^0.1.0",
|
|
40
|
+
"@owlmeans/route": "^0.1.0",
|
|
41
|
+
"@owlmeans/server-context": "^0.1.0",
|
|
42
|
+
"@owlmeans/server-module": "^0.1.0",
|
|
43
|
+
"ajv-errors": "^3.0.0",
|
|
44
|
+
"fastify": "^5.0.0",
|
|
45
|
+
"fastify-raw-body": "^5.0.0",
|
|
46
|
+
"pino-pretty": "^13.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"ajv": "*",
|
|
50
|
+
"ajv-formats": "*"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.7.8",
|
|
54
|
+
"nodemon": "^3.1.7",
|
|
55
|
+
"npm-check": "^6.0.1",
|
|
56
|
+
"typescript": "^5.6.3"
|
|
57
|
+
},
|
|
58
|
+
"private": false,
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/consts.ts
ADDED
package/src/errors.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
import { ApiError } from '@owlmeans/api'
|
|
3
|
+
import { ResilientError } from '@owlmeans/error'
|
|
4
|
+
|
|
5
|
+
export class AuthFailedError extends ApiError {
|
|
6
|
+
public static override typeName = 'AuthFailedError'
|
|
7
|
+
|
|
8
|
+
constructor(message: string = 'error') {
|
|
9
|
+
super(`auth:${message}`)
|
|
10
|
+
this.type = AuthFailedError.typeName
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class AccessError extends ApiError {
|
|
15
|
+
public static override typeName = `Access${AuthFailedError.typeName}`
|
|
16
|
+
|
|
17
|
+
constructor(message: string = 'error') {
|
|
18
|
+
super(`access:${message}`)
|
|
19
|
+
this.type = AccessError.typeName
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class NoFileError extends ApiError {
|
|
24
|
+
public static override typeName = 'NoFileError'
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
super('no file')
|
|
28
|
+
this.type = NoFileError.typeName
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ResilientError.registerErrorClass(AuthFailedError)
|
|
33
|
+
ResilientError.registerErrorClass(AccessError)
|
|
34
|
+
ResilientError.registerErrorClass(NoFileError)
|
|
35
|
+
|
package/src/helper.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
|
|
2
|
+
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
3
|
+
import { assertContext } from '@owlmeans/context'
|
|
4
|
+
import { ModuleOutcome } from '@owlmeans/module'
|
|
5
|
+
import type { AbstractRequest, AbstractResponse } from '@owlmeans/module'
|
|
6
|
+
import type { RefedModuleHandler } from '@owlmeans/server-module'
|
|
7
|
+
import type { Config, Context } from './types.js'
|
|
8
|
+
import type { FastifyRequest } from 'fastify'
|
|
9
|
+
import type { MultipartFile } from '@fastify/multipart'
|
|
10
|
+
|
|
11
|
+
const _castContextFromOriginal = <C extends BasicConfig, T extends BasicContext<C> = BasicContext<C>>(req: AbstractRequest, def: T): T => {
|
|
12
|
+
return req.original._ctx ?? def
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const handleBody: <T>(
|
|
16
|
+
handler: (payload: T, ctx: BasicContext<BasicConfig>, req: AbstractRequest) => Promise<any>
|
|
17
|
+
) => RefedModuleHandler<AbstractResponse<any>> = handler => ref => async (req, res) => {
|
|
18
|
+
const ctx = assertContext(ref.ref?.ctx) as Context
|
|
19
|
+
try {
|
|
20
|
+
res.resolve(await handler(
|
|
21
|
+
req.body as any,
|
|
22
|
+
_castContextFromOriginal<Config, Context>(req, ctx) as BasicContext<BasicConfig>,
|
|
23
|
+
req,
|
|
24
|
+
), ModuleOutcome.Ok)
|
|
25
|
+
} catch (e) {
|
|
26
|
+
res.reject(e as Error)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return res.value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const handleParams: <T>(
|
|
33
|
+
handler: (payload: T, ctx: BasicContext<BasicConfig>, req: AbstractRequest) => Promise<any>
|
|
34
|
+
// @TODO Here and everywher it looks like AbstractResponse is messed up here instead of abstract request
|
|
35
|
+
) => RefedModuleHandler<AbstractResponse<any>> = handler => ref => async (req, res) => {
|
|
36
|
+
const ctx = assertContext(ref.ref?.ctx) as Context
|
|
37
|
+
try {
|
|
38
|
+
res.resolve(await handler(
|
|
39
|
+
req.params as any,
|
|
40
|
+
_castContextFromOriginal<Config, Context>(req, ctx) as BasicContext<BasicConfig>,
|
|
41
|
+
req,
|
|
42
|
+
), ModuleOutcome.Ok)
|
|
43
|
+
} catch (e) {
|
|
44
|
+
res.reject(e as Error)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return res.value
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const handleRequest: (
|
|
51
|
+
handler: (payload: AbstractRequest, ctx: BasicContext<BasicConfig>, res?: AbstractResponse<any>) => Promise<any>
|
|
52
|
+
) => RefedModuleHandler<AbstractResponse<any>> = handler => ref => async (req, res) => {
|
|
53
|
+
const ctx = assertContext(ref.ref?.ctx) as Context
|
|
54
|
+
try {
|
|
55
|
+
res.resolve(await handler(
|
|
56
|
+
req, _castContextFromOriginal<Config, Context>(req, ctx) as BasicContext<BasicConfig>, res
|
|
57
|
+
), ModuleOutcome.Ok)
|
|
58
|
+
} catch (e) {
|
|
59
|
+
res.reject(e as Error)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return res.value
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const handleIntermediate: (
|
|
66
|
+
handler: (payload: AbstractRequest, ctx: BasicContext<BasicConfig>) => Promise<BasicContext<BasicConfig> | null>
|
|
67
|
+
) => RefedModuleHandler<AbstractResponse<Context | null>> = handler => ref => async (req, res) => {
|
|
68
|
+
const ctx = assertContext(ref.ref?.ctx) as Context
|
|
69
|
+
try {
|
|
70
|
+
const result = await handler(
|
|
71
|
+
req, _castContextFromOriginal<Config, Context>(req, ctx) as BasicContext<BasicConfig>
|
|
72
|
+
)
|
|
73
|
+
if (result != null) {
|
|
74
|
+
res.resolve(result)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return res.value
|
|
78
|
+
} catch (e) {
|
|
79
|
+
res.reject(e as Error)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const extractUploadedFile = async <T extends {} = {}>(req: AbstractRequest<T>): Promise<UploadedFile | undefined> => {
|
|
84
|
+
const request = req.original as FastifyRequest
|
|
85
|
+
|
|
86
|
+
return request.file()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface UploadedFile extends MultipartFile { }
|
package/src/index.ts
ADDED
package/src/server.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
2
|
+
import type { ApiServer, ApiServerAppend } from './types.js'
|
|
3
|
+
import { Layer, assertContext, createService } from '@owlmeans/context'
|
|
4
|
+
import { canServeModule, executeResponse, provideRequest } from './utils/index.js'
|
|
5
|
+
import { DEFAULT_ALIAS, CLOSED_HOST, PORT, OPENED_HOST } from './consts.js'
|
|
6
|
+
import type { ServerModule } from '@owlmeans/server-module'
|
|
7
|
+
import { RouteMethod } from '@owlmeans/route'
|
|
8
|
+
import { createServerHandler, fixFormatDates } from './utils/index.js'
|
|
9
|
+
import { provideResponse } from '@owlmeans/module'
|
|
10
|
+
import { TOKEN_UPDATE } from '@owlmeans/auth-common'
|
|
11
|
+
|
|
12
|
+
import Fastify from 'fastify'
|
|
13
|
+
import type { FastifyRequest } from 'fastify'
|
|
14
|
+
import cors from '@fastify/cors'
|
|
15
|
+
import rawBody from 'fastify-raw-body'
|
|
16
|
+
import Middie from '@fastify/middie'
|
|
17
|
+
import Helmet from '@fastify/helmet'
|
|
18
|
+
import Multipart from '@fastify/multipart'
|
|
19
|
+
|
|
20
|
+
import formatsPlugin from 'ajv-formats'
|
|
21
|
+
import Ajv from 'ajv'
|
|
22
|
+
import ajvErrors from "ajv-errors"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const ajv = new Ajv({
|
|
26
|
+
removeAdditional: true,
|
|
27
|
+
useDefaults: true,
|
|
28
|
+
coerceTypes: true,
|
|
29
|
+
allErrors: true,
|
|
30
|
+
strict: false
|
|
31
|
+
})
|
|
32
|
+
formatsPlugin(ajv)
|
|
33
|
+
ajvErrors(ajv, { singleError: true })
|
|
34
|
+
|
|
35
|
+
type Config = ServerConfig
|
|
36
|
+
type Context = ServerContext<Config>
|
|
37
|
+
|
|
38
|
+
export const createApiServer = (alias: string): ApiServer => {
|
|
39
|
+
const location = `service:${alias}`
|
|
40
|
+
const _assertContext = (context: Context | undefined): Context => assertContext<Config, Context>(context, location)
|
|
41
|
+
|
|
42
|
+
const service = createService<ApiServer>(alias, {
|
|
43
|
+
server: Fastify({
|
|
44
|
+
logger: true,
|
|
45
|
+
/*{
|
|
46
|
+
transport: {
|
|
47
|
+
target: 'pino-pretty',
|
|
48
|
+
options: {
|
|
49
|
+
singleLine: true,
|
|
50
|
+
translateTime: 'HH:MM:ss Z',
|
|
51
|
+
ignore: 'pid,hostname',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}*/
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
layers: [Layer.System],
|
|
58
|
+
|
|
59
|
+
listen: async () => {
|
|
60
|
+
const context = _assertContext(service.ctx as Context)
|
|
61
|
+
|
|
62
|
+
const config = context.cfg.services[context.cfg.service]
|
|
63
|
+
|
|
64
|
+
const port = config?.internalPort ?? config?.port ?? PORT
|
|
65
|
+
const host = config.opened === true ? OPENED_HOST : CLOSED_HOST
|
|
66
|
+
await service.server.listen({ port, host })
|
|
67
|
+
console.info(`${location}: server listening on ${host}${port != null ? `:${port}` : ''}`)
|
|
68
|
+
|
|
69
|
+
process.on('SIGTERM', () => {
|
|
70
|
+
service.server.close().then(() => {
|
|
71
|
+
process.exit(0)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}, service => async () => {
|
|
76
|
+
if (service.server.server.listening) {
|
|
77
|
+
await service.server.close()
|
|
78
|
+
service.server = Fastify({ logger: true })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const context = _assertContext(service.ctx as Context)
|
|
82
|
+
|
|
83
|
+
const server = service.server
|
|
84
|
+
// @TODO We should ensure some way that Resilient Error is thrown and go to the flow
|
|
85
|
+
server.setValidatorCompiler(opts => ajv.compile(opts.schema))
|
|
86
|
+
// @TODO It's quite unsafe and should be properly configured
|
|
87
|
+
await server.register(cors, {
|
|
88
|
+
origin: '*',
|
|
89
|
+
exposedHeaders: [TOKEN_UPDATE]
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
await server.register(Multipart, {
|
|
93
|
+
throwFileSizeLimit: true,
|
|
94
|
+
// @TODO It shouldn't be this way, becaues the file is buffer this way
|
|
95
|
+
attachFieldsToBody: 'keyValues',
|
|
96
|
+
limits: {
|
|
97
|
+
fileSize: 5 * 1024 * 1024,
|
|
98
|
+
files: 5,
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
await server.register(Helmet)
|
|
102
|
+
await server.register(rawBody, { field: 'rawBody', global: true, runFirst: true })
|
|
103
|
+
await server.register(Middie)
|
|
104
|
+
|
|
105
|
+
server.addHook('preHandler', async (request, reply) => {
|
|
106
|
+
const context = _assertContext(service.ctx as Context);
|
|
107
|
+
// We pass context further using fastify request object
|
|
108
|
+
(request as any)._ctx = await context.modules<ServerModule<FastifyRequest>>().filter(
|
|
109
|
+
module => canServeModule(context, module) && module.route.isIntermediate()
|
|
110
|
+
).reduce<Promise<Context>>(async (promise, module) => {
|
|
111
|
+
let context = await promise
|
|
112
|
+
if (reply.sent) {
|
|
113
|
+
return context
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// await module.route.resolve(context as any)
|
|
117
|
+
await module.resolve()
|
|
118
|
+
// Actually intermediate module can be created without handler by elevate function
|
|
119
|
+
if (module.route.match(request) && module.handle != null) {
|
|
120
|
+
const response = provideResponse(reply)
|
|
121
|
+
const currentRequest = provideRequest(module.alias, request, true)
|
|
122
|
+
currentRequest.original._ctx = context
|
|
123
|
+
const result: Context = await module.handle(currentRequest, response)
|
|
124
|
+
if (result != null) {
|
|
125
|
+
context = result
|
|
126
|
+
}
|
|
127
|
+
executeResponse(response, reply, true)
|
|
128
|
+
}
|
|
129
|
+
return context
|
|
130
|
+
}, Promise.resolve(context))
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
await Promise.all(
|
|
134
|
+
context.modules<ServerModule<FastifyRequest>>()
|
|
135
|
+
.filter(module => canServeModule(context, module) && !module.route.isIntermediate())
|
|
136
|
+
.map(async module => {
|
|
137
|
+
// await module.route.resolve(context as any)
|
|
138
|
+
await module.resolve()
|
|
139
|
+
const method = module.route.route.method ?? RouteMethod.GET
|
|
140
|
+
if (module.handle == null) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
server.route({
|
|
144
|
+
url: module.getPath(), method,
|
|
145
|
+
schema: {
|
|
146
|
+
consumes: [
|
|
147
|
+
'application/json',
|
|
148
|
+
'application/x-www-form-urlencoded',
|
|
149
|
+
'multipart/form-data',
|
|
150
|
+
],
|
|
151
|
+
querystring: fixFormatDates(module.filter?.query ?? {}),
|
|
152
|
+
...(method !== RouteMethod.GET ? { body: fixFormatDates(module.filter?.body ?? {}) } : {}),
|
|
153
|
+
params: fixFormatDates(module.filter?.params ?? {}),
|
|
154
|
+
response: module.filter?.response,
|
|
155
|
+
headers: fixFormatDates(module.filter?.headers ?? {})
|
|
156
|
+
},
|
|
157
|
+
handler: createServerHandler(module, location)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
service.initialized = true
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return service
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const appendApiServer = <C extends Config, T extends ServerContext<C>>(
|
|
169
|
+
ctx: T, alias: string = DEFAULT_ALIAS
|
|
170
|
+
): T & ApiServerAppend => {
|
|
171
|
+
const service = createApiServer(alias)
|
|
172
|
+
const context = ctx as T & ApiServerAppend
|
|
173
|
+
|
|
174
|
+
context.registerService(service)
|
|
175
|
+
|
|
176
|
+
if (context.getApiServer == null) {
|
|
177
|
+
context.getApiServer = () => ctx.service(service.alias)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return context
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
import type { InitializedService, Layer } from '@owlmeans/context'
|
|
3
|
+
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
4
|
+
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
|
|
5
|
+
import '@fastify/middie/types/index.d.ts'
|
|
6
|
+
|
|
7
|
+
export interface ApiServer extends InitializedService {
|
|
8
|
+
server: FastifyInstance
|
|
9
|
+
|
|
10
|
+
layers: [Layer.System]
|
|
11
|
+
|
|
12
|
+
listen: () => Promise<void>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Request extends FastifyRequest { }
|
|
16
|
+
|
|
17
|
+
export interface Response extends FastifyReply { }
|
|
18
|
+
|
|
19
|
+
export interface ApiServerAppend {
|
|
20
|
+
getApiServer: () => ApiServer
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Config extends ServerConfig { }
|
|
24
|
+
|
|
25
|
+
export interface Context<C extends Config = Config> extends ServerContext<C>,
|
|
26
|
+
ApiServerAppend { }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { assertContext } from '@owlmeans/context'
|
|
2
|
+
import type { Config, Context, Request } from '../types.js'
|
|
3
|
+
import { DEFAULT_ALIAS } from '../consts.js'
|
|
4
|
+
|
|
5
|
+
export const populateContext = <C extends Config, T extends Context<C>>(req: Request, context: T): void =>
|
|
6
|
+
void ((req as any)._ctx = context)
|
|
7
|
+
|
|
8
|
+
export const extractContext = <C extends Config, T extends Context<C>>(req: Request, ctx?: T, location?: string): T =>
|
|
9
|
+
assertContext<C, T>((req as any)._ctx ?? ctx, location ?? DEFAULT_ALIAS)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FORBIDDEN_ERROR, SERVER_ERROR, UNAUTHORIZED_ERROR } from '@owlmeans/api'
|
|
2
|
+
import type { FastifyReply } from 'fastify'
|
|
3
|
+
import { AccessError, AuthFailedError } from '../errors.js'
|
|
4
|
+
import { ResilientError } from '@owlmeans/error'
|
|
5
|
+
|
|
6
|
+
export const handleError = (error: Error, reply: FastifyReply) => {
|
|
7
|
+
let code: number = SERVER_ERROR
|
|
8
|
+
if (error instanceof AuthFailedError) {
|
|
9
|
+
code = UNAUTHORIZED_ERROR
|
|
10
|
+
} else if (error instanceof AccessError) {
|
|
11
|
+
code = FORBIDDEN_ERROR
|
|
12
|
+
}
|
|
13
|
+
if (!reply.sent) {
|
|
14
|
+
reply.code(code).send(
|
|
15
|
+
ResilientError.marshal(ResilientError.ensure(error as Error)).message
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { isContextWithoutIds, Layer } from '@owlmeans/context'
|
|
2
|
+
import type { FastifyReply, FastifyRequest } from 'fastify'
|
|
3
|
+
import type { ServerModule } from '@owlmeans/server-module'
|
|
4
|
+
import type { GuardService } from '@owlmeans/module'
|
|
5
|
+
import { provideResponse } from '@owlmeans/module'
|
|
6
|
+
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
7
|
+
import { executeResponse, provideRequest } from './payload.js'
|
|
8
|
+
import { AuthFailedError } from '../errors.js'
|
|
9
|
+
import type { Auth } from '@owlmeans/auth'
|
|
10
|
+
|
|
11
|
+
type Config = ServerConfig
|
|
12
|
+
interface Context<C extends Config = Config> extends ServerContext<C> { }
|
|
13
|
+
|
|
14
|
+
export const authorize = async <C extends Config, T extends Context<C>>(
|
|
15
|
+
context: T, module: ServerModule<FastifyRequest>,
|
|
16
|
+
req: FastifyRequest, reply: FastifyReply
|
|
17
|
+
): Promise<[T, ServerModule<FastifyRequest>]> => {
|
|
18
|
+
const guards = module.getGuards()
|
|
19
|
+
if (guards.length > 0) {
|
|
20
|
+
const response = provideResponse(reply)
|
|
21
|
+
const request = provideRequest(module.alias, req)
|
|
22
|
+
|
|
23
|
+
let guard: GuardService | undefined = undefined
|
|
24
|
+
for (const alias of guards) {
|
|
25
|
+
const _guard: GuardService = context.service(alias)
|
|
26
|
+
if (await _guard.match(request, response)) {
|
|
27
|
+
guard = _guard
|
|
28
|
+
}
|
|
29
|
+
executeResponse(response, reply, true)
|
|
30
|
+
if (guard != null) {
|
|
31
|
+
break
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (guard == null) {
|
|
36
|
+
throw new AuthFailedError()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const authResponse = provideResponse<Auth>(reply)
|
|
40
|
+
if (!await guard.handle<boolean>(request, authResponse)) {
|
|
41
|
+
throw new AuthFailedError(guard.alias)
|
|
42
|
+
}
|
|
43
|
+
executeResponse(authResponse, reply, true)
|
|
44
|
+
// Guard that returns true and does not provide an error is an optional guard
|
|
45
|
+
// if (authResponse.value == null) {
|
|
46
|
+
// throw SyntaxError(`Guard that returns true and does not provide an error, should provide authorization`)
|
|
47
|
+
// }
|
|
48
|
+
request.auth = authResponse.value;
|
|
49
|
+
if (request.auth != null) {
|
|
50
|
+
(req as any)._auth = request.auth
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (request.auth?.entityId != null) {
|
|
54
|
+
// @TODO Probably we need to downgrade context in this case
|
|
55
|
+
if (!isContextWithoutIds(context as any)) {
|
|
56
|
+
throw SyntaxError(`Context should be without ids during authorization ${context.cfg.layer}:${context.cfg.layerId}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (isContextWithoutIds(context as any) && context.cfg.layer !== Layer.Service) {
|
|
60
|
+
context = await context.updateContext(undefined, Layer.Service)
|
|
61
|
+
await context.waitForInitialized()
|
|
62
|
+
}
|
|
63
|
+
context = await context.updateContext(request.auth.entityId, Layer.Entity)
|
|
64
|
+
await context.waitForInitialized()
|
|
65
|
+
|
|
66
|
+
// We elevate module to the context level if it was changed
|
|
67
|
+
module = context.module(module.alias)
|
|
68
|
+
await module.resolve()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Update context in request object
|
|
72
|
+
(req as any)._ctx = context
|
|
73
|
+
|
|
74
|
+
return [context, module]
|
|
75
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ModuleOutcome } from '@owlmeans/module'
|
|
2
|
+
import type { AbstractResponse, AbstractRequest } from '@owlmeans/module'
|
|
3
|
+
import type { Request, Response } from '../types.js'
|
|
4
|
+
import { ACCEPTED, CREATED, OK, SERVER_ERROR } from '@owlmeans/api'
|
|
5
|
+
import type { AnySchemaObject } from 'ajv'
|
|
6
|
+
|
|
7
|
+
export const provideRequest = (alias: string, req: Request, provision?: boolean): AbstractRequest => {
|
|
8
|
+
provision = provision ?? false
|
|
9
|
+
return {
|
|
10
|
+
alias,
|
|
11
|
+
auth: (req as any)._auth ?? undefined,
|
|
12
|
+
params: req.params as Record<string, string | number | undefined | null>,
|
|
13
|
+
body: req.body as Record<string, any>,
|
|
14
|
+
headers: req.headers,
|
|
15
|
+
query: req.query as Record<string, string | number | undefined | null>,
|
|
16
|
+
path: req.url,
|
|
17
|
+
original: provision ? req : undefined
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @throws {Error}
|
|
23
|
+
*/
|
|
24
|
+
export const executeResponse = <T>(response: AbstractResponse<T>, reply: Response, throwOnError?: boolean) => {
|
|
25
|
+
if (response.error != null) {
|
|
26
|
+
if (throwOnError ?? false) {
|
|
27
|
+
throw response.error
|
|
28
|
+
}
|
|
29
|
+
reply.code(SERVER_ERROR).send(response.error.message)
|
|
30
|
+
} else if (response.outcome != null) {
|
|
31
|
+
switch (response.outcome) {
|
|
32
|
+
case ModuleOutcome.Accepted:
|
|
33
|
+
reply.code(ACCEPTED).send(response.value)
|
|
34
|
+
break
|
|
35
|
+
case ModuleOutcome.Created:
|
|
36
|
+
reply.code(CREATED).send(response.value)
|
|
37
|
+
break
|
|
38
|
+
case ModuleOutcome.Ok:
|
|
39
|
+
default:
|
|
40
|
+
reply.code(OK).send(response.value)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export const fixFormatDates = (schema: AnySchemaObject) => {
|
|
47
|
+
if (schema.type === 'object') {
|
|
48
|
+
if (schema.format === 'date-time') {
|
|
49
|
+
schema.type = 'string'
|
|
50
|
+
schema.format = 'date-time'
|
|
51
|
+
if (schema.required != null) {
|
|
52
|
+
delete schema.required
|
|
53
|
+
}
|
|
54
|
+
} else if (schema.properties != null) {
|
|
55
|
+
schema.properties = Object.fromEntries(
|
|
56
|
+
Object.entries(schema.properties).map(([key, value]) => [key, fixFormatDates(value as AnySchemaObject)])
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return schema
|
|
62
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AppType } from '@owlmeans/context'
|
|
2
|
+
import type { FastifyReply, FastifyRequest } from 'fastify'
|
|
3
|
+
import { assertContext } from '@owlmeans/context'
|
|
4
|
+
import type { FixerService, ServerModule } from '@owlmeans/server-module'
|
|
5
|
+
import type { CommonModule } from '@owlmeans/module'
|
|
6
|
+
import type { GateService } from '@owlmeans/module'
|
|
7
|
+
import { provideResponse } from '@owlmeans/module'
|
|
8
|
+
import type { ServerContext, ServerConfig } from '@owlmeans/server-context'
|
|
9
|
+
import { ResilientError } from '@owlmeans/error'
|
|
10
|
+
import { OK } from '@owlmeans/api'
|
|
11
|
+
import { handleError } from './error.js'
|
|
12
|
+
import { executeResponse, provideRequest } from './payload.js'
|
|
13
|
+
import { authorize } from './guards.js'
|
|
14
|
+
import { RouteProtocols } from '@owlmeans/route'
|
|
15
|
+
|
|
16
|
+
type Config = ServerConfig
|
|
17
|
+
type Context = ServerContext<Config>
|
|
18
|
+
|
|
19
|
+
export const canServeModule = (context: Context, module: CommonModule): module is ServerModule<unknown> => {
|
|
20
|
+
if (module.route.route.type !== AppType.Backend) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
if (module.route.route.service != null && module.route.route.service !== context.cfg.service) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
if (module.route.route.protocol === RouteProtocols.SOCKET) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return 'isIntermediate' in module.route
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const createServerHandler = (module: ServerModule<FastifyRequest>, location: string) =>
|
|
34
|
+
async (req: FastifyRequest, reply: FastifyReply) => {
|
|
35
|
+
// We passed context using fastify request object
|
|
36
|
+
let context = assertContext<Config, Context>((req as any)._ctx, location)
|
|
37
|
+
try {
|
|
38
|
+
const authorized = await authorize(context, module, req, reply)
|
|
39
|
+
context = authorized[0]
|
|
40
|
+
module = authorized[1]
|
|
41
|
+
|
|
42
|
+
const response = provideResponse(reply)
|
|
43
|
+
const request = provideRequest(module.alias, req, true)
|
|
44
|
+
|
|
45
|
+
const gates = module.getGates()
|
|
46
|
+
for (const [srv, params] of gates) {
|
|
47
|
+
const gate: GateService = context.service(srv)
|
|
48
|
+
await gate.assert(request, response, params)
|
|
49
|
+
executeResponse(response, reply, true)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await module.handle(request, response)
|
|
53
|
+
|
|
54
|
+
executeResponse(response, reply, true)
|
|
55
|
+
if (!reply.sent) {
|
|
56
|
+
console.warn(`SENDS DEFAULT RESPONSE: ${module.alias}`)
|
|
57
|
+
reply.code(OK).send(response.value)
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(`Error in ${module.alias} (${location})`)
|
|
61
|
+
console.error(JSON.stringify(error, null, 2))
|
|
62
|
+
console.error(error)
|
|
63
|
+
if (module.fixer != null) {
|
|
64
|
+
const fixer: FixerService = context.service(module.fixer)
|
|
65
|
+
fixer.handle(reply, ResilientError.ensure(error as Error))
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
handleError(error as Error, reply)
|
|
69
|
+
}
|
|
70
|
+
}
|