@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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +613 -0
  3. package/build/.gitkeep +0 -0
  4. package/build/consts.d.ts +5 -0
  5. package/build/consts.d.ts.map +1 -0
  6. package/build/consts.js +5 -0
  7. package/build/consts.js.map +1 -0
  8. package/build/errors.d.ts +14 -0
  9. package/build/errors.d.ts.map +1 -0
  10. package/build/errors.js +27 -0
  11. package/build/errors.js.map +1 -0
  12. package/build/helper.d.ts +13 -0
  13. package/build/helper.d.ts.map +1 -0
  14. package/build/helper.js +53 -0
  15. package/build/helper.js.map +1 -0
  16. package/build/index.d.ts +6 -0
  17. package/build/index.d.ts.map +1 -0
  18. package/build/index.js +5 -0
  19. package/build/index.js.map +1 -0
  20. package/build/server.d.ts +7 -0
  21. package/build/server.d.ts.map +1 -0
  22. package/build/server.js +146 -0
  23. package/build/server.js.map +1 -0
  24. package/build/types.d.ts +21 -0
  25. package/build/types.d.ts.map +1 -0
  26. package/build/types.js +2 -0
  27. package/build/types.js.map +1 -0
  28. package/build/utils/context.d.ts +4 -0
  29. package/build/utils/context.d.ts.map +1 -0
  30. package/build/utils/context.js +5 -0
  31. package/build/utils/context.js.map +1 -0
  32. package/build/utils/error.d.ts +3 -0
  33. package/build/utils/error.d.ts.map +1 -0
  34. package/build/utils/error.js +16 -0
  35. package/build/utils/error.js.map +1 -0
  36. package/build/utils/guards.d.ts +9 -0
  37. package/build/utils/guards.d.ts.map +1 -0
  38. package/build/utils/guards.js +57 -0
  39. package/build/utils/guards.js.map +1 -0
  40. package/build/utils/index.d.ts +7 -0
  41. package/build/utils/index.d.ts.map +1 -0
  42. package/build/utils/index.js +7 -0
  43. package/build/utils/index.js.map +1 -0
  44. package/build/utils/payload.d.ts +10 -0
  45. package/build/utils/payload.d.ts.map +1 -0
  46. package/build/utils/payload.js +55 -0
  47. package/build/utils/payload.js.map +1 -0
  48. package/build/utils/server.d.ts +10 -0
  49. package/build/utils/server.d.ts.map +1 -0
  50. package/build/utils/server.js +56 -0
  51. package/build/utils/server.js.map +1 -0
  52. package/package.json +62 -0
  53. package/src/consts.ts +7 -0
  54. package/src/errors.ts +35 -0
  55. package/src/helper.ts +89 -0
  56. package/src/index.ts +6 -0
  57. package/src/server.ts +183 -0
  58. package/src/types.ts +26 -0
  59. package/src/utils/context.ts +9 -0
  60. package/src/utils/error.ts +18 -0
  61. package/src/utils/guards.ts +75 -0
  62. package/src/utils/index.ts +7 -0
  63. package/src/utils/payload.ts +62 -0
  64. package/src/utils/server.ts +70 -0
  65. package/tsconfig.json +15 -0
  66. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/utils/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAE3C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAIjD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAKhD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,OAAgB,EAAE,MAAoB,EAAmC,EAAE;IACxG,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC;QAChD,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,IAAI,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,KAAK,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QAC7F,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,KAAK,cAAc,CAAC,MAAM,EAAE,CAAC;QAC1D,OAAO,KAAK,CAAA;IACd,CAAC;IAED,OAAO,gBAAgB,IAAI,MAAM,CAAC,KAAK,CAAA;AACzC,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAoC,EAAE,QAAgB,EAAE,EAAE,CAC5F,KAAK,EAAE,GAAmB,EAAE,KAAmB,EAAE,EAAE;IACjD,iDAAiD;IACjD,IAAI,OAAO,GAAG,aAAa,CAAmB,GAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;IACzE,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;QAC/D,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,CAAA;QACvB,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,CAAA;QAEtB,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,CAAA;QACvC,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;QAEvD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAA;QAC/B,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YAClC,MAAM,IAAI,GAAgB,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;YAC9C,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAA;YAC5C,eAAe,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;QACxC,CAAC;QAED,MAAM,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;QAEtC,eAAe,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;QACtC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAChB,OAAO,CAAC,IAAI,CAAC,2BAA2B,MAAM,CAAC,KAAK,EAAE,CAAC,CAAA;YACvD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QACrC,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,YAAY,MAAM,CAAC,KAAK,KAAK,QAAQ,GAAG,CAAC,CAAA;QACvD,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;QAC7C,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACpB,IAAI,MAAM,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,MAAM,KAAK,GAAiB,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACzD,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,KAAc,CAAC,CAAC,CAAA;YAC1D,OAAM;QACR,CAAC;QACD,WAAW,CAAC,KAAc,EAAE,KAAK,CAAC,CAAA;IACpC,CAAC;AACH,CAAC,CAAA"}
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
@@ -0,0 +1,7 @@
1
+
2
+ export const DEFAULT_ALIAS = 'api-server'
3
+
4
+ export const PORT = 80
5
+
6
+ export const CLOSED_HOST = '127.0.0.1'
7
+ export const OPENED_HOST = '0.0.0.0'
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
@@ -0,0 +1,6 @@
1
+
2
+ export type * from './types.js'
3
+ export * from './server.js'
4
+ export * from './consts.js'
5
+ export * from './errors.js'
6
+ export * from './helper.js'
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,7 @@
1
+
2
+ export * from './error.js'
3
+ export * from './server.js'
4
+ export * from './payload.js'
5
+ export * from './server.js'
6
+ export * from './guards.js'
7
+ export * from './context.js'
@@ -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
+ }