diff --git a/apps/api/package.json b/apps/api/package.json index 45fd8edcd..25e1689a7 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -62,10 +62,12 @@ "pg": "^8.12.0", "pg-hstore": "^2.3.4", "pino": "^9.2.0", + "pino-pretty": "^11.2.1", "protobufjs": "^6.11.2", "semver": "^7.3.8", "sequelize": "^6.21.3", "sequelize-typescript": "^2.1.5", + "sql-formatter": "^15.3.2", "stripe": "^10.14.0", "tsyringe": "^4.8.0", "uuid": "^9.0.1" diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 65a811283..a0bfb96b8 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -8,10 +8,9 @@ import { sentry } from "@hono/sentry"; import * as Sentry from "@sentry/node"; import { Hono } from "hono"; import { cors } from "hono/cors"; +import { container } from "tsyringe"; -import { logHttpRequests } from "@src/core/services/log-http-requests/logHttpRequests"; -import { Logger } from "@src/core/services/logger/Logger"; -import { RequestStorage } from "@src/core/services/request-storage/RequestStorage"; +import { HttpLoggerService, LoggerService, RequestStorageService } from "@src/core"; import packageJson from "../package.json"; import { chainDb, syncUserSchema, userDb } from "./db/dbConnection"; import { apiRouter } from "./routers/apiRouter"; @@ -61,8 +60,8 @@ const scheduler = new Scheduler({ } }); -appHono.use(RequestStorage.middleware()); -appHono.use(logHttpRequests()); +appHono.use(container.resolve(RequestStorageService).intercept()); +appHono.use(container.resolve(HttpLoggerService).intercept()); appHono.use( "*", sentry({ @@ -104,7 +103,7 @@ function startScheduler() { scheduler.start(); } -const appLogger = new Logger({ context: "APP" }); +const appLogger = new LoggerService({ context: "APP" }); /** * Initialize database diff --git a/apps/api/src/billing/config/env.ts b/apps/api/src/billing/config/env.config.ts similarity index 91% rename from apps/api/src/billing/config/env.ts rename to apps/api/src/billing/config/env.config.ts index a2e28b833..7376a505b 100644 --- a/apps/api/src/billing/config/env.ts +++ b/apps/api/src/billing/config/env.config.ts @@ -15,4 +15,4 @@ const envSchema = z.object({ GAS_SAFETY_MULTIPLIER: z.number({ coerce: true }).default(1.5) }); -export const env = envSchema.parse(process.env); +export const envConfig = envSchema.parse(process.env); diff --git a/apps/api/src/billing/config/index.ts b/apps/api/src/billing/config/index.ts index 935c8cda0..8e88be4c8 100644 --- a/apps/api/src/billing/config/index.ts +++ b/apps/api/src/billing/config/index.ts @@ -1,7 +1,7 @@ -import { env } from "./env"; -import { USDC_IBC_DENOMS } from "./network"; +import { envConfig } from "./env.config"; +import { USDC_IBC_DENOMS } from "./network.config"; export const config = { - ...env, + ...envConfig, USDC_IBC_DENOMS }; diff --git a/apps/api/src/billing/config/network.ts b/apps/api/src/billing/config/network.config.ts similarity index 100% rename from apps/api/src/billing/config/network.ts rename to apps/api/src/billing/config/network.config.ts diff --git a/apps/api/src/billing/controllers/wallet/WalletController.ts b/apps/api/src/billing/controllers/wallet/wallet.controller.ts similarity index 84% rename from apps/api/src/billing/controllers/wallet/WalletController.ts rename to apps/api/src/billing/controllers/wallet/wallet.controller.ts index 7cd5e3789..73e201a54 100644 --- a/apps/api/src/billing/controllers/wallet/WalletController.ts +++ b/apps/api/src/billing/controllers/wallet/wallet.controller.ts @@ -3,15 +3,15 @@ import { singleton } from "tsyringe"; import { UserWalletRepository } from "@src/billing/repositories"; import { CreateWalletInput, CreateWalletOutput } from "@src/billing/routes"; -import { WalletInitializer, WalletManager } from "@src/billing/services"; +import { WalletInitializerService, WalletService } from "@src/billing/services"; import { WithTransaction } from "@src/core/services"; @singleton() export class WalletController { constructor( - private readonly walletManager: WalletManager, + private readonly walletManager: WalletService, private readonly userWalletRepository: UserWalletRepository, - private readonly walletInitializer: WalletInitializer + private readonly walletInitializer: WalletInitializerService ) {} @WithTransaction() diff --git a/apps/api/src/billing/model-schemas/index.ts b/apps/api/src/billing/model-schemas/index.ts index c88a47c41..b50d9db48 100644 --- a/apps/api/src/billing/model-schemas/index.ts +++ b/apps/api/src/billing/model-schemas/index.ts @@ -1 +1 @@ -export * from "./userWalletSchema"; +export * from "./user-wallet/user-wallet.schema"; diff --git a/apps/api/src/billing/model-schemas/userWalletSchema.ts b/apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts similarity index 100% rename from apps/api/src/billing/model-schemas/userWalletSchema.ts rename to apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts diff --git a/apps/api/src/billing/models/index.ts b/apps/api/src/billing/models/index.ts index 7c6aea0d1..adfe57585 100644 --- a/apps/api/src/billing/models/index.ts +++ b/apps/api/src/billing/models/index.ts @@ -1 +1 @@ -export * from "./user-wallet/UserWalletModel"; +export * from "./user-wallet/user-wallet.model"; diff --git a/apps/api/src/billing/models/user-wallet/UserWalletModel.ts b/apps/api/src/billing/models/user-wallet/user-wallet.model.ts similarity index 100% rename from apps/api/src/billing/models/user-wallet/UserWalletModel.ts rename to apps/api/src/billing/models/user-wallet/user-wallet.model.ts diff --git a/apps/api/src/billing/providers/configProvider.ts b/apps/api/src/billing/providers/config.provider.ts similarity index 100% rename from apps/api/src/billing/providers/configProvider.ts rename to apps/api/src/billing/providers/config.provider.ts diff --git a/apps/api/src/billing/providers/index.ts b/apps/api/src/billing/providers/index.ts index b28f1ba2a..2c8f88597 100644 --- a/apps/api/src/billing/providers/index.ts +++ b/apps/api/src/billing/providers/index.ts @@ -1,5 +1,5 @@ -import "./userWalletSchemaProvider"; -import "./configProvider"; +import "./user-wallet-schema.provider"; +import "./config.provider"; -export * from "./userWalletSchemaProvider"; -export * from "./configProvider"; +export * from "./user-wallet-schema.provider"; +export * from "./config.provider"; diff --git a/apps/api/src/billing/providers/userWalletSchemaProvider.ts b/apps/api/src/billing/providers/user-wallet-schema.provider.ts similarity index 100% rename from apps/api/src/billing/providers/userWalletSchemaProvider.ts rename to apps/api/src/billing/providers/user-wallet-schema.provider.ts diff --git a/apps/api/src/billing/repositories/index.ts b/apps/api/src/billing/repositories/index.ts index 446d923ae..60853884a 100644 --- a/apps/api/src/billing/repositories/index.ts +++ b/apps/api/src/billing/repositories/index.ts @@ -1 +1 @@ -export * from "./user-wallet/UserWalletRepository"; +export * from "./user-wallet/user-wallet.repository"; diff --git a/apps/api/src/billing/repositories/user-wallet/UserWalletRepository.ts b/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts similarity index 94% rename from apps/api/src/billing/repositories/user-wallet/UserWalletRepository.ts rename to apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts index 8527fb67c..c1ec7836a 100644 --- a/apps/api/src/billing/repositories/user-wallet/UserWalletRepository.ts +++ b/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts @@ -4,7 +4,7 @@ import { singleton } from "tsyringe"; import { InjectUserWalletSchema, UserWalletSchema } from "@src/billing/providers"; import { ApiPgDatabase, InjectPg } from "@src/core/providers"; -import { TxManager } from "@src/core/services"; +import { TxService } from "@src/core/services"; export type UserInput = Partial; @@ -13,7 +13,7 @@ export class UserWalletRepository { constructor( @InjectPg() private readonly pg: ApiPgDatabase, @InjectUserWalletSchema() private readonly userWallet: UserWalletSchema, - private readonly txManager: TxManager + private readonly txManager: TxService ) {} async create(input: Pick) { diff --git a/apps/api/src/billing/routes/index.ts b/apps/api/src/billing/routes/index.ts index b479fdc13..4dae6508c 100644 --- a/apps/api/src/billing/routes/index.ts +++ b/apps/api/src/billing/routes/index.ts @@ -1 +1 @@ -export * from "./wallet"; +export * from "./wallet/wallet.router"; diff --git a/apps/api/src/billing/routes/wallet.ts b/apps/api/src/billing/routes/wallet/wallet.router.ts similarity index 97% rename from apps/api/src/billing/routes/wallet.ts rename to apps/api/src/billing/routes/wallet/wallet.router.ts index dcf83080d..74d6960a0 100644 --- a/apps/api/src/billing/routes/wallet.ts +++ b/apps/api/src/billing/routes/wallet/wallet.router.ts @@ -2,7 +2,7 @@ import { createRoute, OpenAPIHono } from "@hono/zod-openapi"; import { container } from "tsyringe"; import { z } from "zod"; -import { WalletController } from "@src/billing/controllers/wallet/WalletController"; +import { WalletController } from "@src/billing/controllers/wallet/wallet.controller"; export const CreateWalletInputSchema = z.object({ userId: z.string().openapi({}) diff --git a/apps/api/src/billing/services/index.ts b/apps/api/src/billing/services/index.ts index f91a55b2c..563ab0262 100644 --- a/apps/api/src/billing/services/index.ts +++ b/apps/api/src/billing/services/index.ts @@ -1,3 +1,3 @@ -export * from "./wallet-manager/WalletManager"; -export * from "./rpc-message-service/RpcMessageService"; -export * from "./wallet-initializer/WalletInitializer"; +export * from "@src/billing/services/wallet/wallet.service"; +export * from "./rpc-message-service/rpc-message.service"; +export * from "./wallet-initializer/wallet-initializer.service"; diff --git a/apps/api/src/billing/services/rpc-message-service/RpcMessageService.ts b/apps/api/src/billing/services/rpc-message-service/rpc-message.service.ts similarity index 100% rename from apps/api/src/billing/services/rpc-message-service/RpcMessageService.ts rename to apps/api/src/billing/services/rpc-message-service/rpc-message.service.ts diff --git a/apps/api/src/billing/services/wallet-initializer/WalletInitializer.ts b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts similarity index 81% rename from apps/api/src/billing/services/wallet-initializer/WalletInitializer.ts rename to apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts index 8847e0ae0..aa2bd8a96 100644 --- a/apps/api/src/billing/services/wallet-initializer/WalletInitializer.ts +++ b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts @@ -1,13 +1,13 @@ import { singleton } from "tsyringe"; import { UserInput, UserWalletRepository } from "@src/billing/repositories"; -import { WalletManager } from "@src/billing/services"; +import { WalletService } from "@src/billing/services"; import { WithTransaction } from "@src/core/services"; @singleton() -export class WalletInitializer { +export class WalletInitializerService { constructor( - private readonly walletManager: WalletManager, + private readonly walletManager: WalletService, private readonly userWalletRepository: UserWalletRepository ) {} diff --git a/apps/api/src/billing/services/wallet-manager/WalletManager.ts b/apps/api/src/billing/services/wallet/wallet.service.ts similarity index 93% rename from apps/api/src/billing/services/wallet-manager/WalletManager.ts rename to apps/api/src/billing/services/wallet/wallet.service.ts index a3c54a0f7..9269a98e0 100644 --- a/apps/api/src/billing/services/wallet-manager/WalletManager.ts +++ b/apps/api/src/billing/services/wallet/wallet.service.ts @@ -5,8 +5,8 @@ import add from "date-fns/add"; import { singleton } from "tsyringe"; import { BillingConfig, InjectBillingConfig } from "@src/billing/providers"; -import { RpcMessageService } from "@src/billing/services/rpc-message-service/RpcMessageService"; -import { ContextualLogger } from "@src/core/services"; +import { RpcMessageService } from "@src/billing/services/rpc-message-service/rpc-message.service"; +import { ContextualLoggerService } from "@src/core/services"; interface SpendingAuthorizationOptions { address: string; @@ -18,7 +18,7 @@ interface SpendingAuthorizationOptions { } @singleton() -export class WalletManager { +export class WalletService { private readonly PREFIX = "akash"; private readonly HD_PATH = "m/44'/118'/0'/0"; @@ -27,12 +27,13 @@ export class WalletManager { private client: SigningStargateClient; - private readonly logger = new ContextualLogger({ context: WalletManager.name }); - constructor( @InjectBillingConfig() private readonly config: BillingConfig, - private readonly rpcMessageService: RpcMessageService - ) {} + private readonly rpcMessageService: RpcMessageService, + private readonly logger: ContextualLoggerService + ) { + this.logger.setContext({ context: WalletService.name }); + } async createAndAuthorizeTrialSpending({ addressIndex }: { addressIndex: number }) { const { address } = await this.createWallet({ addressIndex }); diff --git a/apps/api/src/console.ts b/apps/api/src/console.ts index 83eaf854c..319e014ef 100644 --- a/apps/api/src/console.ts +++ b/apps/api/src/console.ts @@ -3,8 +3,8 @@ import "reflect-metadata"; import { Command } from "commander"; import { container } from "tsyringe"; -import { WalletController } from "@src/billing/controllers/wallet/WalletController"; -import { migrateDb } from "@src/core/services/postgres/postgres"; +import { WalletController } from "@src/billing/controllers/wallet/wallet.controller"; +import { PostgresMigratorService } from "@src/core"; const program = new Command(); @@ -14,7 +14,7 @@ program .command("refill-wallets") .description("Refill draining wallets") .action(async () => { - await migrateDb(); + await container.resolve(PostgresMigratorService).migrate(); await container.resolve(WalletController).refillAll(); }); diff --git a/apps/api/src/core/config/env.ts b/apps/api/src/core/config/env.config.ts similarity index 69% rename from apps/api/src/core/config/env.ts rename to apps/api/src/core/config/env.config.ts index 9d19d1328..d02eddce3 100644 --- a/apps/api/src/core/config/env.ts +++ b/apps/api/src/core/config/env.config.ts @@ -6,7 +6,8 @@ dotenv.config(); const envSchema = z.object({ LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).optional().default("info"), + LOG_FORMAT: z.enum(["json", "pretty"]).optional().default("json"), POSTGRES_DB_URI: z.string() }); -export const env = envSchema.parse(process.env); +export const envConfig = envSchema.parse(process.env); diff --git a/apps/api/src/core/config/index.ts b/apps/api/src/core/config/index.ts index 61aeccbe0..70d5f7bd2 100644 --- a/apps/api/src/core/config/index.ts +++ b/apps/api/src/core/config/index.ts @@ -1,6 +1,5 @@ -import { env } from "./env"; +import { envConfig } from "./env.config"; export const config = { - ...env + ...envConfig }; -export type CoreConfig = typeof config; diff --git a/apps/api/src/core/index.ts b/apps/api/src/core/index.ts index db9936ecf..6928ee035 100644 --- a/apps/api/src/core/index.ts +++ b/apps/api/src/core/index.ts @@ -1,3 +1,3 @@ -import "./providers"; +export * from "./providers"; export * from "./services"; diff --git a/apps/api/src/core/providers/PostgresProvider.ts b/apps/api/src/core/providers/PostgresProvider.ts deleted file mode 100644 index 16f2dd375..000000000 --- a/apps/api/src/core/providers/PostgresProvider.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { container, inject } from "tsyringe"; - -import { pgDatabase } from "@src/core/services/postgres/postgres"; - -export const POSTGRES_DB = "POSTGRES_DB"; -container.register(POSTGRES_DB, { useValue: pgDatabase }); - -export const InjectPg = () => inject(POSTGRES_DB); -export type ApiPgDatabase = typeof pgDatabase; diff --git a/apps/api/src/core/providers/configProvider.ts b/apps/api/src/core/providers/config.provider.ts similarity index 81% rename from apps/api/src/core/providers/configProvider.ts rename to apps/api/src/core/providers/config.provider.ts index 69745c8a3..f8471d4cb 100644 --- a/apps/api/src/core/providers/configProvider.ts +++ b/apps/api/src/core/providers/config.provider.ts @@ -5,3 +5,5 @@ import { config } from "@src/core/config"; export const CORE_CONFIG = "CORE_CONFIG"; container.register(CORE_CONFIG, { useValue: config }); + +export type CoreConfig = typeof config; diff --git a/apps/api/src/core/providers/index.ts b/apps/api/src/core/providers/index.ts index d17adece0..bf289a425 100644 --- a/apps/api/src/core/providers/index.ts +++ b/apps/api/src/core/providers/index.ts @@ -1,2 +1,2 @@ -export * from "./PostgresProvider"; -export * from "./configProvider"; +export * from "./postgres.provider"; +export * from "./config.provider"; diff --git a/apps/api/src/core/providers/postgres.provider.ts b/apps/api/src/core/providers/postgres.provider.ts new file mode 100644 index 000000000..1f70af22e --- /dev/null +++ b/apps/api/src/core/providers/postgres.provider.ts @@ -0,0 +1,22 @@ +import { DefaultLogger } from "drizzle-orm/logger"; +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; +import { container, inject } from "tsyringe"; + +import * as billingSchemas from "@src/billing/model-schemas"; +import { config } from "@src/core/config"; +import { PostgresLoggerService } from "@src/core/services/postgres-logger/postgres-logger.service"; + +const pool = new Pool({ + connectionString: config.POSTGRES_DB_URI +}); + +export const pgDatabase = drizzle(pool, { logger: new DefaultLogger({ writer: container.resolve(PostgresLoggerService) }), schema: billingSchemas }); + +export type ApiPgSchema = typeof billingSchemas; + +export const POSTGRES_DB = "POSTGRES_DB"; +container.register(POSTGRES_DB, { useValue: pgDatabase }); + +export const InjectPg = () => inject(POSTGRES_DB); +export type ApiPgDatabase = typeof pgDatabase; diff --git a/apps/api/src/core/services/contextual-logger/ContextualLogger.ts b/apps/api/src/core/services/contextual-logger/ContextualLogger.ts deleted file mode 100644 index 2ed00e0f2..000000000 --- a/apps/api/src/core/services/contextual-logger/ContextualLogger.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Logger } from "@src/core/services/logger/Logger"; -import { RequestStorage } from "@src/core/services/request-storage/RequestStorage"; - -export class ContextualLogger extends Logger { - protected get logger() { - const context = RequestStorage.getContext(); - const requestId = context?.get("requestId"); - - return requestId ? super.logger.child({ requestId }) : super.logger; - } -} diff --git a/apps/api/src/core/services/contextual-logger/contextual-logger.service.ts b/apps/api/src/core/services/contextual-logger/contextual-logger.service.ts new file mode 100644 index 000000000..1e51c4b00 --- /dev/null +++ b/apps/api/src/core/services/contextual-logger/contextual-logger.service.ts @@ -0,0 +1,32 @@ +import { Bindings } from "pino"; +import { injectable } from "tsyringe"; + +import { LoggerService } from "@src/core/services/logger/logger.service"; +import { RequestStorageService } from "@src/core/services/request-storage/request-storage.service"; + +@injectable() +export class ContextualLoggerService extends LoggerService { + constructor(private readonly requestStorage: RequestStorageService) { + super(); + } + + setContext(bindings: Bindings) { + this.pino = this.pino.child(bindings); + } + + protected toLoggableInput(message: any) { + const { context } = this.requestStorage; + const requestId = context?.get("requestId"); + const loggableMessage = super.toLoggableInput(message); + + if (!requestId) { + return loggableMessage; + } + + if (typeof loggableMessage === "object" && !Array.isArray(loggableMessage)) { + return { requestId, ...loggableMessage }; + } + + return { requestId, msg: loggableMessage }; + } +} diff --git a/apps/api/src/core/services/http-logger/http-logger.service.ts b/apps/api/src/core/services/http-logger/http-logger.service.ts new file mode 100644 index 000000000..3fc7061ed --- /dev/null +++ b/apps/api/src/core/services/http-logger/http-logger.service.ts @@ -0,0 +1,29 @@ +import { Context, Next } from "hono"; +import { singleton } from "tsyringe"; + +import { ContextualLoggerService } from "@src/core/services"; +import type { HonoInterceptor } from "@src/core/types/hono-interceptor.type"; + +@singleton() +export class HttpLoggerService implements HonoInterceptor { + constructor(private readonly logger: ContextualLoggerService) { + logger.setContext({ context: "HTTP" }); + } + + intercept() { + return async (c: Context, next: Next) => { + const timer = performance.now(); + this.logger.info({ + method: c.req.method, + url: c.req.url + }); + + await next(); + + this.logger.info({ + status: c.res.status, + duration: `${(performance.now() - timer).toFixed(3)}ms` + }); + }; + } +} diff --git a/apps/api/src/core/services/index.ts b/apps/api/src/core/services/index.ts index a184047c8..6412e53d5 100644 --- a/apps/api/src/core/services/index.ts +++ b/apps/api/src/core/services/index.ts @@ -1,6 +1,6 @@ -export * from "./tx-manager/TxManager"; -export * from "./logger/Logger"; -export * from "./contextual-logger/ContextualLogger"; -export * from "./postgres/postgres"; -export * from "./log-http-requests/logHttpRequests"; -export * from "./request-storage/RequestStorage"; +export * from "@src/core/services/tx/tx.service"; +export * from "./logger/logger.service"; +export * from "./contextual-logger/contextual-logger.service"; +export * from "@src/core/services/http-logger/http-logger.service"; +export * from "./request-storage/request-storage.service"; +export * from "./postgres-migrator/postgres-migrator.service"; diff --git a/apps/api/src/core/services/log-http-requests/logHttpRequests.ts b/apps/api/src/core/services/log-http-requests/logHttpRequests.ts deleted file mode 100644 index 46e87b680..000000000 --- a/apps/api/src/core/services/log-http-requests/logHttpRequests.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Context, Next } from "hono"; - -import { ContextualLogger } from "@src/core/services"; - -export const logHttpRequests = () => async (c: Context, next: Next) => { - const logger = new ContextualLogger({ context: "HTTP" }); - const timer = performance.now(); - logger.info({ - method: c.req.method, - url: c.req.url - }); - - await next(); - - logger.info({ - status: c.res.status, - duration: `${(performance.now() - timer).toFixed(3)}ms` - }); -}; diff --git a/apps/api/src/core/services/logger/Logger.ts b/apps/api/src/core/services/logger/Logger.ts deleted file mode 100644 index 47d596e90..000000000 --- a/apps/api/src/core/services/logger/Logger.ts +++ /dev/null @@ -1,44 +0,0 @@ -import pino, { Bindings } from "pino"; - -import { config } from "@src/core/config"; - -export class Logger { - private readonly pino: pino.Logger; - - protected get logger() { - return this.pino; - } - - constructor(bindings?: Bindings) { - this.pino = pino({ level: config.LOG_LEVEL }); - - if (bindings) { - this.pino = this.logger.child(bindings); - } - } - - info(message: any) { - message = this.toLoggableInput(message); - return this.logger.info(message); - } - - error(message: any) { - this.logger.error(this.toLoggableInput(message)); - } - - warn(message: any) { - return this.logger.warn(this.toLoggableInput(message)); - } - - debug(message: any) { - return this.logger.debug(this.toLoggableInput(message)); - } - - private toLoggableInput(message: any) { - if (message instanceof Error) { - return message.stack; - } - - return message; - } -} diff --git a/apps/api/src/core/services/logger/logger.service.ts b/apps/api/src/core/services/logger/logger.service.ts new file mode 100644 index 000000000..32052addc --- /dev/null +++ b/apps/api/src/core/services/logger/logger.service.ts @@ -0,0 +1,50 @@ +import pino, { Bindings, LoggerOptions } from "pino"; + +import { config } from "@src/core/config"; + +export class LoggerService { + protected pino: pino.Logger; + + readonly isPretty = config.LOG_FORMAT === "pretty"; + + constructor(bindings?: Bindings) { + const options: LoggerOptions = { level: config.LOG_LEVEL }; + + if (this.isPretty) { + options.transport = { + target: "pino-pretty" + }; + } + + this.pino = pino(options); + + if (bindings) { + this.pino = this.pino.child(bindings); + } + } + + info(message: any) { + message = this.toLoggableInput(message); + return this.pino.info(message); + } + + error(message: any) { + this.pino.error(this.toLoggableInput(message)); + } + + warn(message: any) { + return this.pino.warn(this.toLoggableInput(message)); + } + + debug(message: any) { + return this.pino.debug(this.toLoggableInput(message)); + } + + protected toLoggableInput(message: any) { + if (message instanceof Error) { + return message.stack; + } + + return message; + } +} diff --git a/apps/api/src/core/services/postgres/postgres.ts b/apps/api/src/core/services/postgres/postgres.ts deleted file mode 100644 index 63eb2895c..000000000 --- a/apps/api/src/core/services/postgres/postgres.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { DefaultLogger, LogWriter } from "drizzle-orm/logger"; -import { drizzle } from "drizzle-orm/node-postgres"; -import { migrate } from "drizzle-orm/node-postgres/migrator"; -import { Pool } from "pg"; - -import * as billingSchemas from "@src/billing/model-schemas"; -import { config } from "@src/core/config"; -import { ContextualLogger } from "@src/core/services/contextual-logger/ContextualLogger"; - -const pool = new Pool({ - connectionString: config.POSTGRES_DB_URI -}); - -class ContextualLogWriter implements LogWriter { - logger = new ContextualLogger({ context: "POSTGRES" }); - - write(message: string) { - this.logger.debug({ query: message.replace(/^Query: /, "") }); - } -} - -export const pgDatabase = drizzle(pool, { logger: new DefaultLogger({ writer: new ContextualLogWriter() }), schema: billingSchemas }); -export const migrateDb = () => migrate(pgDatabase, { migrationsFolder: "./drizzle" }); - -export type ApiPgSchema = typeof billingSchemas; diff --git a/apps/api/src/core/services/request-storage/RequestStorage.ts b/apps/api/src/core/services/request-storage/request-storage.service.ts similarity index 59% rename from apps/api/src/core/services/request-storage/RequestStorage.ts rename to apps/api/src/core/services/request-storage/request-storage.service.ts index 03bdea7a6..19444b79b 100644 --- a/apps/api/src/core/services/request-storage/RequestStorage.ts +++ b/apps/api/src/core/services/request-storage/request-storage.service.ts @@ -1,13 +1,21 @@ import { Context, Next } from "hono"; import { AsyncLocalStorage } from "node:async_hooks"; +import { singleton } from "tsyringe"; import { v4 as uuid } from "uuid"; -export class RequestStorage { - static CONTEXT_KEY = "CONTEXT"; +import type { HonoInterceptor } from "@src/core/types/hono-interceptor.type"; - static storage = new AsyncLocalStorage>(); +@singleton() +export class RequestStorageService implements HonoInterceptor { + private readonly CONTEXT_KEY = "CONTEXT"; - static middleware() { + private readonly storage = new AsyncLocalStorage>(); + + get context() { + return this.storage.getStore()?.get(this.CONTEXT_KEY); + } + + intercept() { return async (c: Context, next: Next) => { const requestId = c.req.header("X-Request-Id") || uuid(); c.set("requestId", requestId); @@ -16,7 +24,7 @@ export class RequestStorage { }; } - static async runWithContext(context: Context, cb: () => Promise) { + private async runWithContext(context: Context, cb: () => Promise) { return await new Promise((resolve, reject) => { this.storage.run(new Map(), () => { this.storage.getStore().set(this.CONTEXT_KEY, context); @@ -24,8 +32,4 @@ export class RequestStorage { }); }); } - - static getContext() { - return this.storage.getStore()?.get(this.CONTEXT_KEY); - } } diff --git a/apps/api/src/core/services/tx-manager/TxManager.ts b/apps/api/src/core/services/tx/tx.service.ts similarity index 86% rename from apps/api/src/core/services/tx-manager/TxManager.ts rename to apps/api/src/core/services/tx/tx.service.ts index f3bb49ac4..803af9418 100644 --- a/apps/api/src/core/services/tx-manager/TxManager.ts +++ b/apps/api/src/core/services/tx/tx.service.ts @@ -4,13 +4,12 @@ import type { PgTransaction } from "drizzle-orm/pg-core"; import { AsyncLocalStorage } from "node:async_hooks"; import { container, singleton } from "tsyringe"; -import { ApiPgDatabase, InjectPg } from "@src/core/providers"; -import type { ApiPgSchema } from "@src/core/services"; +import { ApiPgDatabase, ApiPgSchema, InjectPg } from "@src/core/providers/postgres.provider"; type TxType = "PG_TX"; @singleton() -export class TxManager { +export class TxService { private readonly storage = new AsyncLocalStorage>>>(); constructor(@InjectPg() private readonly pg: ApiPgDatabase) {} @@ -36,7 +35,7 @@ export function WithTransaction() { const originalMethod = descriptor.value; descriptor.value = async function (...args: unknown[]) { - const txManager = container.resolve(TxManager); + const txManager = container.resolve(TxService); return txManager.transaction(() => originalMethod.apply(this, args)); }; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index a38b44d8b..041d547ea 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,4 +1,11 @@ +import "reflect-metadata"; + +import { container } from "tsyringe"; + import { initApp } from "@src/app"; -import { migrateDb } from "@src/core/services/postgres/postgres"; +import { PostgresMigratorService } from "@src/core"; -migrateDb().then(() => initApp()); +container + .resolve(PostgresMigratorService) + .migrate() + .then(() => initApp()); diff --git a/apps/api/test/functional/create-wallet.spec.ts b/apps/api/test/functional/create-wallet.spec.ts index 2dc39bd1c..625f7d1f2 100644 --- a/apps/api/test/functional/create-wallet.spec.ts +++ b/apps/api/test/functional/create-wallet.spec.ts @@ -4,7 +4,7 @@ import { container } from "tsyringe"; import { app, initDb } from "@src/app"; import { USER_WALLET_SCHEMA, UserWalletSchema } from "@src/billing/providers"; -import { ApiPgDatabase, POSTGRES_DB } from "@src/core/providers/PostgresProvider"; +import { ApiPgDatabase, POSTGRES_DB } from "@src/core"; import { closeConnections } from "@src/db/dbConnection"; jest.setTimeout(30000); diff --git a/apps/api/test/setup-functional-tests.ts b/apps/api/test/setup-functional-tests.ts index 02ca42de0..d5e7ec147 100644 --- a/apps/api/test/setup-functional-tests.ts +++ b/apps/api/test/setup-functional-tests.ts @@ -1,11 +1,12 @@ import "reflect-metadata"; import dotenv from "dotenv"; +import { container } from "tsyringe"; -import { migrateDb } from "@src/core/services/postgres/postgres"; +import { PostgresMigratorService } from "@src/core"; dotenv.config({ path: ".env.functional.test" }); beforeAll(async () => { - await migrateDb(); + await container.resolve(PostgresMigratorService).migrate(); }); diff --git a/package-lock.json b/package-lock.json index 6bbf9b6cb..f1a27e79f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,10 +61,12 @@ "pg": "^8.12.0", "pg-hstore": "^2.3.4", "pino": "^9.2.0", + "pino-pretty": "^11.2.1", "protobufjs": "^6.11.2", "semver": "^7.3.8", "sequelize": "^6.21.3", "sequelize-typescript": "^2.1.5", + "sql-formatter": "^15.3.2", "stripe": "^10.14.0", "tsyringe": "^4.8.0", "uuid": "^9.0.1" @@ -19620,8 +19622,7 @@ "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" }, "node_modules/colors": { "version": "1.4.0", @@ -20599,6 +20600,14 @@ "date-fns": ">=2.0.0" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "engines": { + "node": "*" + } + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -21025,6 +21034,11 @@ "node": ">=8" } }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==" + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -22806,6 +22820,11 @@ "node": ">=0.10.0" } }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -23470,6 +23489,17 @@ "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==" }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -23916,6 +23946,11 @@ "he": "bin/he" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, "node_modules/hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -26955,6 +26990,14 @@ } } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "engines": { + "node": ">=10" + } + }, "node_modules/js-crypto-env": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/js-crypto-env/-/js-crypto-env-0.3.2.tgz", @@ -29185,6 +29228,11 @@ "integrity": "sha512-2I8/T3X/hLxB2oPHgqcNYUVdA/ZEFShT7IAujifIPMfKkNbLOqY8XCoyHCXrsdjb36dW9MwoTwBCFpXKMwNwaQ==", "peer": true }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, "node_modules/motion": { "version": "10.16.2", "resolved": "https://registry.npmjs.org/motion/-/motion-10.16.2.tgz", @@ -29318,6 +29366,32 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/nearley/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -30975,6 +31049,78 @@ "split2": "^4.0.0" } }, + "node_modules/pino-pretty": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.2.1.tgz", + "integrity": "sha512-O05NuD9tkRasFRWVaF/uHLOvoRDFD7tb5VMertr78rbsYFjYp48Vg3477EshVAF5eZaEw+OpDl/tu+B0R5o+7g==", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pino-pretty/node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-pretty/node_modules/sonic-boom": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", + "integrity": "sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/pino-pretty/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/pino-std-serializers": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", @@ -31932,6 +32078,11 @@ "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==" }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==" + }, "node_modules/rainbow-sprinkles": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/rainbow-sprinkles/-/rainbow-sprinkles-0.17.1.tgz", @@ -31942,6 +32093,18 @@ "@vanilla-extract/dynamic": "^2" } }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -32818,7 +32981,6 @@ "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, "engines": { "node": ">=0.12" } @@ -33219,6 +33381,11 @@ "pbts": "bin/pbts" } }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, "node_modules/secure-random": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/secure-random/-/secure-random-1.1.2.tgz", @@ -34022,6 +34189,19 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sql-formatter": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.3.2.tgz", + "integrity": "sha512-pNxSMf5DtwhpZ8gUcOGCGZIWtCcyAUx9oLgAtlO4ag7DvlfnETL0BGqXaISc84pNrXvTWmt8Wal1FWKxdTsL3Q==", + "dependencies": { + "argparse": "^2.0.1", + "get-stdin": "=8.0.0", + "nearley": "^2.20.1" + }, + "bin": { + "sql-formatter": "bin/sql-formatter-cli.cjs" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",