@navios/core 0.1.9 → 0.1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navios/core",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "author": {
5
5
  "name": "Oleksandr Hanzha",
6
6
  "email": "alex@granted.name"
@@ -15,7 +15,7 @@
15
15
  "main": "./dist/index.js",
16
16
  "module": "./dist/index.mjs",
17
17
  "peerDependencies": {
18
- "@navios/common": "^0.1.2",
18
+ "@navios/common": "^0.1.3",
19
19
  "zod": "^3.23.8"
20
20
  },
21
21
  "exports": {
@@ -32,7 +32,8 @@
32
32
  "./package.json": "./package.json"
33
33
  },
34
34
  "devDependencies": {
35
- "@navios/common": "^0.1.2",
35
+ "@fastify/multipart": "^9.0.3",
36
+ "@navios/common": "^0.1.3",
36
37
  "tsx": "^4.19.4",
37
38
  "typescript": "^5.8.3",
38
39
  "zod": "^3.24.4"
@@ -3,5 +3,6 @@ export * from './endpoint.decorator.mjs'
3
3
  export * from './header.decorator.mjs'
4
4
  export * from './http-code.decorator.mjs'
5
5
  export * from './module.decorator.mjs'
6
+ export * from './multipart.decorator.mjs'
6
7
  export * from './stream.decorator.mjs'
7
8
  export * from './use-guards.decorator.mjs'
@@ -0,0 +1,100 @@
1
+ import type {
2
+ BaseEndpointConfig,
3
+ EndpointFunctionArgs,
4
+ HttpMethod,
5
+ } from '@navios/common'
6
+ import type { AnyZodObject, z, ZodType } from 'zod'
7
+
8
+ import { ZodDiscriminatedUnion } from 'zod'
9
+
10
+ import { EndpointType, getEndpointMetadata } from '../metadata/index.mjs'
11
+
12
+ export type MultipartParams<
13
+ EndpointDeclaration extends {
14
+ config: BaseEndpointConfig<any, any, any, any, any>
15
+ },
16
+ Url extends string = EndpointDeclaration['config']['url'],
17
+ QuerySchema = EndpointDeclaration['config']['querySchema'],
18
+ > = QuerySchema extends AnyZodObject
19
+ ? EndpointDeclaration['config']['requestSchema'] extends ZodType
20
+ ? EndpointFunctionArgs<
21
+ Url,
22
+ QuerySchema,
23
+ EndpointDeclaration['config']['requestSchema']
24
+ >
25
+ : EndpointFunctionArgs<Url, QuerySchema, undefined>
26
+ : EndpointDeclaration['config']['requestSchema'] extends ZodType
27
+ ? EndpointFunctionArgs<
28
+ Url,
29
+ undefined,
30
+ EndpointDeclaration['config']['requestSchema']
31
+ >
32
+ : EndpointFunctionArgs<Url, undefined, undefined>
33
+
34
+ export type MultipartResult<
35
+ EndpointDeclaration extends {
36
+ config: BaseEndpointConfig<any, any, any, any, any>
37
+ },
38
+ > =
39
+ EndpointDeclaration['config']['responseSchema'] extends ZodDiscriminatedUnion<
40
+ any,
41
+ infer Options
42
+ >
43
+ ? Promise<z.input<Options[number]>>
44
+ : Promise<z.input<EndpointDeclaration['config']['responseSchema']>>
45
+
46
+ export function Multipart<
47
+ Method extends HttpMethod = HttpMethod,
48
+ Url extends string = string,
49
+ QuerySchema = undefined,
50
+ ResponseSchema extends ZodType = ZodType,
51
+ RequestSchema = ZodType,
52
+ >(endpoint: {
53
+ config: BaseEndpointConfig<
54
+ Method,
55
+ Url,
56
+ QuerySchema,
57
+ ResponseSchema,
58
+ RequestSchema
59
+ >
60
+ }) {
61
+ return (
62
+ target: (
63
+ params: QuerySchema extends AnyZodObject
64
+ ? RequestSchema extends ZodType
65
+ ? EndpointFunctionArgs<Url, QuerySchema, RequestSchema>
66
+ : EndpointFunctionArgs<Url, QuerySchema, undefined>
67
+ : RequestSchema extends ZodType
68
+ ? EndpointFunctionArgs<Url, undefined, RequestSchema>
69
+ : EndpointFunctionArgs<Url, undefined, undefined>,
70
+ ) => Promise<z.input<ResponseSchema>>,
71
+ context: ClassMethodDecoratorContext,
72
+ ) => {
73
+ if (typeof target !== 'function') {
74
+ throw new Error(
75
+ '[Navios] Endpoint decorator can only be used on functions.',
76
+ )
77
+ }
78
+ if (context.kind !== 'method') {
79
+ throw new Error(
80
+ '[Navios] Endpoint decorator can only be used on methods.',
81
+ )
82
+ }
83
+ const config = endpoint.config
84
+ if (context.metadata) {
85
+ let endpointMetadata = getEndpointMetadata(target, context)
86
+ if (endpointMetadata.config && endpointMetadata.config.url) {
87
+ throw new Error(
88
+ `[Navios] Endpoint ${config.method} ${config.url} already exists. Please use a different method or url.`,
89
+ )
90
+ }
91
+ // @ts-expect-error We don't need to set correctly in the metadata
92
+ endpointMetadata.config = config
93
+ endpointMetadata.type = EndpointType.Multipart
94
+ endpointMetadata.classMethod = target.name
95
+ endpointMetadata.httpMethod = config.method
96
+ endpointMetadata.url = config.url
97
+ }
98
+ return target
99
+ }
100
+ }
@@ -17,6 +17,7 @@ export enum EndpointType {
17
17
  Unknown = 'unknown',
18
18
  Endpoint = 'endpoint',
19
19
  Stream = 'stream',
20
+ Multipart = 'multipart',
20
21
  Handler = 'handler',
21
22
  }
22
23
 
@@ -1,4 +1,5 @@
1
1
  import type { FastifyCorsOptions } from '@fastify/cors'
2
+ import type { FastifyMultipartOptions } from '@fastify/multipart'
2
3
  import type {
3
4
  FastifyInstance,
4
5
  FastifyListenOptions,
@@ -50,6 +51,7 @@ export class NaviosApplication {
50
51
  })
51
52
  private server: FastifyInstance | null = null
52
53
  private corsOptions: FastifyCorsOptions | null = null
54
+ private multipartOptions: FastifyMultipartOptions | true | null = null
53
55
  private globalPrefix: string | null = null
54
56
 
55
57
  private appModule: ClassTypeWithInstance<NaviosModule> | null = null
@@ -81,6 +83,10 @@ export class NaviosApplication {
81
83
  await this.server.register(cors, this.corsOptions)
82
84
  }
83
85
 
86
+ if (this.multipartOptions) {
87
+ await this.configureMultipart(this.server, this.multipartOptions)
88
+ }
89
+
84
90
  await this.initModules()
85
91
  await this.server.ready()
86
92
 
@@ -147,6 +153,26 @@ export class NaviosApplication {
147
153
  })
148
154
  }
149
155
 
156
+ async configureMultipart(
157
+ server: FastifyInstance,
158
+ options: FastifyMultipartOptions | true,
159
+ ): Promise<void> {
160
+ if (options) {
161
+ try {
162
+ const multipartModule = await import('@fastify/multipart')
163
+ await server.register(
164
+ multipartModule.default,
165
+ typeof options === 'object' ? options : {},
166
+ )
167
+ } catch (error) {
168
+ this.logger.error(
169
+ `@fastify/multipart is not installed. Please install it.`,
170
+ )
171
+ throw error
172
+ }
173
+ }
174
+ }
175
+
150
176
  private async initModules() {
151
177
  const modules = this.moduleLoader.getAllModules()
152
178
  const promises: PromiseLike<any>[] = []
@@ -183,6 +209,10 @@ export class NaviosApplication {
183
209
  this.corsOptions = options
184
210
  }
185
211
 
212
+ enableMultipart(options: FastifyMultipartOptions) {
213
+ this.multipartOptions = options
214
+ }
215
+
186
216
  setGlobalPrefix(prefix: string) {
187
217
  this.globalPrefix = prefix
188
218
  }
@@ -1,9 +1,12 @@
1
1
  import type { BaseEndpointConfig } from '@navios/common'
2
2
  import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
3
3
  import type { ZodTypeProvider } from 'fastify-type-provider-zod'
4
+ import type { AnyZodObject } from 'zod'
4
5
 
5
6
  import { NaviosException } from '@navios/common'
6
7
 
8
+ import { ZodArray } from 'zod'
9
+
7
10
  import type { EndpointMetadata, ModuleMetadata } from '../metadata/index.mjs'
8
11
  import type { ClassType } from '../service-locator/index.mjs'
9
12
 
@@ -100,7 +103,7 @@ export class ControllerAdapterService {
100
103
  if (querySchema) {
101
104
  schema.querystring = querySchema
102
105
  }
103
- if (requestSchema) {
106
+ if (requestSchema && endpointMetadata.type !== EndpointType.Multipart) {
104
107
  schema.body = requestSchema
105
108
  }
106
109
  if (responseSchema) {
@@ -135,6 +138,12 @@ export class ControllerAdapterService {
135
138
  executionContext,
136
139
  endpointMetadata,
137
140
  )
141
+ case EndpointType.Multipart:
142
+ return this.provideHandlerForMultipart(
143
+ controller,
144
+ executionContext,
145
+ endpointMetadata,
146
+ )
138
147
  case EndpointType.Handler:
139
148
  this.logger.error('Not implemented yet')
140
149
  throw new NaviosException('Not implemented yet')
@@ -218,4 +227,78 @@ export class ControllerAdapterService {
218
227
  }
219
228
  }
220
229
  }
230
+
231
+ private provideHandlerForMultipart(
232
+ controller: ClassType,
233
+ executionContext: ExecutionContext,
234
+ endpointMetadata: EndpointMetadata,
235
+ ): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {
236
+ const config = endpointMetadata.config as BaseEndpointConfig
237
+ const requestSchema = config.requestSchema as unknown as AnyZodObject
238
+ const shape = requestSchema._def.shape()
239
+ return async (request, reply) => {
240
+ getServiceLocator().registerInstance(Request, request)
241
+ getServiceLocator().registerInstance(Reply, reply)
242
+ getServiceLocator().registerInstance(
243
+ ExecutionContextToken,
244
+ executionContext,
245
+ )
246
+ executionContext.provideRequest(request)
247
+ executionContext.provideReply(reply)
248
+ const controllerInstance = await inject(controller)
249
+ try {
250
+ const parts = request.parts()
251
+ const { query, params } = request
252
+ const argument: Record<string, any> = {}
253
+ if (query && Object.keys(query).length > 0) {
254
+ argument.params = query
255
+ }
256
+ if (params && Object.keys(params).length > 0) {
257
+ argument.urlParams = params
258
+ }
259
+ const req: Record<string, any> = {}
260
+ for await (const part of parts) {
261
+ if (!shape[part.fieldname]) {
262
+ throw new NaviosException(
263
+ `Invalid field name ${part.fieldname} for multipart request`,
264
+ )
265
+ }
266
+ const schema = shape[part.fieldname]
267
+ if (part.type === 'file') {
268
+ const file = new File([await part.toBuffer()], part.filename, {
269
+ type: part.mimetype,
270
+ })
271
+ if (schema instanceof ZodArray) {
272
+ if (!req[part.fieldname]) {
273
+ req[part.fieldname] = []
274
+ }
275
+ req[part.fieldname].push(file)
276
+ } else {
277
+ req[part.fieldname] = file
278
+ }
279
+ } else {
280
+ if (schema instanceof ZodArray) {
281
+ if (!req[part.fieldname]) {
282
+ req[part.fieldname] = []
283
+ }
284
+ req[part.fieldname].push(part.value)
285
+ } else {
286
+ req[part.fieldname] = part.value
287
+ }
288
+ }
289
+ }
290
+ argument.body = requestSchema.parse(req)
291
+ const result =
292
+ await controllerInstance[endpointMetadata.classMethod](argument)
293
+ reply
294
+ .status(endpointMetadata.successStatusCode)
295
+ .headers(endpointMetadata.headers)
296
+ .send(result)
297
+ } finally {
298
+ getServiceLocator().removeInstance(Request)
299
+ getServiceLocator().removeInstance(Reply)
300
+ getServiceLocator().removeInstance(ExecutionContextToken)
301
+ }
302
+ }
303
+ }
221
304
  }