From b3555daf1d25e7a774dc28c6403263add93732a5 Mon Sep 17 00:00:00 2001 From: ilkinguluzada Date: Tue, 27 May 2025 22:57:50 +0400 Subject: [PATCH] feat(DSD): implement exchange rates --- fullstack/task/jest.config.ts | 16 +- fullstack/task/packages/client/package.json | 4 + fullstack/task/packages/client/src/App.tsx | 36 +- .../ExchangeRates/ExchangeRates.tsx | 125 ++++ .../client/src/hooks/useExchangeRates.ts | 59 ++ fullstack/task/packages/client/src/main.tsx | 45 +- fullstack/task/packages/client/src/theme.ts | 23 + fullstack/task/packages/server/package.json | 5 +- fullstack/task/packages/server/schema.gql | 20 +- .../src/entities/exchange-rate.entity.ts | 34 + .../packages/server/src/entities/index.ts | 1 + .../1748367064065-create-exchange-rate.ts | 26 + .../__tests__/exchange-rate.resolver.spec.ts | 36 + .../__tests__/exchange-rate.service.spec.ts | 78 +++ .../exchange-rate/exchange-rate.module.ts | 5 +- .../exchange-rate/exchange-rate.resolver.ts | 19 +- .../exchange-rate/exchange-rate.service.ts | 64 +- fullstack/task/yarn.lock | 617 +++++++++++++++++- 18 files changed, 1173 insertions(+), 40 deletions(-) create mode 100644 fullstack/task/packages/client/src/components/ExchangeRates/ExchangeRates.tsx create mode 100644 fullstack/task/packages/client/src/hooks/useExchangeRates.ts create mode 100644 fullstack/task/packages/client/src/theme.ts create mode 100644 fullstack/task/packages/server/src/entities/exchange-rate.entity.ts create mode 100644 fullstack/task/packages/server/src/migrations/1748367064065-create-exchange-rate.ts create mode 100644 fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.resolver.spec.ts create mode 100644 fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.service.spec.ts diff --git a/fullstack/task/jest.config.ts b/fullstack/task/jest.config.ts index 0050fc39..50c8386f 100644 --- a/fullstack/task/jest.config.ts +++ b/fullstack/task/jest.config.ts @@ -1,11 +1,17 @@ import type { Config } from 'jest'; const config: Config = { - preset: 'ts-jest', - testEnvironment: 'node', - transform: { - '^.+\\.ts?$': 'ts-jest', - }, + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).ts'], + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'js', 'json'], + moduleDirectories: ['node_modules', 'src'], + collectCoverage: true, + collectCoverageFrom: ['src/**/*.ts', '!src/**/main.ts', '!**/migrations/**'], + verbose: true, }; export default config; diff --git a/fullstack/task/packages/client/package.json b/fullstack/task/packages/client/package.json index 819a4b34..10aac4b1 100644 --- a/fullstack/task/packages/client/package.json +++ b/fullstack/task/packages/client/package.json @@ -4,6 +4,10 @@ "version": "1.0.0", "dependencies": { "@apollo/client": "3.7.10", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.1.0", + "@mui/material": "^7.1.0", "graphql": "16.6.0", "graphql-tag": "2.12.6", "graphql-ws": "5.12.0", diff --git a/fullstack/task/packages/client/src/App.tsx b/fullstack/task/packages/client/src/App.tsx index f62d2a02..3e6a207c 100644 --- a/fullstack/task/packages/client/src/App.tsx +++ b/fullstack/task/packages/client/src/App.tsx @@ -1,5 +1,35 @@ -function App() { - return

TODO

; -} +import React, { useContext } from 'react'; +import { Box, IconButton, Paper, Typography } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import Brightness4Icon from '@mui/icons-material/Brightness4'; +import Brightness7Icon from '@mui/icons-material/Brightness7'; +import { ColorModeContext } from './theme'; +import ExchangeRates from './components/ExchangeRates/ExchangeRates'; + +const App: React.FC = () => { + const theme = useTheme(); + const colorMode = useContext(ColorModeContext); + + return ( + + + + Exchange Rates + + {theme.palette.mode === 'dark' ? : } + + + + + + + ); +}; export default App; diff --git a/fullstack/task/packages/client/src/components/ExchangeRates/ExchangeRates.tsx b/fullstack/task/packages/client/src/components/ExchangeRates/ExchangeRates.tsx new file mode 100644 index 00000000..a544c736 --- /dev/null +++ b/fullstack/task/packages/client/src/components/ExchangeRates/ExchangeRates.tsx @@ -0,0 +1,125 @@ +import React, { useState, useMemo } from 'react'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import { + Box, + Button, + Chip, + CircularProgress, + IconButton, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, +} from '@mui/material'; +import { useExchangeRates } from '../../hooks/useExchangeRates'; + +const ExchangeRates: React.FC = () => { + const { exchangeRates, loading, error, age, refetch } = useExchangeRates(); + const [search, setSearch] = useState(''); + + const filtered = useMemo(() => { + if (!search) return exchangeRates; + const lower = search.toLowerCase(); + return exchangeRates.filter(r => + r.currency.toLowerCase().includes(lower) || + r.code.toLowerCase().includes(lower) || + String(r.amount).includes(lower) || + String(r.rate).includes(lower) + ); + }, [exchangeRates, search]); + + // highlight matching text + const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const highlight = (value: string | number) => { + const str = String(value); + if (!search) return str; + const parts = str.split(new RegExp(`(${escapeRegExp(search)})`, 'gi')); + return parts.map((part, i) => + part.toLowerCase() === search.toLowerCase() ? ( + + {part} + + ) : ( + part + ) + ); + }; + + return ( + + + + } + label={`Last fetched: ${age}`} + variant="outlined" + /> + refetch({ bypassCache: true })} + aria-label="refresh rates" + > + + + + setSearch(e.target.value)} + /> + + + + {error ? ( + + + Error fetching exchange rates: {error.message} + + + ) : loading ? ( + + + + ) : ( + + + + Currency + Code + Amount + Rate + + + + {filtered.length === 0 ? ( + + + No matching records + + + ) : ( + filtered.map(r => ( + + {highlight(r.currency)} + {highlight(r.code)} + {highlight(r.amount)} + {highlight(r.rate.toFixed(4))} + + )) + )} + +
+ )} +
+
+ ); +}; + +export default ExchangeRates; diff --git a/fullstack/task/packages/client/src/hooks/useExchangeRates.ts b/fullstack/task/packages/client/src/hooks/useExchangeRates.ts new file mode 100644 index 00000000..6834dfbe --- /dev/null +++ b/fullstack/task/packages/client/src/hooks/useExchangeRates.ts @@ -0,0 +1,59 @@ +import { gql, useQuery } from '@apollo/client'; +import { useEffect, useMemo, useState } from 'react'; + +export interface ExchangeRate { + id: string; + currency: string; + code: string; + amount: number; + rate: number; + createdAtUtc: string; +} + +const GET_EXCHANGE_RATES = gql` + query GetExchangeRates($bypassCache: Boolean!) { + exchangeRates(bypassCache: $bypassCache) { + id + currency + code + amount + rate + createdAtUtc + } + } +`; + +export function useExchangeRates() { + const { data, loading, error, refetch } = useQuery< + { exchangeRates: ExchangeRate[] }, + { bypassCache: boolean } + >(GET_EXCHANGE_RATES, { + variables: { bypassCache: false }, + pollInterval: 30_000, + fetchPolicy: 'cache-first', + notifyOnNetworkStatusChange: true, + }); + + const [lastFetched, setLastFetched] = useState(null); + + useEffect(() => { + const ts = data?.exchangeRates?.[0]?.createdAtUtc; + if (ts) setLastFetched(new Date(ts)); + }, [data]); + + const age = useMemo(() => { + if (!lastFetched) return ''; + const secs = Math.floor((Date.now() - lastFetched.getTime()) / 1000); + if (secs < 60) return `${secs} second${secs !== 1 ? 's' : ''} ago`; + const mins = Math.floor(secs / 60); + return `${mins} minute${mins !== 1 ? 's' : ''} ago`; + }, [lastFetched]); + + return { + exchangeRates: data?.exchangeRates ?? [], + loading, + error, + age, + refetch, + }; +} diff --git a/fullstack/task/packages/client/src/main.tsx b/fullstack/task/packages/client/src/main.tsx index ef5f39c2..61101bd9 100644 --- a/fullstack/task/packages/client/src/main.tsx +++ b/fullstack/task/packages/client/src/main.tsx @@ -1,17 +1,44 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'; +import { ThemeProvider, CssBaseline, createTheme } from '@mui/material'; +import { getDesignTokens, ColorModeContext } from './theme'; import App from './App'; -const client = new ApolloClient({ - uri: 'http://localhost:4000/graphql', +function Root() { + const [mode, setMode] = React.useState<'light' | 'dark'>('dark'); + + const colorMode = React.useMemo(() => ({ + toggleColorMode: () => { + setMode(prev => (prev === 'light' ? 'dark' : 'light')); + } + }), []); + + const theme = React.useMemo( + () => createTheme(getDesignTokens(mode)), + [mode] + ); + + const client = new ApolloClient({ + // Ilkin: better come from env. Also port 4000 was not working locally + uri: 'http://localhost:4001/graphql', cache: new InMemoryCache(), -}); + }); + + return ( + + + + + + + + + ); +} -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - - - - +ReactDOM.createRoot(document.getElementById('root')!).render( + + + ); diff --git a/fullstack/task/packages/client/src/theme.ts b/fullstack/task/packages/client/src/theme.ts new file mode 100644 index 00000000..fe60a2ed --- /dev/null +++ b/fullstack/task/packages/client/src/theme.ts @@ -0,0 +1,23 @@ +import React from 'react'; +import { PaletteMode, ThemeOptions } from '@mui/material'; +import { deepOrange, teal } from '@mui/material/colors'; + +export const ColorModeContext = React.createContext({ + toggleColorMode: () => {}, +}); + +export const getDesignTokens = (mode: PaletteMode): ThemeOptions => ({ + palette: { + mode, + primary: mode === 'light' ? teal : deepOrange, + }, + components: { + MuiButton: { + styleOverrides: { + root: { + textTransform: 'none', + }, + }, + }, + }, +}); diff --git a/fullstack/task/packages/server/package.json b/fullstack/task/packages/server/package.json index 09d138c0..2114ae80 100644 --- a/fullstack/task/packages/server/package.json +++ b/fullstack/task/packages/server/package.json @@ -6,7 +6,10 @@ "license": "UNLICENSED", "scripts": { "build": "nest build", - "serve": "node dist/main" + "serve": "node dist/main", + "test": "jest --passWithNoTests", + "test:watch": "jest --watch", + "test:cov": "jest --coverage" }, "dependencies": { "@nestjs/apollo": "10.1.3", diff --git a/fullstack/task/packages/server/schema.gql b/fullstack/task/packages/server/schema.gql index 2453a887..828953a1 100644 --- a/fullstack/task/packages/server/schema.gql +++ b/fullstack/task/packages/server/schema.gql @@ -2,14 +2,16 @@ # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) # ------------------------------------------------------ -type Example { +type ExchangeRate { id: ID! createdAtUtc: DateTime! updatedAtUtc: DateTime deleteDateUtc: DateTime version: Int! - name: String! - value: String! + currency: String! + code: String! + rate: Float! + amount: Int! } """ @@ -17,8 +19,18 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date """ scalar DateTime +type Example { + id: ID! + createdAtUtc: DateTime! + updatedAtUtc: DateTime + deleteDateUtc: DateTime + version: Int! + name: String! + value: String! +} + type Query { - exchangeRates: String! + exchangeRates(bypassCache: Boolean = false): [ExchangeRate!]! exampleByName(name: String!): Example } diff --git a/fullstack/task/packages/server/src/entities/exchange-rate.entity.ts b/fullstack/task/packages/server/src/entities/exchange-rate.entity.ts new file mode 100644 index 00000000..2d25c743 --- /dev/null +++ b/fullstack/task/packages/server/src/entities/exchange-rate.entity.ts @@ -0,0 +1,34 @@ +import { Field, Float, Int, ObjectType } from '@nestjs/graphql'; +import { Column, Entity } from 'typeorm'; +import { IsInt, IsNumber, IsString, MinLength } from 'class-validator'; +import { EntityWithMeta } from '../common'; +import { VAR_CHAR } from './constants'; + +@ObjectType() +@Entity() +export class ExchangeRate extends EntityWithMeta { + @IsString() + @MinLength(1) + @Field(() => String) + @Column({ ...VAR_CHAR }) + public currency!: string; + + @IsString() + @MinLength(1) + @Field(() => String) + @Column({ ...VAR_CHAR }) + public code!: string; + + @IsNumber() + @Field(() => Float) + @Column('decimal') + public rate!: number; + + @IsInt() + @Field(() => Int) + @Column('int') + public amount!: number; + + @Field(() => Date) + public createdAtUtc!: Date; +} diff --git a/fullstack/task/packages/server/src/entities/index.ts b/fullstack/task/packages/server/src/entities/index.ts index 6b536a4a..a0486afb 100644 --- a/fullstack/task/packages/server/src/entities/index.ts +++ b/fullstack/task/packages/server/src/entities/index.ts @@ -1 +1,2 @@ +export * from './exchange-rate.entity'; export * from './example.entity'; diff --git a/fullstack/task/packages/server/src/migrations/1748367064065-create-exchange-rate.ts b/fullstack/task/packages/server/src/migrations/1748367064065-create-exchange-rate.ts new file mode 100644 index 00000000..a72c82a1 --- /dev/null +++ b/fullstack/task/packages/server/src/migrations/1748367064065-create-exchange-rate.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class createExchangeRate1695820000000 implements MigrationInterface { + name = 'createExchangeRate1748367064065'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "exchange_rate" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "createdAtUtc" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "updatedAtUtc" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "deleteDateUtc" TIMESTAMP WITH TIME ZONE, + "version" integer NOT NULL, + "currency" character varying(255) NOT NULL, + "code" character varying(255) NOT NULL, + "rate" decimal NOT NULL, + "amount" integer NOT NULL, + CONSTRAINT "PK_exchange_rate_id" PRIMARY KEY ("id") + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "exchange_rate"`); + } +} diff --git a/fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.resolver.spec.ts b/fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.resolver.spec.ts new file mode 100644 index 00000000..dedeabda --- /dev/null +++ b/fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.resolver.spec.ts @@ -0,0 +1,36 @@ +import { ExchangeRateResolver } from '../exchange-rate.resolver'; +import { ExchangeRateService } from '../exchange-rate.service'; +import { ExchangeRate } from '../../../entities/exchange-rate.entity'; + +describe('ExchangeRateResolver (unit)', () => { + let resolver: ExchangeRateResolver; + let service: jest.Mocked; + + beforeEach(() => { + service = { + getExchangeRates: jest.fn().mockResolvedValue([ + Object.assign(new ExchangeRate(), { + id: 'abc', + currency: 'Test', + code: 'TST', + amount: 1, + rate: 1.0, + createdAtUtc: new Date(), + }), + ]), + } as any; + + resolver = new ExchangeRateResolver(service); + }); + + it('calls getExchangeRates with bypassCache=false by default', async () => { + const result = await resolver.exchangeRates(false); + expect(result).toHaveLength(1); + expect(service.getExchangeRates).toHaveBeenCalledWith(false); + }); + + it('passes the bypassCache argument through', async () => { + await resolver.exchangeRates(true); + expect(service.getExchangeRates).toHaveBeenCalledWith(true); + }); +}); diff --git a/fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.service.spec.ts b/fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.service.spec.ts new file mode 100644 index 00000000..2a0cda98 --- /dev/null +++ b/fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.service.spec.ts @@ -0,0 +1,78 @@ +import axios from 'axios'; +import { InternalServerErrorException } from '@nestjs/common'; +import { ExchangeRateService } from '../exchange-rate.service'; +import { ExchangeRate } from '../../../entities/exchange-rate.entity'; +import { MoreThan, FindOperator, Repository } from 'typeorm'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +describe('ExchangeRateService (unit)', () => { + let service: ExchangeRateService; + let repo: jest.Mocked, 'find' | 'clear' | 'save' | 'create'>>; + + beforeEach(() => { + repo = { + find: jest.fn(), + clear: jest.fn(), + save: jest.fn(), + create: jest.fn(dto => Object.assign(new ExchangeRate(), dto)), // <-- now defined + } as any; + + service = new ExchangeRateService(repo as any); + }); + + it('returns cached rates if they exist and are fresh', async () => { + const now = new Date(); + const cached = [ + Object.assign(new ExchangeRate(), { + id: '1', + currency: 'Foo', + code: 'FOO', + amount: 1, + rate: 1.23, + createdAtUtc: now, + }), + ]; + repo.find.mockResolvedValue(cached); + + const result = await service.getExchangeRates(false); + expect(result).toBe(cached); + expect(mockedAxios.get).not.toHaveBeenCalled(); + expect(repo.find).toHaveBeenCalledWith({ + where: { createdAtUtc: expect.any(FindOperator) }, + }); + }); + + it('fetches, parses, clears, saves and returns new rates when cache is stale', async () => { + repo.find.mockResolvedValue([]); + + const sampleTxt = [ + 'Header line 1', + 'Header line 2', + 'Country|USD|1|USD|23,45', + 'Country|EUR|1|EUR|25,60', + ].join('\n'); + mockedAxios.get.mockResolvedValue({ data: sampleTxt }); + + repo.clear.mockResolvedValue(undefined); + repo.save.mockResolvedValue([] as any); + + const rates = await service.getExchangeRates(false); + + expect(repo.clear).toHaveBeenCalled(); + expect(repo.save).toHaveBeenCalled(); + expect(rates).toHaveLength(2); + expect(rates[0].code).toBe('USD'); + expect(rates[1].code).toBe('EUR'); + }); + + it('throws InternalServerErrorException when the HTTP fetch fails', async () => { + repo.find.mockResolvedValue([]); + mockedAxios.get.mockRejectedValue(new Error('network down')); + + await expect(service.getExchangeRates(false)).rejects.toBeInstanceOf( + InternalServerErrorException, + ); + }); +}); diff --git a/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.module.ts b/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.module.ts index d972c6e4..6b78ad78 100644 --- a/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.module.ts +++ b/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ExchangeRate } from '../../entities/exchange-rate.entity'; import { ExchangeRateService } from './exchange-rate.service'; import { ExchangeRateResolver } from './exchange-rate.resolver'; @Module({ - imports: [], + imports: [TypeOrmModule.forFeature([ExchangeRate])], providers: [ExchangeRateService, ExchangeRateResolver], - exports: [ExchangeRateService], }) export class ExchangeRateModule {} diff --git a/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.resolver.ts b/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.resolver.ts index 39087b66..2ab42621 100644 --- a/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.resolver.ts +++ b/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.resolver.ts @@ -1,13 +1,20 @@ -import { Query, Resolver } from '@nestjs/graphql'; +import { Args, Query, Resolver } from '@nestjs/graphql'; +import { ExchangeRate } from '../../entities/exchange-rate.entity'; import { ExchangeRateService } from './exchange-rate.service'; -@Resolver() +@Resolver(() => ExchangeRate) export class ExchangeRateResolver { constructor(private readonly exchangeRateService: ExchangeRateService) {} - // TODO: Implement a GraphQL Query that returns the exchange rates - @Query(() => String) - async exchangeRates(): Promise { - return 'Hello'; + @Query(() => [ExchangeRate]) + async exchangeRates( + @Args('bypassCache', { + type: () => Boolean, + nullable: true, + defaultValue: false, + }) + bypassCache: boolean + ): Promise { + return this.exchangeRateService.getExchangeRates(bypassCache); } } diff --git a/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.service.ts b/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.service.ts index d9ee80bb..473fe471 100644 --- a/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.service.ts +++ b/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.service.ts @@ -1,11 +1,63 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { MoreThan, Repository } from 'typeorm'; +import axios from 'axios'; +import { ExchangeRate } from '../../entities/exchange-rate.entity'; + +const CNB_EXCHANGE_URL = + 'https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt?date='; @Injectable() export class ExchangeRateService { - public getExchangeRates = async () => { - // TODO: Implement the fetching and parsing of the exchange rates. - // Use this method in the resolver. + constructor( + @InjectRepository(ExchangeRate) + private readonly exchangeRateRepo: Repository + ) {} + + async getExchangeRates(bypassCache = false): Promise { + if (!bypassCache) { + const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); + const cached = await this.exchangeRateRepo.find({ + where: { createdAtUtc: MoreThan(fiveMinutesAgo) }, + }); + if (cached.length > 0) { + return cached; + } + } + + const now = new Date(); + const day = String(now.getDate()).padStart(2, '0'); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const year = now.getFullYear(); + const dateStr = `${day}.${month}.${year}`; + const url = `${CNB_EXCHANGE_URL}${dateStr}`; + + let raw: string; + try { + const { data } = await axios.get(url); + raw = data; + } catch (err: any) { + throw new InternalServerErrorException( + `Failed to fetch exchange rates from CNB: ${err.message}` + ); + } + + const lines = raw.split('\n').slice(2); + await this.exchangeRateRepo.clear(); + + const rates = lines + .filter((line) => line.trim()) + .map((line) => { + const [, currency, amount, code, rate] = line.split('|'); + return this.exchangeRateRepo.create({ + currency, + code, + amount: parseInt(amount, 10), + rate: parseFloat(rate.replace(',', '.')), + }); + }); - return []; - }; + await this.exchangeRateRepo.save(rates); + return rates; + } } diff --git a/fullstack/task/yarn.lock b/fullstack/task/yarn.lock index cb18ed2c..b790085b 100644 --- a/fullstack/task/yarn.lock +++ b/fullstack/task/yarn.lock @@ -300,6 +300,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/code-frame@npm:7.27.1" + dependencies: + "@babel/helper-validator-identifier": ^7.27.1 + js-tokens: ^4.0.0 + picocolors: ^1.1.1 + checksum: 5874edc5d37406c4a0bb14cf79c8e51ad412fb0423d176775ac14fc0259831be1bf95bdda9c2aa651126990505e09a9f0ed85deaa99893bc316d2682c5115bdc + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.22.9": version: 7.22.20 resolution: "@babel/compat-data@npm:7.22.20" @@ -342,6 +353,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.27.3": + version: 7.27.3 + resolution: "@babel/generator@npm:7.27.3" + dependencies: + "@babel/parser": ^7.27.3 + "@babel/types": ^7.27.3 + "@jridgewell/gen-mapping": ^0.3.5 + "@jridgewell/trace-mapping": ^0.3.25 + jsesc: ^3.0.2 + checksum: c0b1b399ff62fa0f1903679ab2b088fd4312c33154c0ae78228094c196ecf53ce8e525b8f5e537ac3117ff9a49cdf7b3640f129114908dfadc6541853f3747a2 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" @@ -399,6 +423,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.16.7": + version: 7.27.1 + resolution: "@babel/helper-module-imports@npm:7.27.1" + dependencies: + "@babel/traverse": ^7.27.1 + "@babel/types": ^7.27.1 + checksum: 92d01c71c0e4aacdc2babce418a9a1a27a8f7d770a210ffa0f3933f321befab18b655bc1241bebc40767516731de0b85639140c42e45a8210abe1e792f115b28 + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.23.0": version: 7.23.0 resolution: "@babel/helper-module-transforms@npm:7.23.0" @@ -446,6 +480,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-string-parser@npm:7.27.1" + checksum: 0a8464adc4b39b138aedcb443b09f4005d86207d7126e5e079177e05c3116107d856ec08282b365e9a79a9872f40f4092a6127f8d74c8a01c1ef789dacfc25d6 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-validator-identifier@npm:7.22.20" @@ -453,6 +494,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-validator-identifier@npm:7.27.1" + checksum: 3c7e8391e59d6c85baeefe9afb86432f2ab821c6232b00ea9082a51d3e7e95a2f3fb083d74dc1f49ac82cf238e1d2295dafcb001f7b0fab479f3f56af5eaaa47 + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.22.15": version: 7.22.15 resolution: "@babel/helper-validator-option@npm:7.22.15" @@ -491,6 +539,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.3": + version: 7.27.3 + resolution: "@babel/parser@npm:7.27.3" + dependencies: + "@babel/types": ^7.27.3 + bin: + parser: ./bin/babel-parser.js + checksum: aef2cfd154e47a639615d173d3f05a8ce8007fcc5a0ade013c953adee71a8bc19465a147e060cc67388fd748b62a3b42bf3b5cc3e83d4f8add526b3b722e2231 + languageName: node + linkType: hard + "@babel/plugin-syntax-async-generators@npm:^7.8.4": version: 7.8.4 resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" @@ -712,6 +771,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.27.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7": + version: 7.27.3 + resolution: "@babel/runtime@npm:7.27.3" + checksum: 4eadd745756c9de7a9bba1b2b138b8b99f34aa544cf69c7e6fd1d2a120ba6d31580e93f00657145add7a8945db8ffea5ef7fdbe18f0f0f8e82f2929cb56a0d61 + languageName: node + linkType: hard + "@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3": version: 7.22.15 resolution: "@babel/template@npm:7.22.15" @@ -723,6 +789,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.27.2": + version: 7.27.2 + resolution: "@babel/template@npm:7.27.2" + dependencies: + "@babel/code-frame": ^7.27.1 + "@babel/parser": ^7.27.2 + "@babel/types": ^7.27.1 + checksum: ff5628bc066060624afd970616090e5bba91c6240c2e4b458d13267a523572cbfcbf549391eec8217b94b064cf96571c6273f0c04b28a8567b96edc675c28e27 + languageName: node + linkType: hard + "@babel/traverse@npm:^7.23.0, @babel/traverse@npm:^7.4.5, @babel/traverse@npm:^7.7.2": version: 7.23.0 resolution: "@babel/traverse@npm:7.23.0" @@ -741,6 +818,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.27.1": + version: 7.27.3 + resolution: "@babel/traverse@npm:7.27.3" + dependencies: + "@babel/code-frame": ^7.27.1 + "@babel/generator": ^7.27.3 + "@babel/parser": ^7.27.3 + "@babel/template": ^7.27.2 + "@babel/types": ^7.27.3 + debug: ^4.3.1 + globals: ^11.1.0 + checksum: 914402382921b796b740f7c90d56ba130ffd5eeda8d18dc82f1243a1a510ff21a26d5b713df08c8e8aad2ffc969ce4624cee309406d69bcee8efa350483688c9 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3": version: 7.23.0 resolution: "@babel/types@npm:7.23.0" @@ -752,6 +844,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3": + version: 7.27.3 + resolution: "@babel/types@npm:7.27.3" + dependencies: + "@babel/helper-string-parser": ^7.27.1 + "@babel/helper-validator-identifier": ^7.27.1 + checksum: f0d43c0231f3ebc118480e149292dcd92ea128e2650285ced99ff2e5610db2171305f59aa07406ba0cb36af8e4331a53a69576d6b0c3f3176144dd3ad514b9ae + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -802,6 +904,45 @@ __metadata: languageName: unknown linkType: soft +"@emotion/babel-plugin@npm:^11.13.5": + version: 11.13.5 + resolution: "@emotion/babel-plugin@npm:11.13.5" + dependencies: + "@babel/helper-module-imports": ^7.16.7 + "@babel/runtime": ^7.18.3 + "@emotion/hash": ^0.9.2 + "@emotion/memoize": ^0.9.0 + "@emotion/serialize": ^1.3.3 + babel-plugin-macros: ^3.1.0 + convert-source-map: ^1.5.0 + escape-string-regexp: ^4.0.0 + find-root: ^1.1.0 + source-map: ^0.5.7 + stylis: 4.2.0 + checksum: c41df7e6c19520e76d1939f884be878bf88b5ba00bd3de9d05c5b6c5baa5051686ab124d7317a0645de1b017b574d8139ae1d6390ec267fbe8e85a5252afb542 + languageName: node + linkType: hard + +"@emotion/cache@npm:^11.13.5, @emotion/cache@npm:^11.14.0": + version: 11.14.0 + resolution: "@emotion/cache@npm:11.14.0" + dependencies: + "@emotion/memoize": ^0.9.0 + "@emotion/sheet": ^1.4.0 + "@emotion/utils": ^1.4.2 + "@emotion/weak-memoize": ^0.4.0 + stylis: 4.2.0 + checksum: 0a81591541ea43bc7851742e6444b7800d72e98006f94e775ae6ea0806662d14e0a86ff940f5f19d33b4bb2c427c882aa65d417e7322a6e0d5f20fe65ed920c9 + languageName: node + linkType: hard + +"@emotion/hash@npm:^0.9.2": + version: 0.9.2 + resolution: "@emotion/hash@npm:0.9.2" + checksum: 379bde2830ccb0328c2617ec009642321c0e009a46aa383dfbe75b679c6aea977ca698c832d225a893901f29d7b3eef0e38cf341f560f6b2b56f1ff23c172387 + languageName: node + linkType: hard + "@emotion/is-prop-valid@npm:^1.1.0": version: 1.2.1 resolution: "@emotion/is-prop-valid@npm:1.2.1" @@ -811,6 +952,15 @@ __metadata: languageName: node linkType: hard +"@emotion/is-prop-valid@npm:^1.3.0": + version: 1.3.1 + resolution: "@emotion/is-prop-valid@npm:1.3.1" + dependencies: + "@emotion/memoize": ^0.9.0 + checksum: fe6549d54f389e1a17cb02d832af7ee85fb6ea126fc18d02ca47216e8ff19332c1983f4a0ba68602cfcd3b325ffd4ebf0b2d0c6270f1e7e6fe3fca4ba7741e1a + languageName: node + linkType: hard + "@emotion/memoize@npm:^0.8.1": version: 0.8.1 resolution: "@emotion/memoize@npm:0.8.1" @@ -818,6 +968,74 @@ __metadata: languageName: node linkType: hard +"@emotion/memoize@npm:^0.9.0": + version: 0.9.0 + resolution: "@emotion/memoize@npm:0.9.0" + checksum: 038132359397348e378c593a773b1148cd0cf0a2285ffd067a0f63447b945f5278860d9de718f906a74c7c940ba1783ac2ca18f1c06a307b01cc0e3944e783b1 + languageName: node + linkType: hard + +"@emotion/react@npm:^11.14.0": + version: 11.14.0 + resolution: "@emotion/react@npm:11.14.0" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.13.5 + "@emotion/cache": ^11.14.0 + "@emotion/serialize": ^1.3.3 + "@emotion/use-insertion-effect-with-fallbacks": ^1.2.0 + "@emotion/utils": ^1.4.2 + "@emotion/weak-memoize": ^0.4.0 + hoist-non-react-statics: ^3.3.1 + peerDependencies: + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 3cf023b11d132b56168713764d6fced8e5a1f0687dfe0caa2782dfd428c8f9e30f9826a919965a311d87b523cd196722aaf75919cd0f6bd0fd57f8a6a0281500 + languageName: node + linkType: hard + +"@emotion/serialize@npm:^1.3.3": + version: 1.3.3 + resolution: "@emotion/serialize@npm:1.3.3" + dependencies: + "@emotion/hash": ^0.9.2 + "@emotion/memoize": ^0.9.0 + "@emotion/unitless": ^0.10.0 + "@emotion/utils": ^1.4.2 + csstype: ^3.0.2 + checksum: 510331233767ae4e09e925287ca2c7269b320fa1d737ea86db5b3c861a734483ea832394c0c1fe5b21468fe335624a75e72818831d303ba38125f54f44ba02e7 + languageName: node + linkType: hard + +"@emotion/sheet@npm:^1.4.0": + version: 1.4.0 + resolution: "@emotion/sheet@npm:1.4.0" + checksum: eeb1212e3289db8e083e72e7e401cd6d1a84deece87e9ce184f7b96b9b5dbd6f070a89057255a6ff14d9865c3ce31f27c39248a053e4cdd875540359042586b4 + languageName: node + linkType: hard + +"@emotion/styled@npm:^11.14.0": + version: 11.14.0 + resolution: "@emotion/styled@npm:11.14.0" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.13.5 + "@emotion/is-prop-valid": ^1.3.0 + "@emotion/serialize": ^1.3.3 + "@emotion/use-insertion-effect-with-fallbacks": ^1.2.0 + "@emotion/utils": ^1.4.2 + peerDependencies: + "@emotion/react": ^11.0.0-rc.0 + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 9c1b842e942e69fb6037d1ab161046d2bcfeff95fd2ccfdab30acaaf6b89dc07b14bb00f8cc8ec14df11e6746c8e4e1d781bc54d10bd739aab44966ded64d4fb + languageName: node + linkType: hard + "@emotion/stylis@npm:^0.8.4": version: 0.8.5 resolution: "@emotion/stylis@npm:0.8.5" @@ -825,6 +1043,13 @@ __metadata: languageName: node linkType: hard +"@emotion/unitless@npm:^0.10.0": + version: 0.10.0 + resolution: "@emotion/unitless@npm:0.10.0" + checksum: d79346df31a933e6d33518e92636afeb603ce043f3857d0a39a2ac78a09ef0be8bedff40130930cb25df1beeee12d96ee38613963886fa377c681a89970b787c + languageName: node + linkType: hard + "@emotion/unitless@npm:^0.7.4": version: 0.7.5 resolution: "@emotion/unitless@npm:0.7.5" @@ -832,6 +1057,29 @@ __metadata: languageName: node linkType: hard +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.2.0": + version: 1.2.0 + resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.2.0" + peerDependencies: + react: ">=16.8.0" + checksum: 8ff6aec7f2924526ff8c8f8f93d4b8236376e2e12c435314a18c9a373016e24dfdf984e82bbc83712b8e90ff4783cd765eb39fc7050d1a43245e5728740ddd71 + languageName: node + linkType: hard + +"@emotion/utils@npm:^1.4.2": + version: 1.4.2 + resolution: "@emotion/utils@npm:1.4.2" + checksum: 04cf76849c6401205c058b82689fd0ec5bf501aed6974880fe9681a1d61543efb97e848f4c38664ac4a9068c7ad2d1cb84f73bde6cf95f1208aa3c28e0190321 + languageName: node + linkType: hard + +"@emotion/weak-memoize@npm:^0.4.0": + version: 0.4.0 + resolution: "@emotion/weak-memoize@npm:0.4.0" + checksum: db5da0e89bd752c78b6bd65a1e56231f0abebe2f71c0bd8fc47dff96408f7065b02e214080f99924f6a3bfe7ee15afc48dad999d76df86b39b16e513f7a94f52 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.15.18": version: 0.15.18 resolution: "@esbuild/android-arm@npm:0.15.18" @@ -1313,6 +1561,17 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.8 + resolution: "@jridgewell/gen-mapping@npm:0.3.8" + dependencies: + "@jridgewell/set-array": ^1.2.1 + "@jridgewell/sourcemap-codec": ^1.4.10 + "@jridgewell/trace-mapping": ^0.3.24 + checksum: c0687b5227461717aa537fe71a42e356bcd1c43293b3353796a148bf3b0d6f59109def46c22f05b60e29a46f19b2e4676d027959a7c53a6c92b9d5b0d87d0420 + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.1 resolution: "@jridgewell/resolve-uri@npm:3.1.1" @@ -1327,6 +1586,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 + languageName: node + linkType: hard + "@jridgewell/source-map@npm:^0.3.3": version: 0.3.5 resolution: "@jridgewell/source-map@npm:0.3.5" @@ -1364,6 +1630,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" + dependencies: + "@jridgewell/resolve-uri": ^3.1.0 + "@jridgewell/sourcemap-codec": ^1.4.14 + checksum: 9d3c40d225e139987b50c48988f8717a54a8c994d8a948ee42e1412e08988761d0754d7d10b803061cc3aebf35f92a5dbbab493bd0e1a9ef9e89a2130e83ba34 + languageName: node + linkType: hard + "@lukeed/csprng@npm:^1.0.0": version: 1.1.0 resolution: "@lukeed/csprng@npm:1.1.0" @@ -1390,6 +1666,167 @@ __metadata: languageName: node linkType: hard +"@mui/core-downloads-tracker@npm:^7.1.0": + version: 7.1.0 + resolution: "@mui/core-downloads-tracker@npm:7.1.0" + checksum: bf625749ef497f1707d12ef0ab85db0da1417c47df39ef91207de283c8fcd4ef1aab4ce2c4a1f1db532e88a9ba8133d0cc4348cb83a28692047b74fb9952a151 + languageName: node + linkType: hard + +"@mui/icons-material@npm:^7.1.0": + version: 7.1.0 + resolution: "@mui/icons-material@npm:7.1.0" + dependencies: + "@babel/runtime": ^7.27.1 + peerDependencies: + "@mui/material": ^7.1.0 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 582d1bdaffa7f712598a537b6852ec12f87fe576239e3bc743c72a9b470fb9aa67b3655b5b70ed69893eccce91d80e8ecc6c3356b1b42edf217f4bc7e2256620 + languageName: node + linkType: hard + +"@mui/material@npm:^7.1.0": + version: 7.1.0 + resolution: "@mui/material@npm:7.1.0" + dependencies: + "@babel/runtime": ^7.27.1 + "@mui/core-downloads-tracker": ^7.1.0 + "@mui/system": ^7.1.0 + "@mui/types": ^7.4.2 + "@mui/utils": ^7.1.0 + "@popperjs/core": ^2.11.8 + "@types/react-transition-group": ^4.4.12 + clsx: ^2.1.1 + csstype: ^3.1.3 + prop-types: ^15.8.1 + react-is: ^19.1.0 + react-transition-group: ^4.4.5 + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@mui/material-pigment-css": ^7.1.0 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@mui/material-pigment-css": + optional: true + "@types/react": + optional: true + checksum: 9522571d7ba2ea87b9c208c0b28ea975b28211bb66129ebe13bf6f13854dc2e1f0f853e70a8bcbf6b68693a347f4971c40bc5d247f8c4d9c66ab67d065e7cdbf + languageName: node + linkType: hard + +"@mui/private-theming@npm:^7.1.0": + version: 7.1.0 + resolution: "@mui/private-theming@npm:7.1.0" + dependencies: + "@babel/runtime": ^7.27.1 + "@mui/utils": ^7.1.0 + prop-types: ^15.8.1 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 639888a7f69eb42ee673f66c7c62bd9414cc02254456865417312d7fc3245e723b2ea92902258f7c6a10c6949c0725da09026d4f96cea259d5c9f49762473f8a + languageName: node + linkType: hard + +"@mui/styled-engine@npm:^7.1.0": + version: 7.1.0 + resolution: "@mui/styled-engine@npm:7.1.0" + dependencies: + "@babel/runtime": ^7.27.1 + "@emotion/cache": ^11.13.5 + "@emotion/serialize": ^1.3.3 + "@emotion/sheet": ^1.4.0 + csstype: ^3.1.3 + prop-types: ^15.8.1 + peerDependencies: + "@emotion/react": ^11.4.1 + "@emotion/styled": ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + checksum: b23daa7188f5e9a7f5f9db5b0836dbea784a14dbe9e3cded2a072651d23085f0b36f179b063c9a3533903b9fc067558379ac31bf0c447ba8d1315c77ce221700 + languageName: node + linkType: hard + +"@mui/system@npm:^7.1.0": + version: 7.1.0 + resolution: "@mui/system@npm:7.1.0" + dependencies: + "@babel/runtime": ^7.27.1 + "@mui/private-theming": ^7.1.0 + "@mui/styled-engine": ^7.1.0 + "@mui/types": ^7.4.2 + "@mui/utils": ^7.1.0 + clsx: ^2.1.1 + csstype: ^3.1.3 + prop-types: ^15.8.1 + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 9a77664fe82b0687da1fa07997aaa7438e36b0ea7701f7bceb8173ddc969f08f838b05eeecc123b842184f30988c6521e18244a67a2b893b1ab2c842812a1a57 + languageName: node + linkType: hard + +"@mui/types@npm:^7.4.2": + version: 7.4.2 + resolution: "@mui/types@npm:7.4.2" + dependencies: + "@babel/runtime": ^7.27.1 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: eb0c8eb4af9270faab8420ce46bed98ce880cc4324078211885e0fc6a61bb1afc1e5784944d27cfcb2f08ce6b56daaec33dd7b5bc0981a620be0ce59acd22eac + languageName: node + linkType: hard + +"@mui/utils@npm:^7.1.0": + version: 7.1.0 + resolution: "@mui/utils@npm:7.1.0" + dependencies: + "@babel/runtime": ^7.27.1 + "@mui/types": ^7.4.2 + "@types/prop-types": ^15.7.14 + clsx: ^2.1.1 + prop-types: ^15.8.1 + react-is: ^19.1.0 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 85e4b101e8f3c9a97df47b8eecef34e65b3bfeba4c6b3dece42ff505c6268db28f7f4cc72877481494dbbc42efe267585e0b0025e3fbfb48a0755149ef3d3d1c + languageName: node + linkType: hard + "@nestjs/apollo@npm:10.1.3": version: 10.1.3 resolution: "@nestjs/apollo@npm:10.1.3" @@ -1792,6 +2229,13 @@ __metadata: languageName: node linkType: hard +"@popperjs/core@npm:^2.11.8": + version: 2.11.8 + resolution: "@popperjs/core@npm:2.11.8" + checksum: e5c69fdebf52a4012f6a1f14817ca8e9599cb1be73dd1387e1785e2ed5e5f0862ff817f420a87c7fc532add1f88a12e25aeb010ffcbdc98eace3d55ce2139cf0 + languageName: node + linkType: hard + "@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": version: 1.1.2 resolution: "@protobufjs/aspromise@npm:1.1.2" @@ -2390,6 +2834,13 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:^15.7.14": + version: 15.7.14 + resolution: "@types/prop-types@npm:15.7.14" + checksum: d0c5407b9ccc3dd5fae0ccf9b1007e7622ba5e6f1c18399b4f24dff33619d469da4b9fa918a374f19dc0d9fe6a013362aab0b844b606cfc10676efba3f5f736d + languageName: node + linkType: hard + "@types/qs@npm:*": version: 6.9.8 resolution: "@types/qs@npm:6.9.8" @@ -2413,6 +2864,15 @@ __metadata: languageName: node linkType: hard +"@types/react-transition-group@npm:^4.4.12": + version: 4.4.12 + resolution: "@types/react-transition-group@npm:4.4.12" + peerDependencies: + "@types/react": "*" + checksum: 13d36396cae4d3c316b03d4a0ba299f0d039c59368ba65e04b0c3dc06fd0a16f59d2c669c3e32d6d525a95423f156b84e550d26bff0bdd8df285f305f8f3a0ed + languageName: node + linkType: hard + "@types/react@npm:*": version: 18.2.22 resolution: "@types/react@npm:18.2.22" @@ -3527,6 +3987,17 @@ __metadata: languageName: node linkType: hard +"babel-plugin-macros@npm:^3.1.0": + version: 3.1.0 + resolution: "babel-plugin-macros@npm:3.1.0" + dependencies: + "@babel/runtime": ^7.12.5 + cosmiconfig: ^7.0.0 + resolve: ^1.19.0 + checksum: 765de4abebd3e4688ebdfbff8571ddc8cd8061f839bb6c3e550b0344a4027b04c60491f843296ce3f3379fb356cc873d57a9ee6694262547eb822c14a25be9a6 + languageName: node + linkType: hard + "babel-plugin-styled-components@npm:>= 1.12.0": version: 2.1.4 resolution: "babel-plugin-styled-components@npm:2.1.4" @@ -4075,6 +4546,10 @@ __metadata: "@babel/core": 7.23.0 "@dishboard/eslint-config": 1.0.0 "@dishboard/jest-config": 1.0.0 + "@emotion/react": ^11.14.0 + "@emotion/styled": ^11.14.0 + "@mui/icons-material": ^7.1.0 + "@mui/material": ^7.1.0 "@types/jest": 28.1.8 "@types/react": 18.0.17 "@types/react-dom": 18.0.6 @@ -4131,6 +4606,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^2.1.1": + version: 2.1.1 + resolution: "clsx@npm:2.1.1" + checksum: acd3e1ab9d8a433ecb3cc2f6a05ab95fe50b4a3cfc5ba47abb6cbf3754585fcb87b84e90c822a1f256c4198e3b41c7f6c391577ffc8678ad587fc0976b24fd57 + languageName: node + linkType: hard + "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -4294,7 +4776,7 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^1.4.0, convert-source-map@npm:^1.6.0": +"convert-source-map@npm:^1.4.0, convert-source-map@npm:^1.5.0, convert-source-map@npm:^1.6.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" checksum: dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 @@ -4360,7 +4842,7 @@ __metadata: languageName: node linkType: hard -"cosmiconfig@npm:^7.0.1": +"cosmiconfig@npm:^7.0.0, cosmiconfig@npm:^7.0.1": version: 7.1.0 resolution: "cosmiconfig@npm:7.1.0" dependencies: @@ -4442,6 +4924,13 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^3.1.3": + version: 3.1.3 + resolution: "csstype@npm:3.1.3" + checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7 + languageName: node + linkType: hard + "damerau-levenshtein@npm:^1.0.8": version: 1.0.8 resolution: "damerau-levenshtein@npm:1.0.8" @@ -4683,6 +5172,16 @@ __metadata: languageName: node linkType: hard +"dom-helpers@npm:^5.0.1": + version: 5.2.1 + resolution: "dom-helpers@npm:5.2.1" + dependencies: + "@babel/runtime": ^7.8.7 + csstype: ^3.0.2 + checksum: 863ba9e086f7093df3376b43e74ce4422571d404fc9828bf2c56140963d5edf0e56160f9b2f3bb61b282c07f8fc8134f023c98fd684bddcb12daf7b0f14d951c + languageName: node + linkType: hard + "dotenv-expand@npm:8.0.3": version: 8.0.3 resolution: "dotenv-expand@npm:8.0.3" @@ -5848,6 +6347,13 @@ __metadata: languageName: node linkType: hard +"find-root@npm:^1.1.0": + version: 1.1.0 + resolution: "find-root@npm:1.1.0" + checksum: b2a59fe4b6c932eef36c45a048ae8f93c85640212ebe8363164814990ee20f154197505965f3f4f102efc33bfb1cbc26fd17c4a2fc739ebc51b886b137cbefaf + languageName: node + linkType: hard + "find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" @@ -6090,6 +6596,13 @@ __metadata: languageName: node linkType: hard +"function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 2b0ff4ce708d99715ad14a6d1f894e2a83242e4a52ccfcefaee5e40050562e5f6dafc1adbb4ce2d4ab47279a45dc736ab91ea5042d843c3c092820dfe032efb1 + languageName: node + linkType: hard + "function.prototype.name@npm:^1.1.6": version: 1.1.6 resolution: "function.prototype.name@npm:1.1.6" @@ -6476,6 +6989,15 @@ __metadata: languageName: node linkType: hard +"hasown@npm:^2.0.2": + version: 2.0.2 + resolution: "hasown@npm:2.0.2" + dependencies: + function-bind: ^1.1.2 + checksum: e8516f776a15149ca6c6ed2ae3110c417a00b62260e222590e54aa367cbcd6ed99122020b37b7fbdf05748df57b265e70095d7bf35a47660587619b15ffb93db + languageName: node + linkType: hard + "help-me@npm:^3.0.0": version: 3.0.0 resolution: "help-me@npm:3.0.0" @@ -6493,7 +7015,7 @@ __metadata: languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": +"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -6803,6 +7325,15 @@ __metadata: languageName: node linkType: hard +"is-core-module@npm:^2.16.0": + version: 2.16.1 + resolution: "is-core-module@npm:2.16.1" + dependencies: + hasown: ^2.0.2 + checksum: 6ec5b3c42d9cbf1ac23f164b16b8a140c3cec338bf8f884c076ca89950c7cc04c33e78f02b8cae7ff4751f3247e3174b2330f1fe4de194c7210deb8b1ea316a7 + languageName: node + linkType: hard + "is-date-object@npm:^1.0.1": version: 1.0.5 resolution: "is-date-object@npm:1.0.5" @@ -7596,6 +8127,15 @@ __metadata: languageName: node linkType: hard +"jsesc@npm:^3.0.2": + version: 3.1.0 + resolution: "jsesc@npm:3.1.0" + bin: + jsesc: bin/jsesc + checksum: 19c94095ea026725540c0d29da33ab03144f6bcf2d4159e4833d534976e99e0c09c38cefa9a575279a51fc36b31166f8d6d05c9fe2645d5f15851d690b41f17f + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -9181,6 +9721,13 @@ __metadata: languageName: node linkType: hard +"picocolors@npm:^1.1.1": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 + languageName: node + linkType: hard + "picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" @@ -9323,7 +9870,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -9468,6 +10015,13 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^19.1.0": + version: 19.1.0 + resolution: "react-is@npm:19.1.0" + checksum: 3eb4eac7f09bf178bdc6fa98d384f5f243b85de7c99679a88b0154ead4d818ad94386ccb00ea31ec52409ffd13299057f5ec6ca2eaec06f9f7eddc1ad4832332 + languageName: node + linkType: hard + "react-refresh@npm:^0.14.0": version: 0.14.0 resolution: "react-refresh@npm:0.14.0" @@ -9475,6 +10029,21 @@ __metadata: languageName: node linkType: hard +"react-transition-group@npm:^4.4.5": + version: 4.4.5 + resolution: "react-transition-group@npm:4.4.5" + dependencies: + "@babel/runtime": ^7.5.5 + dom-helpers: ^5.0.1 + loose-envify: ^1.4.0 + prop-types: ^15.6.2 + peerDependencies: + react: ">=16.6.0" + react-dom: ">=16.6.0" + checksum: 75602840106aa9c6545149d6d7ae1502fb7b7abadcce70a6954c4b64a438ff1cd16fc77a0a1e5197cdd72da398f39eb929ea06f9005c45b132ed34e056ebdeb1 + languageName: node + linkType: hard + "react@npm:18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0" @@ -9631,6 +10200,19 @@ __metadata: languageName: node linkType: hard +"resolve@npm:^1.19.0": + version: 1.22.10 + resolution: "resolve@npm:1.22.10" + dependencies: + is-core-module: ^2.16.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: ab7a32ff4046fcd7c6fdd525b24a7527847d03c3650c733b909b01b757f92eb23510afa9cc3e9bf3f26a3e073b48c88c706dfd4c1d2fb4a16a96b73b6328ddcf + languageName: node + linkType: hard + "resolve@npm:^2.0.0-next.3": version: 2.0.0-next.4 resolution: "resolve@npm:2.0.0-next.4" @@ -9657,6 +10239,19 @@ __metadata: languageName: node linkType: hard +"resolve@patch:resolve@^1.19.0#~builtin": + version: 1.22.10 + resolution: "resolve@patch:resolve@npm%3A1.22.10#~builtin::version=1.22.10&hash=07638b" + dependencies: + is-core-module: ^2.16.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: 8aac1e4e4628bd00bf4b94b23de137dd3fe44097a8d528fd66db74484be929936e20c696e1a3edf4488f37e14180b73df6f600992baea3e089e8674291f16c9d + languageName: node + linkType: hard + "resolve@patch:resolve@^2.0.0-next.3#~builtin": version: 2.0.0-next.4 resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin::version=2.0.0-next.4&hash=07638b" @@ -10202,6 +10797,13 @@ __metadata: languageName: node linkType: hard +"source-map@npm:^0.5.7": + version: 0.5.7 + resolution: "source-map@npm:0.5.7" + checksum: 5dc2043b93d2f194142c7f38f74a24670cd7a0063acdaf4bf01d2964b402257ae843c2a8fa822ad5b71013b5fcafa55af7421383da919752f22ff488bc553f4d + languageName: node + linkType: hard + "source-map@npm:^0.6.0, source-map@npm:^0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" @@ -10466,6 +11068,13 @@ __metadata: languageName: node linkType: hard +"stylis@npm:4.2.0": + version: 4.2.0 + resolution: "stylis@npm:4.2.0" + checksum: 0eb6cc1b866dc17a6037d0a82ac7fa877eba6a757443e79e7c4f35bacedbf6421fadcab4363b39667b43355cbaaa570a3cde850f776498e5450f32ed2f9b7584 + languageName: node + linkType: hard + "subscriptions-transport-ws@npm:0.11.0": version: 0.11.0 resolution: "subscriptions-transport-ws@npm:0.11.0"