@navios/core 0.1.1 → 0.1.3

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.1",
3
+ "version": "0.1.3",
4
4
  "author": {
5
5
  "name": "Oleksandr Hanzha",
6
6
  "email": "alex@granted.name"
@@ -0,0 +1,16 @@
1
+ import type { Path, PathValue } from './types.mjs'
2
+
3
+ export interface ConfigService<Config = Record<string, unknown>> {
4
+ getConfig: () => Config
5
+ get: <Key extends Path<Config>>(key: Key) => PathValue<Config, Key> | null
6
+
7
+ getOrDefault: <Key extends Path<Config>>(
8
+ key: Key,
9
+ defaultValue: PathValue<Config, Key>,
10
+ ) => PathValue<Config, Key>
11
+
12
+ getOrThrow: <Key extends Path<Config>>(
13
+ key: Key,
14
+ errorMessage?: string,
15
+ ) => PathValue<Config, Key>
16
+ }
@@ -0,0 +1,62 @@
1
+ import { z } from 'zod'
2
+
3
+ import type { ConfigService } from './config-service.interface.mjs'
4
+
5
+ import { Logger } from '../logger/index.mjs'
6
+ import {
7
+ getInjectableToken,
8
+ inject,
9
+ Injectable,
10
+ InjectableType,
11
+ InjectionToken,
12
+ } from '../service-locator/index.mjs'
13
+ import { ConfigServiceInstance } from './config.service.mjs'
14
+
15
+ export const ConfigProviderInjectionToken = 'ConfigProvider'
16
+
17
+ export const ConfigProviderOptions = z.object({
18
+ load: z.function(),
19
+ })
20
+ export const ConfigProvider = InjectionToken.create<
21
+ ConfigService,
22
+ typeof ConfigProviderOptions
23
+ >(ConfigProviderInjectionToken, ConfigProviderOptions)
24
+
25
+ @Injectable({
26
+ token: ConfigProvider,
27
+ type: InjectableType.Factory,
28
+ })
29
+ export class ConfigProviderFactory {
30
+ logger = inject(Logger, {
31
+ context: 'ConfigService',
32
+ })
33
+
34
+ async create(ctx: any, args: z.infer<typeof ConfigProviderOptions>) {
35
+ const { load } = args
36
+ const logger = await this.logger
37
+ try {
38
+ const config = await load()
39
+
40
+ return new ConfigServiceInstance(config, logger)
41
+ } catch (error) {
42
+ logger.error('Error loading config', error)
43
+ throw error
44
+ }
45
+ }
46
+ }
47
+
48
+ export function makeConfigToken<Config extends Record<string, unknown>>(
49
+ options: z.input<typeof ConfigProviderOptions>,
50
+ ): InjectionToken<ConfigService<Config>> {
51
+ @Injectable({
52
+ type: InjectableType.Factory,
53
+ })
54
+ class ConfigServiceImpl {
55
+ configService = inject(ConfigProvider, options)
56
+
57
+ create() {
58
+ return this.configService
59
+ }
60
+ }
61
+ return getInjectableToken(ConfigServiceImpl)
62
+ }
@@ -0,0 +1,69 @@
1
+ import { NaviosException } from '@navios/common'
2
+
3
+ import type { LoggerService } from '../logger/index.mjs'
4
+ import type { ConfigService } from './config-service.interface.mjs'
5
+ import type { Path, PathValue } from './types.mjs'
6
+
7
+ export class ConfigServiceInstance<Config = Record<string, unknown>>
8
+ implements ConfigService<Config>
9
+ {
10
+ constructor(
11
+ private config: Config = {} as Config,
12
+ private logger: LoggerService,
13
+ ) {}
14
+
15
+ getConfig(): Config {
16
+ return this.config
17
+ }
18
+
19
+ get<Key extends Path<Config>>(key: Key): PathValue<Config, Key> | null {
20
+ try {
21
+ const parts = String(key).split('.')
22
+ let value: any = this.config
23
+
24
+ for (const part of parts) {
25
+ if (
26
+ value === null ||
27
+ value === undefined ||
28
+ typeof value !== 'object'
29
+ ) {
30
+ return null
31
+ }
32
+ value = value[part]
33
+ }
34
+
35
+ return (value as PathValue<Config, Key>) ?? null
36
+ } catch (error) {
37
+ this.logger.debug?.(
38
+ `Failed to get config value for key ${String(key)}`,
39
+ error,
40
+ )
41
+ return null
42
+ }
43
+ }
44
+
45
+ getOrDefault<Key extends Path<Config>>(
46
+ key: Key,
47
+ defaultValue: PathValue<Config, Key>,
48
+ ): PathValue<Config, Key> {
49
+ const value = this.get(key)
50
+ return value !== null ? value : defaultValue
51
+ }
52
+
53
+ getOrThrow<Key extends Path<Config>>(
54
+ key: Key,
55
+ errorMessage?: string,
56
+ ): PathValue<Config, Key> {
57
+ const value = this.get(key)
58
+
59
+ if (value === null) {
60
+ const message =
61
+ errorMessage ||
62
+ `Configuration value for key "${String(key)}" is not defined`
63
+ this.logger.error(message)
64
+ throw new NaviosException(message)
65
+ }
66
+
67
+ return value
68
+ }
69
+ }
@@ -0,0 +1,5 @@
1
+ export * from './utils/index.mjs'
2
+ export * from './config.provider.mjs'
3
+ export * from './config.service.mjs'
4
+ export * from './config-service.interface.mjs'
5
+ export * from './types.mjs'
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Evaluates to `true` if `T` is `any`. `false` otherwise.
3
+ * (c) https://stackoverflow.com/a/68633327/5290447
4
+ */
5
+ type IsAny<T> = unknown extends T
6
+ ? [keyof T] extends [never]
7
+ ? false
8
+ : true
9
+ : false
10
+
11
+ type ExcludedParts =
12
+ | 'services'
13
+ | 'mailer'
14
+ | 'aws'
15
+ | 'computedTimeRates'
16
+ | 'aiModelsRates'
17
+ type ExcludedKeys = 'computedTimeRates' | 'aiModelsRates' | 'aiModel'
18
+
19
+ export type PathImpl<T, Key extends keyof T> = Key extends string
20
+ ? Key extends ExcludedKeys
21
+ ? never
22
+ : IsAny<T[Key]> extends true
23
+ ? never
24
+ : T[Key] extends string
25
+ ? never
26
+ : T[Key] extends any[]
27
+ ? never
28
+ : T[Key] extends Record<string, any>
29
+ ? Key extends ExcludedParts
30
+ ? `${Key}.${Exclude<keyof T[Key], keyof any[]> & string}`
31
+ :
32
+ | `${Key}.${PathImpl<T[Key], Exclude<keyof T[Key], keyof any[]>> & string}`
33
+ | `${Key}.${Exclude<keyof T[Key], keyof any[]> & string}`
34
+ : never
35
+ : never
36
+
37
+ export type PathImpl2<T> = PathImpl<T, keyof T> | keyof T
38
+
39
+ export type Path<T> = keyof T extends string
40
+ ? PathImpl2<T> extends infer P
41
+ ? P extends string | keyof T
42
+ ? P
43
+ : keyof T
44
+ : keyof T
45
+ : never
46
+
47
+ export type PathValue<
48
+ T,
49
+ P extends Path<T>,
50
+ > = P extends `${infer Key}.${infer Rest}`
51
+ ? Key extends keyof T
52
+ ? Rest extends Path<T[Key]>
53
+ ? PathValue<T[Key], Rest>
54
+ : never
55
+ : never
56
+ : P extends keyof T
57
+ ? T[P]
58
+ : never
@@ -0,0 +1,23 @@
1
+ import { env } from 'node:process'
2
+
3
+ export function envInt(
4
+ key: keyof NodeJS.ProcessEnv,
5
+ defaultValue: number,
6
+ ): number {
7
+ const envKey = env[key] || process.env[key]
8
+
9
+ return envKey ? parseInt(envKey as string, 10) : defaultValue
10
+ }
11
+
12
+ export function envString<
13
+ DefaultValue extends string | undefined,
14
+ Ensured = DefaultValue extends string ? true : false,
15
+ >(
16
+ key: keyof NodeJS.ProcessEnv,
17
+ defaultValue?: DefaultValue,
18
+ ): Ensured extends true ? string : string | undefined {
19
+ return (env[key] ||
20
+ process.env[key] ||
21
+ defaultValue ||
22
+ undefined) as Ensured extends true ? string : string | undefined
23
+ }
@@ -0,0 +1 @@
1
+ export * from './helpers.mjs'
@@ -1,7 +1,11 @@
1
- import type { BaseEndpointConfig, EndpointFunctionArgs, HttpMethod } from '@navios/common'
1
+ import type {
2
+ BaseEndpointConfig,
3
+ EndpointFunctionArgs,
4
+ HttpMethod,
5
+ } from '@navios/common'
2
6
  import type { AnyZodObject, z, ZodType } from 'zod'
3
7
 
4
- import { getEndpointMetadata } from '../metadata/index.mjs'
8
+ import { EndpointType, getEndpointMetadata } from '../metadata/index.mjs'
5
9
 
6
10
  export type EndpointParams<
7
11
  EndpointDeclaration extends {
@@ -72,6 +76,7 @@ export function Endpoint<
72
76
  }
73
77
  // @ts-expect-error We don't need to set correctly in the metadata
74
78
  endpointMetadata.config = config
79
+ endpointMetadata.type = EndpointType.Config
75
80
  endpointMetadata.classMethod = target.name
76
81
  endpointMetadata.httpMethod = config.method
77
82
  endpointMetadata.url = config.url
@@ -0,0 +1,18 @@
1
+ import type { HttpHeader } from 'fastify/types/utils.js'
2
+
3
+ import { getEndpointMetadata } from '../metadata/index.mjs'
4
+
5
+ export function Header(name: HttpHeader, value: string | number | string[]) {
6
+ return <T extends Function>(
7
+ target: T,
8
+ context: ClassMethodDecoratorContext,
9
+ ) => {
10
+ if (context.kind !== 'method') {
11
+ throw new Error('[Navios] Header decorator can only be used on methods.')
12
+ }
13
+ const metadata = getEndpointMetadata(target, context)
14
+ metadata.headers[name] = value
15
+
16
+ return target
17
+ }
18
+ }
@@ -0,0 +1,18 @@
1
+ import { getEndpointMetadata } from '../metadata/index.mjs'
2
+
3
+ export function HttpCode(code: number) {
4
+ return <T extends Function>(
5
+ target: T,
6
+ context: ClassMethodDecoratorContext,
7
+ ) => {
8
+ if (context.kind !== 'method') {
9
+ throw new Error(
10
+ '[Navios] HttpCode decorator can only be used on methods.',
11
+ )
12
+ }
13
+ const metadata = getEndpointMetadata(target, context)
14
+ metadata.successStatusCode = code
15
+
16
+ return target
17
+ }
18
+ }
@@ -1,4 +1,6 @@
1
1
  export * from './controller.decorator.mjs'
2
2
  export * from './endpoint.decorator.mjs'
3
+ export * from './header.decorator.mjs'
4
+ export * from './http-code.decorator.mjs'
3
5
  export * from './module.decorator.mjs'
4
6
  export * from './use-guards.decorator.mjs'
package/src/index.mts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './config/index.mjs'
1
2
  export * from './decorators/index.mjs'
2
3
  export * from './exceptions/index.mjs'
3
4
  export * from './interfaces/index.mjs'
@@ -184,7 +184,6 @@ export class LoggerInstance implements LoggerService {
184
184
  }
185
185
 
186
186
  static overrideLogger(logger: LoggerService | LogLevel[] | boolean) {
187
- console.log(logger)
188
187
  if (Array.isArray(logger)) {
189
188
  LoggerInstance.logLevels = logger
190
189
  return this.staticInstanceRef?.setLogLevels?.(logger)
@@ -20,8 +20,9 @@ export class PinoWrapper {
20
20
  this.logger.warn(message, ...optionalParams)
21
21
  }
22
22
 
23
- info(message: any, ...optionalParams: any[]) {
24
- this.logger.log(message, ...optionalParams)
23
+ info() {
24
+ // We don't want to populate the logs with the original fastify logs
25
+ // this.logger.debug?.('INFO', message, ...optionalParams)
25
26
  }
26
27
 
27
28
  debug(message: any, ...optionalParams: any[]) {
@@ -32,7 +33,7 @@ export class PinoWrapper {
32
33
  this.logger.verbose?.(message, ...optionalParams)
33
34
  }
34
35
 
35
- silent(message: any, ...optionalParams: any[]) {
36
+ silent() {
36
37
  // noop
37
38
  }
38
39
 
@@ -56,8 +57,8 @@ export class PinoWrapper {
56
57
  }
57
58
  const levels = LoggerInstance['logLevels']
58
59
  if (levels) {
59
- return levels[0]
60
+ return levels.find((level) => level !== 'verbose')
60
61
  }
61
- return 'info'
62
+ return 'warn'
62
63
  }
63
64
  }
@@ -1,4 +1,5 @@
1
1
  import type { BaseEndpointConfig, HttpMethod } from '@navios/common'
2
+ import type { HttpHeader } from 'fastify/types/utils.js'
2
3
 
3
4
  import type { CanActivate } from '../interfaces/index.mjs'
4
5
  import type {
@@ -8,9 +9,18 @@ import type {
8
9
 
9
10
  export const EndpointMetadataKey = Symbol('EndpointMetadataKey')
10
11
 
12
+ export enum EndpointType {
13
+ Unknown = 'unknown',
14
+ Config = 'config',
15
+ Handler = 'handler',
16
+ }
17
+
11
18
  export interface EndpointMetadata {
12
19
  classMethod: string
13
20
  url: string
21
+ successStatusCode: number
22
+ type: EndpointType
23
+ headers: Partial<Record<HttpHeader, number | string | string[] | undefined>>
14
24
  httpMethod: HttpMethod
15
25
  config: BaseEndpointConfig | null
16
26
  guards: Set<
@@ -52,6 +62,9 @@ export function getEndpointMetadata(
52
62
  const newMetadata: EndpointMetadata = {
53
63
  classMethod: target.name,
54
64
  url: '',
65
+ successStatusCode: 200,
66
+ headers: {},
67
+ type: EndpointType.Unknown,
55
68
  httpMethod: 'GET',
56
69
  config: null,
57
70
  guards: new Set<
@@ -16,6 +16,7 @@ import type { NaviosModule } from './interfaces/index.mjs'
16
16
  import type { LoggerService, LogLevel } from './logger/index.mjs'
17
17
  import type { ClassTypeWithInstance } from './service-locator/index.mjs'
18
18
 
19
+ import { HttpException } from './exceptions/index.mjs'
19
20
  import { Logger, PinoWrapper } from './logger/index.mjs'
20
21
  import {
21
22
  getServiceLocator,
@@ -68,6 +69,7 @@ export class NaviosApplication {
68
69
  }
69
70
  await this.moduleLoader.loadModules(this.appModule)
70
71
  this.server = await this.getFastifyInstance(this.options)
72
+ this.configureFastifyInstance(this.server)
71
73
  getServiceLocator().registerInstance(Application, this.server)
72
74
  // Add schema validator and serializer
73
75
  this.server.setValidatorCompiler(validatorCompiler)
@@ -78,6 +80,7 @@ export class NaviosApplication {
78
80
  }
79
81
 
80
82
  await this.initModules()
83
+ await this.server.ready()
81
84
 
82
85
  this.logger.debug('Navios application initialized')
83
86
  }
@@ -110,6 +113,37 @@ export class NaviosApplication {
110
113
  }
111
114
  }
112
115
 
116
+ private configureFastifyInstance(fastifyInstance: FastifyInstance) {
117
+ fastifyInstance.setErrorHandler((error, request, reply) => {
118
+ if (error instanceof HttpException) {
119
+ return reply.status(error.statusCode).send(error.response)
120
+ } else {
121
+ const statusCode = error.statusCode || 500
122
+ const message = error.message || 'Internal Server Error'
123
+ const response = {
124
+ statusCode,
125
+ message,
126
+ error: error.name || 'InternalServerError',
127
+ }
128
+ this.logger.error(
129
+ `Error occurred: ${error.message} on ${request.url}`,
130
+ error,
131
+ )
132
+ return reply.status(statusCode).send(response)
133
+ }
134
+ })
135
+
136
+ fastifyInstance.setNotFoundHandler((req, reply) => {
137
+ const response = {
138
+ statusCode: 404,
139
+ message: 'Not Found',
140
+ error: 'NotFound',
141
+ }
142
+ this.logger.error(`Route not found: ${req.url}`)
143
+ return reply.status(404).send(response)
144
+ })
145
+ }
146
+
113
147
  private async initModules() {
114
148
  const modules = this.moduleLoader.getAllModules()
115
149
  const promises: PromiseLike<any>[] = []
@@ -161,6 +195,7 @@ export class NaviosApplication {
161
195
  if (!this.server) {
162
196
  throw new Error('Server is not initialized. Call init() first.')
163
197
  }
164
- await this.server.listen(options)
198
+ const res = await this.server.listen(options)
199
+ this.logger.debug(`Navios is listening on ${res}`)
165
200
  }
166
201
  }
@@ -1,4 +1,4 @@
1
- import type { AnyZodObject, input, output } from 'zod'
1
+ import type { AnyZodObject, output, z, ZodOptional } from 'zod'
2
2
 
3
3
  import type { FactoryNotFound, UnknownError } from './index.mjs'
4
4
  import type { InjectionToken } from './injection-token.mjs'
@@ -33,11 +33,17 @@ export class ProxyServiceLocator implements ServiceLocator {
33
33
  ): void {
34
34
  return this.serviceLocator.registerAbstractFactory(token, factory)
35
35
  }
36
- public getInstance<Instance, Schema extends AnyZodObject | undefined>(
36
+ public getInstance<
37
+ Instance,
38
+ Schema extends AnyZodObject | ZodOptional<AnyZodObject> | undefined,
39
+ >(
37
40
  token: InjectionToken<Instance, Schema>,
38
- args: Schema extends AnyZodObject ? input<Schema> : undefined,
41
+ args: Schema extends AnyZodObject
42
+ ? z.input<Schema>
43
+ : Schema extends ZodOptional<AnyZodObject>
44
+ ? z.input<Schema> | undefined
45
+ : undefined,
39
46
  ): Promise<[undefined, Instance] | [UnknownError | FactoryNotFound]> {
40
- // @ts-expect-error
41
47
  return this.ctx.inject(token, args).then(
42
48
  (instance) => {
43
49
  return [undefined, instance]
@@ -47,16 +53,29 @@ export class ProxyServiceLocator implements ServiceLocator {
47
53
  },
48
54
  )
49
55
  }
50
- public getOrThrowInstance<Instance, Schema extends AnyZodObject | undefined>(
56
+ public getOrThrowInstance<
57
+ Instance,
58
+ Schema extends AnyZodObject | ZodOptional<AnyZodObject> | undefined,
59
+ >(
51
60
  token: InjectionToken<Instance, Schema>,
52
- args: Schema extends AnyZodObject ? input<Schema> : undefined,
61
+ args: Schema extends AnyZodObject
62
+ ? z.input<Schema>
63
+ : Schema extends ZodOptional<AnyZodObject>
64
+ ? z.input<Schema> | undefined
65
+ : undefined,
53
66
  ): Promise<Instance> {
54
- // @ts-expect-error We need to pass the args to the ctx.inject method
55
67
  return this.ctx.inject(token, args)
56
68
  }
57
- public getSyncInstance<Instance, Schema extends AnyZodObject | undefined>(
69
+ public getSyncInstance<
70
+ Instance,
71
+ Schema extends AnyZodObject | ZodOptional<AnyZodObject> | undefined,
72
+ >(
58
73
  token: InjectionToken<Instance, Schema>,
59
- args: Schema extends AnyZodObject ? input<Schema> : undefined,
74
+ args: Schema extends AnyZodObject
75
+ ? z.input<Schema>
76
+ : Schema extends ZodOptional<AnyZodObject>
77
+ ? z.input<Schema> | undefined
78
+ : undefined,
60
79
  ): Instance | null {
61
80
  return this.serviceLocator.getSyncInstance(token, args)
62
81
  }
@@ -456,7 +456,15 @@ export class ServiceLocator {
456
456
  makeInstanceName(token: InjectionToken<any, any>, args: any) {
457
457
  let stringifiedArgs = args
458
458
  ? ':' +
459
- JSON.stringify(args)
459
+ JSON.stringify(args, (_, value) => {
460
+ if (typeof value === 'function') {
461
+ return `function:${value.name}(${value.length})`
462
+ }
463
+ if (typeof value === 'symbol') {
464
+ return value.toString()
465
+ }
466
+ return value
467
+ })
460
468
  .replaceAll(/"/g, '')
461
469
  .replaceAll(/:/g, '=')
462
470
  .replaceAll(/,/g, '|')