From 5f9e5a4cbb32c8c41a6378474bd805577dc7b2da Mon Sep 17 00:00:00 2001 From: Daniel Constantin Date: Wed, 18 Mar 2026 10:46:02 +0200 Subject: [PATCH 1/3] fix(affiliate-stats): improve error logging for fetching affiliate stats --- .../src/app/routes/affiliate/affiliate-stats/_address/index.ts | 2 +- .../api/src/app/routes/affiliate/trader-stats/_address/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app/routes/affiliate/affiliate-stats/_address/index.ts b/apps/api/src/app/routes/affiliate/affiliate-stats/_address/index.ts index 43f8c132..839e9574 100644 --- a/apps/api/src/app/routes/affiliate/affiliate-stats/_address/index.ts +++ b/apps/api/src/app/routes/affiliate/affiliate-stats/_address/index.ts @@ -54,7 +54,7 @@ const affiliateStats: FastifyPluginAsync = async (fastify): Promise => { lastUpdatedAt: result.lastUpdatedAt, }); } catch (error) { - fastify.log.error('Error fetching affiliate stats:', error); + fastify.log.error({ err: error }, 'Error fetching affiliate stats'); return reply.status(500).send({ message: 'Unexpected error' }); } } diff --git a/apps/api/src/app/routes/affiliate/trader-stats/_address/index.ts b/apps/api/src/app/routes/affiliate/trader-stats/_address/index.ts index 955ea8ed..213f20ad 100644 --- a/apps/api/src/app/routes/affiliate/trader-stats/_address/index.ts +++ b/apps/api/src/app/routes/affiliate/trader-stats/_address/index.ts @@ -54,7 +54,7 @@ const traderStats: FastifyPluginAsync = async (fastify): Promise => { lastUpdatedAt: result.lastUpdatedAt, }); } catch (error) { - fastify.log.error('Error fetching affiliate trader stats:', error); + fastify.log.error({ err: error }, 'Error fetching affiliate trader stats'); return reply.status(500).send({ message: 'Unexpected error' }); } } From 6732502216efe2320ae923db15343f270f131f56 Mon Sep 17 00:00:00 2001 From: Daniel Constantin Date: Wed, 18 Mar 2026 11:03:05 +0200 Subject: [PATCH 2/3] fix(affiliate): for cache to work the service instance must be singleton --- apps/api/src/app/inversify.config.spec.ts | 157 ++++++++++++++++++++++ apps/api/src/app/inversify.config.ts | 7 +- 2 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 apps/api/src/app/inversify.config.spec.ts diff --git a/apps/api/src/app/inversify.config.spec.ts b/apps/api/src/app/inversify.config.spec.ts new file mode 100644 index 00000000..fad2cb26 --- /dev/null +++ b/apps/api/src/app/inversify.config.spec.ts @@ -0,0 +1,157 @@ +import 'reflect-metadata'; +import { decorate, injectable } from 'inversify'; + +const symbol = (name: string): symbol => Symbol.for(name); +const emptyObject = (): Record => ({}); +const getter = () => jest.fn(emptyObject); + +const affiliateStatsServiceSymbol = symbol('AffiliateStatsService'); +const duneRepositorySymbol = symbol('DuneRepository'); + +const getters = { + getAffiliatesRepository: getter(), + getCacheRepository: getter(), + getDuneRepository: jest.fn(() => ({ getQueryResults: jest.fn() })), + getErc20Repository: getter(), + getPushNotificationsRepository: getter(), + getPushSubscriptionsRepository: getter(), + getSimulationRepository: getter(), + getTokenBalancesRepository: getter(), + getTokenHolderRepository: getter(), + getUserBalanceRepository: getter(), + getUsdRepository: getter(), +}; + +const plainClasses = Object.fromEntries( + [ + 'AffiliatesRepository', + 'AffiliateProgramExportService', + 'AffiliateProgramExportServiceImpl', + 'AffiliateStatsService', + 'CacheRepository', + 'DuneRepository', + 'Erc20Repository', + 'HooksService', + 'HooksServiceImpl', + 'Logger', + 'PushNotificationsRepository', + 'PushSubscriptionsRepository', + 'SimulationRepository', + 'SlippageService', + 'SSEService', + 'TokenBalancesRepository', + 'TokenBalancesService', + 'TokenDetailService', + 'TokenHolderRepository', + 'TokenHolderService', + 'UsdRepository', + 'UsdService', + 'UserBalanceRepository', + 'BalanceTrackingService', + ].map((name) => [name, class {}]) +); + +class InjectableStub {} +decorate(injectable(), InjectableStub); + +class MockAffiliateStatsServiceImpl { + static instances: MockAffiliateStatsServiceImpl[] = []; + + constructor( + public readonly duneRepository: unknown, + public readonly cacheTtlMs: number + ) { + MockAffiliateStatsServiceImpl.instances.push(this); + } +} + +const symbols = { + affiliatesRepositorySymbol: symbol('AffiliatesRepository'), + affiliateProgramExportServiceSymbol: symbol('AffiliateProgramExportService'), + affiliateStatsServiceSymbol, + balanceTrackingServiceSymbol: symbol('BalanceTrackingService'), + cacheRepositorySymbol: symbol('CacheRepository'), + duneRepositorySymbol, + erc20RepositorySymbol: symbol('Erc20Repository'), + hooksServiceSymbol: symbol('HooksService'), + pushNotificationsRepositorySymbol: symbol('PushNotificationsRepository'), + pushSubscriptionsRepositorySymbol: symbol('PushSubscriptionsRepository'), + simulationServiceSymbol: symbol('SimulationService'), + slippageServiceSymbol: symbol('SlippageService'), + sseServiceSymbol: symbol('SSEService'), + tenderlyRepositorySymbol: symbol('SimulationRepository'), + tokenBalancesRepositorySymbol: symbol('TokenBalancesRepository'), + tokenBalancesServiceSymbol: symbol('TokenBalancesService'), + tokenDetailServiceSymbol: symbol('TokenDetailService'), + tokenHolderRepositorySymbol: symbol('TokenHolderRepository'), + tokenHolderServiceSymbol: symbol('TokenHolderService'), + usdRepositorySymbol: symbol('UsdRepository'), + usdServiceSymbol: symbol('UsdService'), + userBalanceRepositorySymbol: symbol('UserBalanceRepository'), +}; + +jest.mock('@cowprotocol/repositories', () => ({ + ...plainClasses, + ...symbols, + ...getters, + isCmsEnabled: false, + isDuneEnabled: true, +})); + +jest.mock('@cowprotocol/shared', () => ({ + Logger: plainClasses.Logger, + logger: { warn: jest.fn() }, +})); + +jest.mock('@cowprotocol/services', () => ({ + ...plainClasses, + ...symbols, + ...getters, + AffiliateStatsServiceImpl: MockAffiliateStatsServiceImpl, + BalanceTrackingServiceMain: InjectableStub, + SSEServiceMain: InjectableStub, + SimulationService: InjectableStub, + SlippageServiceMain: InjectableStub, + TokenBalancesServiceMain: InjectableStub, + TokenDetailServiceMain: InjectableStub, + TokenHolderServiceMain: InjectableStub, + UsdServiceMain: InjectableStub, +})); + +describe('getApiContainer', () => { + const originalTtl = process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS; + + beforeEach(() => { + MockAffiliateStatsServiceImpl.instances = []; + process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS = '1234'; + jest.resetModules(); + }); + + afterAll(() => { + if (originalTtl === undefined) { + delete process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS; + return; + } + + process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS = originalTtl; + }); + + it('reuses the same affiliate stats service instance', async () => { + const { getApiContainer } = await import('./inversify.config'); + + const container = getApiContainer(); + const first = container.get( + affiliateStatsServiceSymbol + ); + const second = container.get( + affiliateStatsServiceSymbol + ); + + expect(first).toBe(second); + expect(MockAffiliateStatsServiceImpl.instances).toHaveLength(1); + expect(first.cacheTtlMs).toBe(1234); + expect(first.duneRepository).toBe( + getters.getDuneRepository.mock.results.at(-1)?.value + ); + }); +}); diff --git a/apps/api/src/app/inversify.config.ts b/apps/api/src/app/inversify.config.ts index 68e639dd..65c34f2d 100644 --- a/apps/api/src/app/inversify.config.ts +++ b/apps/api/src/app/inversify.config.ts @@ -74,7 +74,7 @@ import { import { Container } from 'inversify'; import { Logger, logger } from '@cowprotocol/shared'; -const DEFAULT_AFFILIATE_STATS_CACHE_TTL_MS = 3600000; +const DEFAULT_AFFILIATE_STATS_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes function getAffiliateStatsCacheTtlMs(): number { const rawValue = process.env.DUNE_AFFILIATE_STATS_CACHE_TTL_MS; @@ -93,7 +93,7 @@ function getAffiliateStatsCacheTtlMs(): number { return parsed; } -function getApiContainer(): Container { +export function getApiContainer(): Container { const apiContainer = new Container(); // Bind logger @@ -163,7 +163,8 @@ function getApiContainer(): Container { duneRepository, affiliateStatsCacheTtlMs ) - ); + ) + .inSingletonScope(); } if (isDuneEnabled && isCmsEnabled) { From 8a4040223aa6d663c7678b044fe798a5bd350668 Mon Sep 17 00:00:00 2001 From: Daniel Constantin Date: Wed, 29 Apr 2026 13:21:28 +0300 Subject: [PATCH 3/3] ci: integrate prettier as part of eslint --- .eslintrc.json | 10 +++++++++- .prettierignore | 3 ++- .prettierrc | 4 +++- nx.json | 5 +++-- package.json | 4 +++- yarn.lock | 19 +++++++++++++++++++ 6 files changed, 39 insertions(+), 6 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 0be733b7..34c5e093 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,11 +1,13 @@ { "root": true, "ignorePatterns": ["**/*"], - "plugins": ["@nx"], + "extends": ["prettier"], + "plugins": ["@nx", "prettier"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "rules": { + "prettier/prettier": "warn", "@nx/enforce-module-boundaries": [ "error", { @@ -26,6 +28,12 @@ "extends": ["plugin:@nx/typescript"], "rules": {} }, + { + "files": ["libs/repositories/src/gen/**/*.ts"], + "rules": { + "prettier/prettier": "off" + } + }, { "files": ["*.js", "*.jsx"], "extends": ["plugin:@nx/javascript"], diff --git a/.prettierignore b/.prettierignore index 9481e77e..1cece4ad 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ # Add files here to ignore them from prettier formatting /dist -/coverage \ No newline at end of file +/coverage +/libs/repositories/src/gen diff --git a/.prettierrc b/.prettierrc index 544138be..31ba22d8 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,5 @@ { - "singleQuote": true + "semi": false, + "singleQuote": true, + "printWidth": 120 } diff --git a/nx.json b/nx.json index 17ec407b..89d948cd 100644 --- a/nx.json +++ b/nx.json @@ -34,7 +34,8 @@ "inputs": [ "default", "{workspaceRoot}/.eslintrc.json", - "{workspaceRoot}/.eslintignore" + "{workspaceRoot}/.eslintignore", + "{workspaceRoot}/.prettierrc" ] } }, @@ -56,4 +57,4 @@ "appsDir": "apps", "libsDir": "libs" } -} \ No newline at end of file +} diff --git a/package.json b/package.json index c39ba9da..4c0c7166 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@cowprotocol/root", + "name": "cowswap-bff", "description": "Backend for frontend is a series of backend services and libraries that enhance user experience for the frontend", "version": "0.28.0", "license": "MIT", @@ -17,6 +17,7 @@ "build": "nx run-many --all --target=build", "test": "nx run-many --all --target=test", "lint": "nx run-many --all --target=lint", + "lint:fix": "nx run-many --all --target=lint --fix --skip-nx-cache", "new:fastify": "nx generate @nx/node:application --framework=fastify --docker --directory=apps", "new:node": "nx generate @nx/node:application --directory=apps --docker", "new:lib": "nx g @nx/node:library --directory=libs", @@ -98,6 +99,7 @@ "esbuild": "^0.17.17", "eslint": "~8.15.0", "eslint-config-prettier": "8.1.0", + "eslint-plugin-prettier": "^4.2.1", "fastify-tsconfig": "^1.0.1", "jest": "^29.5.0", "jest-environment-node": "^29.4.1", diff --git a/yarn.lock b/yarn.lock index 78009f99..56f09127 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4875,6 +4875,13 @@ eslint-config-prettier@8.1.0: resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz" integrity sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw== +eslint-plugin-prettier@^4.2.1: + version "4.2.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.5.tgz#91ca3f2f01a84f1272cce04e9717550494c0fe06" + integrity sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg== + dependencies: + prettier-linter-helpers "^1.0.0" + eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" @@ -5130,6 +5137,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + fast-glob@3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz" @@ -8013,6 +8025,13 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier-linter-helpers@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz#6a31f88a4bad6c7adda253de12ba4edaea80ebcd" + integrity sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg== + dependencies: + fast-diff "^1.1.2" + prettier@^2.3.1, prettier@^2.6.2: version "2.8.8" resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz"