diff --git a/.github/.gitignore b/.github/.gitignore new file mode 100644 index 00000000..ecb6d6ba --- /dev/null +++ b/.github/.gitignore @@ -0,0 +1,2 @@ +agents +skills \ No newline at end of file diff --git a/.migrations/001_init.down.sql b/.migrations/001_init.down.sql index 80fb9176..1db35c05 100644 --- a/.migrations/001_init.down.sql +++ b/.migrations/001_init.down.sql @@ -1,13 +1,17 @@ -drop table if exists users; -drop table if exists assets; -drop table if exists portfolios; -drop table if exists transactions; +-- 001_init.down.sql -drop view if exists portfolios_ext; +-- triggers +drop trigger if exists check_holdings_before_insert_sell; +drop trigger if exists check_holdings_before_update_sell; + +-- views drop view if exists asset_transactions; drop view if exists asset_holdings; +drop view if exists portfolios_ext; drop view if exists assets_contributions; -drop trigger if exists check_holdings_before_insert_sell; -drop trigger if exists check_holdings_before_update_sell; - +-- tables +drop table if exists users; +drop table if exists assets; +drop table if exists portfolios; +drop table if exists transactions; diff --git a/.migrations/002_portfolio_view.down.sql b/.migrations/002_portfolio_view.down.sql index 30ae95cc..be3fbb37 100644 --- a/.migrations/002_portfolio_view.down.sql +++ b/.migrations/002_portfolio_view.down.sql @@ -1,2 +1,26 @@ -ALTER TABLE transactions -DROP COLUMN comments; \ No newline at end of file +-- 002_portfolio_view.down.sql + +-- revert views to previous state +drop view if exists portfolios_ext; + +create view portfolios_ext as +select + p.*, + coalesce(a.total_invested, 0) as total_invested, + coalesce(a.num_assets, 0) as num_assets +from + portfolios p + left join ( + select + portfolio_id, + sum(invested) as total_invested, + count(id) as num_assets + from + asset_holdings + group by + portfolio_id + ) as a on p.id = a.portfolio_id; + +-- remove comments column +alter table transactions +drop column comments; diff --git a/.migrations/005_num_shares.down.sql b/.migrations/005_num_shares.down.sql index 7765e3ac..2001a1ee 100644 --- a/.migrations/005_num_shares.down.sql +++ b/.migrations/005_num_shares.down.sql @@ -1 +1,125 @@ --- drop view if exists asset_transactions; \ No newline at end of file +-- 005_num_shares.down.sql + +-- drop new triggers +drop trigger if exists check_holdings_before_insert_sell; +drop trigger if exists check_holdings_before_update_sell; + +-- drop new views +drop view if exists transactions_ext; +drop view if exists assets_ext; +drop view if exists portfolios_ext; + +-- recreate portfolios_ext from 002 +create view portfolios_ext as +select + p.*, + coalesce(a.total_invested, 0) as total_invested, + coalesce(a.num_assets, 0) as num_assets, + coalesce(total_invested / sum(total_invested) over (partition by p.user_id), 0) as contribution +from + portfolios p + left join ( + select + portfolio_id, + sum(invested) as total_invested, + count(id) as num_assets + from + asset_holdings + group by + portfolio_id + ) as a on p.id = a.portfolio_id; + +-- recreate old views from 001 +create view asset_holdings as +select + sub.*, + case + when sub.holdings = 0 then null + else sub.invested / sub.holdings + end as avg_price +from + ( + select + a.*, + u.id as user_id, + coalesce( + sum( + case + when t.type = 'buy' then t.quantity + else - t.quantity + end + ), + 0 + ) as holdings, + coalesce( + sum( + case + when t.type = 'buy' then t.quantity * t.price + else - t.quantity * t.price + end + ), + 0 + ) as invested, + count(t.id) as num_tx + from + assets a + inner join portfolios p on p.id = a.portfolio_id + left join transactions t on t.asset_id = a.id + inner join users u on u.id = p.user_id + group by + a.id, + a.name + ) as sub; + +create view asset_transactions as +select + t.*, + a.name, + a.ticker, + p.name as portfolio_name, + p.description as portfolio_description, + p.user_id +from + transactions t + inner join assets a on a.id = t.asset_id + inner join portfolios p on p.id = a.portfolio_id; + +create view assets_contributions as +select + ah.*, + coalesce(ah.invested / coalesce(pt.total_invested, 1), 0) as portfolio_contribution +from + asset_holdings ah + left join portfolios_ext pt on ah.portfolio_id = pt.id; + +-- recreate old triggers +create trigger check_holdings_before_insert_sell before insert on transactions for each row when new.type = 'sell' begin +select + case + when ( + select + holdings + from + asset_holdings a + where + id = new.asset_id + ) < new.quantity then raise (abort, 'Insufficient holdings') + end; + +end; + +create trigger check_holdings_before_update_sell before +update on transactions for each row when new.type = 'sell' begin +select + case + when ( + select + holdings + from + asset_holdings a + where + id = new.asset_id + ) < new.quantity then raise (abort, 'Insufficient holdings') + end; + +end; diff --git a/.migrations/006_self_enriched_tx.down.sql b/.migrations/006_self_enriched_tx.down.sql index e69de29b..f4744044 100644 --- a/.migrations/006_self_enriched_tx.down.sql +++ b/.migrations/006_self_enriched_tx.down.sql @@ -0,0 +1,174 @@ +-- 006_self_enriched_tx.down.sql + +-- drop new views +drop view if exists transactions_ext; +drop view if exists assets_ext; +drop view if exists portfolios_ext; + +-- recreate portfolios_ext from 005 +create view portfolios_ext as +with + portfolio_assets as ( + select + portfolio_id, + sum(invested) as total_invested, + count(id) as num_assets + from + assets_ext + group by + portfolio_id + ) +select + p.*, + coalesce(pa.total_invested, 0) as total_invested, + coalesce(pa.num_assets, 0) as num_assets, + coalesce( + pa.total_invested / sum(pa.total_invested) over user_portfolios, + 0 + ) as contribution +from + portfolios p + left join portfolio_assets as pa on p.id = pa.portfolio_id +window + user_portfolios as ( + partition by + p.user_id + ); + +-- recreate assets_ext from 005 +create view assets_ext as +with + avg_unit_cost as ( + select + asset_id, + sum(quantity * price) / sum(quantity) as avg_price + from + transactions + where + type = 'buy' + group by + asset_id + ), + tx_stats as ( + select + asset_id, + sum(quantity_ext) as holdings, + count(*) as num_txs + from + transactions_ext + group by + asset_id + ) +select + a.*, + u.id as user_id, + coalesce(t.holdings, 0) as holdings, + coalesce(t.holdings * c.avg_price, 0) as invested, + coalesce(t.num_txs, 0) as num_txs, + c.avg_price +from + assets a + inner join portfolios p on p.id = a.portfolio_id + left join avg_unit_cost c on a.id = c.asset_id + left join tx_stats t on a.id = t.asset_id + inner join users u on u.id = p.user_id; + +-- recreate transactions_ext from 005 +create view transactions_ext as +with + txs_ext as ( + select + *, + case + when type = 'buy' then quantity + else 0 - quantity + end as quantity_ext + from + transactions + ), + total_contr as ( + select + asset_id, + sum(quantity_ext) as total_quantity + from + txs_ext + group by + asset_id + ) +select + t.*, + sum(quantity_ext) over upto_tx as holdings, + sum(quantity_ext * price) over upto_tx as total_invested, + coalesce( + sum( + case + when type = 'buy' then quantity * price + else 0 + end + ) over upto_tx / sum( + case + when type = 'buy' then quantity + else 0 + end + ) over upto_tx, + 0 + ) as avg_price, + case + when tc.total_quantity = 0 then 0 + else quantity_ext / tc.total_quantity + end AS contribution, + a.name, + a.ticker, + p.name as portfolio_name, + p.description as portfolio_description, + p.user_id +from + txs_ext t + join total_contr tc on t.asset_id = tc.asset_id + inner join assets a on a.id = t.asset_id + inner join portfolios p on p.id = a.portfolio_id +window + upto_tx as ( + partition by + t.asset_id + order by + date ROWS BETWEEN UNBOUNDED PRECEDING + AND CURRENT ROW + ) +order by + date; + +-- recreate triggers from 005 +drop trigger if exists check_holdings_before_insert_sell; +drop trigger if exists check_holdings_before_update_sell; + +create trigger check_holdings_before_insert_sell before insert on transactions for each row when new.type = 'sell' begin +select + case + when ( + select + holdings + from + assets_ext a + where + id = new.asset_id + ) < new.quantity then raise (abort, 'Insufficient holdings') + end; + +end; + +create trigger check_holdings_before_update_sell before +update on transactions for each row when new.type = 'sell' begin +select + case + when ( + select + holdings + from + assets_ext a + where + id = new.asset_id + ) < new.quantity then raise (abort, 'Insufficient holdings') + end; + +end; diff --git a/.migrations/007_extends_prefs.down.sql b/.migrations/007_extends_prefs.down.sql new file mode 100644 index 00000000..140b182c --- /dev/null +++ b/.migrations/007_extends_prefs.down.sql @@ -0,0 +1,47 @@ +-- 007_extends_prefs.down.sql + +-- recreate old prefs table +drop table if exists prefs_old; + +create table prefs_old ( + id integer primary key autoincrement, + user_id integer not null, + base_ccy text check ( + base_ccy in ( + 'USD', + 'GBP', + 'EUR', + 'CAD', + 'AUD', + 'CHF', + 'SEK', + 'NOK', + 'DKK', + 'NZD', + 'JPY' + ) + ) not null default 'USD', + created datetime default current_timestamp, + modified datetime default current_timestamp, + foreign key (user_id) references users (id) on delete cascade, + unique (user_id) +); + +-- migrate data back +insert into prefs_old (id, user_id, base_ccy, created, modified) +select id, user_id, base_ccy, created, modified +from prefs; + +-- drop new prefs table +drop table prefs; + +-- rename old back to prefs +alter table prefs_old rename to prefs; + +-- recreate trigger +drop trigger if exists insert_user_prefs; + +create trigger insert_user_prefs after insert on users for each row begin +insert into prefs (user_id, base_ccy) +values (new.id, 'USD'); +end; diff --git a/.migrations/007_extends_prefs.up.sql b/.migrations/007_extends_prefs.up.sql new file mode 100644 index 00000000..b5eab9e6 --- /dev/null +++ b/.migrations/007_extends_prefs.up.sql @@ -0,0 +1,45 @@ +ALTER TABLE prefs +RENAME TO prefs_old; + +CREATE TABLE + prefs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + base_ccy TEXT NOT NULL DEFAULT 'USD', + additional TEXT NOT NULL DEFAULT '{}', + created DATETIME DEFAULT CURRENT_TIMESTAMP, + modified DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + UNIQUE (user_id) + ); + +INSERT INTO + prefs ( + id, + user_id, + base_ccy, + additional, + created, + modified + ) +SELECT + id, + user_id, + base_ccy, + '{}' AS additional, + created, + modified +FROM + prefs_old; + +DROP TRIGGER IF EXISTS insert_user_prefs; + +CREATE TRIGGER insert_user_prefs AFTER INSERT ON users FOR EACH ROW BEGIN +INSERT INTO + prefs (user_id, base_ccy) +VALUES + (NEW.id, 'USD'); + +END; + +DROP TABLE prefs_old; \ No newline at end of file diff --git a/README.md b/README.md index 6eee0f19..a821f70c 100644 --- a/README.md +++ b/README.md @@ -1,158 +1,254 @@ # `Assets` — Personal Wealth Tracker [![CHECKS-AND-INTEGRATION-TESTS](https://github.com/venil7/assets/actions/workflows/build-and-test.yaml/badge.svg)](https://github.com/venil7/assets/actions/workflows/build-and-test.yaml) - [![RELEASE-DOCKER-CONTAINER](https://github.com/venil7/assets/actions/workflows/build-docker-container.yaml/badge.svg)](https://github.com/venil7/assets/actions/workflows/build-docker-container.yaml) -A self-hosted net worth and portfolio manager. -Track multiple portfolios (ISA, General, Pension, Crypto, etc.) and monitor individual or total performance. -Supports any asset available via the [Yahoo Finance API](https://finance.yahoo.com/). +A self-hosted portfolio and wealth tracker for personal finance management. Consolidate your investments across brokers and accounts - ISA, 401k, IRA, Taxable, Crypto or custom portfolios - and monitor performance with complete privacy and data ownership. Built on real-time market data from [Yahoo Finance](https://finance.yahoo.com/). + +**Why self-host?** Keep your financial data private. No third-party tracking, no cloud constraints, no monthly subscriptions. Your wealth data stays on your servers. + +## Key Features + +- **Multi-user support** — Manage portfolios for multiple users on a single instance +- **Multi-portfolio tracking** — ISA, 401k, IRA, Taxable, Pension, Crypto, or custom categories +- **Portfolio performance analytics** — Total net worth, individual or aggregated performance +- **Real-time market data** — Live prices for any publicly-traded asset via Yahoo Finance API +- **Multi-currency support** — Track assets in any major currency with automatic FX conversion +- **Return calculations** — Realized gains, unrealized returns, and FX impact analysis +- **Transaction-level insights** — Per-transaction P&L, cost basis tracking +- **CSV import** — Bulk import transactions from broker exports (most platforms supported) +- **REST API** — Full-featured API for integration or headless operation +- **Web dashboard** — Modern, responsive UI for desktop and mobile (built with React + TypeScript) + +## Ideal For -## Features - - Multi user - - Multi portfolio, total net worth - - CSV import from your broker - - All major currencies as base - - Foreign exchange return impact - - capital gains and unrealized return and - - Per transaction impact +- **Individual investors** tracking portfolios across multiple brokers or accounts +- **Households** monitoring combined net worth and investment performance +- **Privacy-conscious users** who want investment data on their own servers +- **Power users** comfortable self-hosting and importing transaction data manually +- **Integration scenarios** where REST API access to portfolio data is needed --- screenshot1 +screenshot0 screenshot2 screenshot3 --- -## Quick Start (Docker) +## Quick Start -```sh -docker pull ghcr.io/venil7/assets:latest && \ +### Docker (Fastest) + +Pull and run the latest image: + +```bash +docker pull ghcr.io/venil7/assets:latest docker run \ - -e ASSETS_JWT_SECRET=S0meSecretVa1ue \ + -e ASSETS_JWT_SECRET=YourSecretKey123 \ -e ASSETS_DB=/data/assets.db \ -p 4020:4020 \ -v ${PWD}:/data \ ghcr.io/venil7/assets ``` -Then navigate to [localhost:4020](localhost:4020) and login using `admin` / `admin` +Then open [http://localhost:4020](http://localhost:4020) and login with `admin` / `admin` -## Docker Compose (Recommended) +> **Note:** Change `ASSETS_JWT_SECRET` to a strong random value for production. -- Create an `.env` with individual parameters, for example: +### Docker Compose (Recommended for Production) -```sh -TAG=latest # or you can try feature branch -ASSETS_CACHE_TTL=10m # how long to cache for , before hitting Yahoo Finance API -ASSETS_JWT_SECRET=S0meSecretVa1ue # a unique key for JWT token -ASSETS_JWT_EXPIRES_IN=1w # how long is JWT valid -ASSETS_JWT_REFRESH_BEFORE=5d # when to refresh JWT, before expiry -``` +1. Create a `.env` file with required parameters: -- Copy this [docker-compose.yaml](docker-compose.yaml) -- Run `docker compose up -d` +```bash +TAG=latest +ASSETS_JWT_SECRET=YourSecretKey123RandomString +ASSETS_CACHE_TTL=10m +ASSETS_JWT_EXPIRES_IN=1w +ASSETS_JWT_REFRESH_BEFORE=5d +``` -### Available `.env` Variables +2. Copy the [docker-compose.yaml](docker-compose.yaml) to your deployment directory +3. Run: `docker compose up -d` +4. Open [http://localhost:8084](http://localhost:8084) and login with `admin` / `admin` + +**Environment Variables Reference** + +| Variable | Purpose | Default | Notes | +|----------|---------|---------|-------| +| `ASSETS_JWT_SECRET` | Token signing key | *(none)* | **Required.** Use a strong random string. | +| `ASSETS_JWT_EXPIRES_IN` | Token expiry duration | `24h` | Format: `24h`, `1w`, etc. | +| `ASSETS_JWT_REFRESH_BEFORE` | Auto-refresh threshold | `12h` | Refresh token before this duration before expiry. | +| `ASSETS_CACHE_TTL` | Market data cache duration | `1m` | How long to cache Yahoo Finance prices. | +| `ASSETS_CACHE_SIZE` | Number of cached items | `1000` | Increase if tracking many assets. | +| `ASSETS_DB` | Database file path | `/data/assets.db` | SQLite database location. | +| `ASSETS_PORT` | Server port | `4020` | Internal container port. | +| `ASSETS_USERNAME` | Default admin username | `admin` | Change after first login. | +| `ASSETS_PASSWORD` | Default admin password | `admin` | Change after first login. | + +## Build & Run Locally + +**Requirements:** [Bun runtime](https://bun.sh) (1.0+) + +```bash +# Clone and setup +git clone https://github.com/venil7/assets.git +cd assets +cp .env.example .env # Create config file +bun install # Install dependencies +``` -| Variable | Description | Default | -| --------------------------- | ------------------------------ | ----------------- | -| `ASSETS_DB` | Path to database | `/data/assets.db` | -| `ASSETS_APP` | Path to public build | `../dist/public` | -| `ASSETS_PORT` | HTTP port | `4020` | -| `ASSETS_CACHE_SIZE` | Cache size | `1000` | -| `ASSETS_CACHE_TTL` | Cache TTL (`ms` or `1m`, etc.) | `1m` | -| `ASSETS_USERNAME` | Admin username | `admin` | -| `ASSETS_PASSWORD` | Admin password | `admin` | -| `ASSETS_JWT_SECRET` | JWT secret | — | -| `ASSETS_JWT_EXPIRES_IN` | Token expiry | `24h` | -| `ASSETS_JWT_REFRESH_BEFORE` | Refresh before expiry | `12h` | +**Development servers** (run in separate terminals): -- Navigate to [localhost:8084](http://localhost:8084), login using `admin` / `admin` +```bash +# Terminal 1: Backend API +bun run backend:dev # Runs on http://localhost:4020 -## Build & Run Locally (Bun) +# Terminal 2: Frontend UI (Vite dev server) +bun run web:dev # Runs on http://localhost:5173, proxies API to :4020 +``` -This software is written in TypeScript and assumes that it runs in [Bun](https://bun.sh). +**Build for production:** -- Clone repository -- Have a `.env` placed in repository root -- The following commands are available +```bash +bun run build # Builds both backend and frontend +``` -```sh -bun install # install all dependencies +**Testing:** -bun run check # checks the integrity of the code -bun run web:dev # runs UI in dev mode -bun run backend:dev # runs backend API in dev mode -bun run build # runs the build for both backend and frontend +```bash +# Run all tests (requires backend:dev running in another terminal) +bun test -bun test # runs unit and integartion tests , make sure to run backend in another terminal +# Run specific package tests +cd packages/backend && bun test +cd packages/core && bun test ``` -- To build a docker container locally `$ docker buildx build -t assets .` +**Code quality checks:** -## Development Notes +```bash +bun run check # Lint and type-check all packages +``` -### build time `.env` parameters placed in `packages/web` +**Build Docker image locally:** -```sh -VITE_ASSETS_URL=http://localhost:4020 # base part of backend REST api url, this param required in VITE DEV mode, but defaults to empty '' in production -VITE_ASSET_BASENAME=/app # the beginnig part of URL before any routing +```bash +docker buildx build -t assets:local . ``` -### sqlite database migration +### TypeScript Monorepo Structure + +This project uses Bun workspaces with three packages: -- To install `migrate` tool run (requires `go`) +- **backend** — Express.js REST API, database queries, business logic +- **core** — Shared types, utilities, validation, domain models +- **web** — React + TypeScript frontend (Vite + SCSS) -```sh -$ go install -tags 'sqlite3' github.com/golang-migrate/migrate/v4/cmd/migrate@latest +## Development + +### Environment Configuration + +**Vite build-time variables** (in `packages/web`): + +```bash +VITE_ASSETS_URL=http://localhost:4020 # Backend base URL (required in dev; empty string in production) +VITE_ASSET_BASENAME=/app # URL path prefix for routing (default: /) ``` -The following `migrate` commands are available +### Database Migrations + +Migrations are managed with [golang-migrate](https://github.com/golang-migrate/migrate). This is **optional**—the app creates the schema automatically on first run. -```sh +If you want to manually manage migrations: + +```bash +# Install migrate tool (requires Go) +go install -tags 'sqlite3' github.com/golang-migrate/migrate/v4/cmd/migrate@latest + +# Create a new migration migrate create -ext sql -dir .migrations/ -seq -digits 3 + +# Apply pending migrations migrate -path ./.migrations -database=sqlite3://assets.db up + +# Rollback migrations migrate -path ./.migrations -database=sqlite3://assets.db down -migrate -path ./.migrations -database=sqlite3://assets.db version ``` -### Running tests +### Testing in Docker + +To run the full test suite in an isolated container: -```sh -# Build a test container +```bash +# Build test image docker buildx build -t assets-test -f ./Dockerfile.test . -# Run tests in a container + +# Run tests docker run -it assets-test ``` -## API-First Design +## API-First Architecture -The UI is optional — the backend exposes a full REST API. +The web UI is optional. The backend exposes a **complete REST API** for any frontend or integration: -- [API Documentation](./API.md) +- REST endpoint documentation: [API.md](./API.md) +- Use in headless mode, CLI tools, or custom integrations +- Full authentication and multi-tenant support -## Upload External Transactions +## Importing Transactions -It is possible to upload transactions for individual assets using CSV files, which can usually be downloaded from most brokers and platforms. The CSV file should have the following columns: +Assets supports bulk CSV import for transaction history. Most brokers and trading platforms allow you to export transaction history in CSV format. -- _type_: The type of transaction, either "buy" or "sell" (case insensitive) -- _quantity_: The number of shares (float) -- _price_: The price per share (float) -- _date_: The date of the transaction in ISO format -- _comments_: Any comments related to the transaction (can be left blank) +### CSV Format -Example: +Your CSV file should have these columns: -``` +| Column | Format | Example | +|--------|--------|---------| +| `type` | `buy` or `sell` (case-insensitive) | `buy` | +| `quantity` | Decimal number | `65.5` | +| `price` | Decimal number | `125.50` | +| `date` | ISO 8601 datetime | `2025-05-11T06:56:39.379Z` | +| `comments` | Text (optional) | `Dividend reinvestment` | + +### Example CSV + +```csv type,quantity,price,date,comments -buy,65,1,2025-05-11T06:56:39.379Z,comment -sell,18,2,2025-06-12T06:56:39.379Z, +buy,65,1,2025-05-11T06:56:39.379Z,Initial investment +sell,18,2,2025-06-12T06:56:39.379Z,Rebalancing +buy,100,125.50,2025-07-01T10:30:00Z,Monthly investment ``` +> **Tip:** Export transaction history from your broker's web portal, clean it up to match this format, and upload via the UI. + ## Contributing -This codebase is 100% hand written, no AI slop. If you feel comfortable with TypeScript, [functional programming](https://amzn.eu/d/axUrvVz) and basic SQL - contributions are welcome. If you find a bug, kindly open a [Github Issue](https://github.com/venil7/assets/issues) +This codebase is **hand-written, AI-free code**. 100% human. + +Contributions welcome if you're comfortable with: + +- **TypeScript** — Full-stack, strict type safety +- **Functional programming** — Heavy use of fp-ts, category theory patterns +- **SQL & SQLite** — Database queries and schema design +- **React** — Modern TypeScript component patterns +- **REST APIs** — Express.js, RESTful design + +### Getting Started + +1. [Build locally](#build--run-locally) with `bun install && bun run backend:dev` +2. Run tests: `bun test` (backend must be running) +3. Make changes, ensure `bun run check` passes (lint + type-check) +4. Submit a PR with clear description of changes + +### Found a Bug? + +Open a [GitHub Issue](https://github.com/venil7/assets/issues) with: + +- Steps to reproduce +- Expected vs. actual behavior +- Your setup (Docker/local, OS, environment) ## Licence diff --git a/TODO.md b/TODO.md index cf46e3c7..9fd0a80e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,12 @@ # todo +## 1.8.1 +- [x] multi asset layered chart for portfolios +- [x] multi portfollio layered chart for home screen +- [x] caching of enriched data +- [x] INR +- [-] mock yahoo api for tests + ## 1.8 - [x] chore: move assets-core to core @@ -15,10 +22,6 @@ - [x] enriched portfolio : break even - [x] chart merge in polars - [x] chart shows transactions -- [ ] multi asset layered chart for portfolios -- [ ] multi portfollio layered chart for home screen -- [ ] INR -- [ ] mock yahoo api for tests ## 1.7.1 diff --git a/bun.lock b/bun.lock index 6407457a..104c9b56 100644 --- a/bun.lock +++ b/bun.lock @@ -7,14 +7,14 @@ "dependencies": { "date-fns": "^4.1.0", "fp-ts": "^2.16.11", + "io-ts": "~2.2.21", "io-ts-types": "^0.5.19", - "ms": "^2.1.3", - "prettier": "^3.6.2", }, "devDependencies": { "@types/bun": "1.3.11", "bun-types": "^1.3.11", - "typescript": "~5.9.3", + "prettier": "^3.6.2", + "typescript": "~6.0.2", }, }, "packages/backend": { @@ -22,15 +22,14 @@ "version": "1.8.0", "dependencies": { "@darkruby/assets-core": "workspace:*", - "@types/ms": "^2.1.0", "cors": "^2.8.5", "csv-parse": "^6.1.0", "csv-stringify": "^6.6.0", "express": "~4.21.2", "heap-js": "^2.7.1", - "io-ts": "~2.2.21", "jsonwebtoken": "^9.0.2", "lru-cache": "^11.0.2", + "ms": "^2.1.3", "nodejs-polars": "^0.24.0", }, "devDependencies": { @@ -39,6 +38,7 @@ "@types/faker": "5", "@types/jsonwebtoken": "^9.0.9", "@types/lru-cache": "^7.10.10", + "@types/ms": "^2.1.0", "faker": "5", "nock": "^14.0.1", }, @@ -74,7 +74,7 @@ "react-dropzone": "^14.3.8", "react-router": "^7.3.0", "react-select": "^5.10.1", - "recharts": "^3.8.0", + "recharts": "3.8.0", }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -119,15 +119,15 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], - "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], @@ -251,7 +251,7 @@ "@fortawesome/free-regular-svg-icons": ["@fortawesome/free-regular-svg-icons@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-iycmlN51EULlQ4D/UU9WZnHiN0CvjJ2TuuCrAh+1MVdzD+4ViKYH2deNAll4XAAYlZa8WAefHR5taSK8hYmSMw=="], - "@fortawesome/react-fontawesome": ["@fortawesome/react-fontawesome@3.2.0", "", { "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~6 || ~7", "react": "^18.0.0 || ^19.0.0" } }, "sha512-E9Gu1hqd6JussVO26EC4WqRZssXMnQr2ol7ZNWkkFOH8jZUaxDJ9Z9WF9wIVkC+kJGXUdY3tlffpDwEKfgQrQw=="], + "@fortawesome/react-fontawesome": ["@fortawesome/react-fontawesome@3.3.0", "", { "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~6 || ~7", "react": "^18.0.0 || ^19.0.0" } }, "sha512-EHmHeTf8WgO29sdY3iX/7ekE3gNUdlc2RW6mm/FzELlHFKfTrA9S4MlyquRR+RRCRCn8+jXfLFpLGB2l7wCWyw=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -311,7 +311,7 @@ "@preact/signals-core": ["@preact/signals-core@1.14.0", "", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="], - "@preact/signals-react": ["@preact/signals-react@3.9.1", "", { "dependencies": { "@preact/signals-core": "^1.14.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-4bj3wUfXrYOqDDs6sX2Y5GBC8jgSa8ah8ZJHN2A+23ej+TdPDrtwVJ0e1cEhwemyOZ7Q7NHbilPRhtd5zVpaBA=="], + "@preact/signals-react": ["@preact/signals-react@3.10.0", "", { "dependencies": { "@preact/signals-core": "^1.14.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-Uxu6lidNVr9z27b/6DbCin86ekzHiJDrLXZii82aXSzvyMXYMr7l0Bab1cKbfWdbkxq13e7kS7paix3pjKBTLA=="], "@react-aria/ssr": ["@react-aria/ssr@3.9.10", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ=="], @@ -323,55 +323,55 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w=="], "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], @@ -379,7 +379,7 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="], + "@swc/helpers": ["@swc/helpers@0.5.20", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -455,27 +455,27 @@ "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], - "@types/warning": ["@types/warning@3.0.3", "", {}, "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q=="], + "@types/warning": ["@types/warning@3.0.4", "", {}, "sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/type-utils": "8.57.2", "@typescript-eslint/utils": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.2", "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.2", "@typescript-eslint/tsconfig-utils": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw=="], "@unhead/react": ["@unhead/react@2.1.12", "", { "dependencies": { "unhead": "2.1.12" }, "peerDependencies": { "react": ">=18.3.1" } }, "sha512-1xXFrxyw29f+kScXfEb0GxjlgtnHxoYau0qpW9k8sgWhQUNnE5gNaH3u+rNhd5IqhyvbdDRJpQ25zoz0HIyGaw=="], @@ -503,13 +503,13 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.7", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="], "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], "bootstrap": ["bootstrap@5.3.8", "", { "peerDependencies": { "@popperjs/core": "^2.11.8" } }, "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], @@ -525,7 +525,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001778", "", {}, "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -561,9 +561,9 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "csv-parse": ["csv-parse@6.1.0", "", {}, "sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw=="], + "csv-parse": ["csv-parse@6.2.1", "", {}, "sha512-LRLMV+UCyfMokp8Wb411duBf1gaBKJfOfBWU9eHMJ+b+cJYZsNu3AFmjJf3+yPGd59Exz1TsMjaSFyxnYB9+IQ=="], - "csv-stringify": ["csv-stringify@6.6.0", "", {}, "sha512-YW32lKOmIBgbxtu3g5SaiqWNwa/9ISQt2EcgOq0+RAIFufFp9is6tqNnKahqE5kuKvrnYAzs28r+s6pXJR8Vcw=="], + "csv-stringify": ["csv-stringify@6.7.0", "", {}, "sha512-UdtziYp5HuTz7e5j8Nvq+a/3HQo+2/aJZ9xntNTpmRRIg/3YYqDVgiS9fvAhtNbnyfbv2ZBe0bqCHqzhE7FqWQ=="], "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], @@ -611,7 +611,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.328", "", {}, "sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], @@ -681,7 +681,7 @@ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - "flatted": ["flatted@3.4.1", "", {}, "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ=="], + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], @@ -719,7 +719,7 @@ "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], - "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="], + "hookable": ["hookable@6.1.0", "", {}, "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw=="], "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], @@ -761,7 +761,7 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="], + "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -901,7 +901,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], @@ -943,7 +943,7 @@ "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - "react-router": ["react-router@7.13.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA=="], + "react-router": ["react-router@7.13.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA=="], "react-select": ["react-select@5.10.2", "", { "dependencies": { "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", "@emotion/react": "^11.8.1", "@floating-ui/dom": "^1.0.1", "@types/react-transition-group": "^4.4.0", "memoize-one": "^6.0.0", "prop-types": "^15.6.0", "react-transition-group": "^4.3.0", "use-isomorphic-layout-effect": "^1.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ=="], @@ -963,7 +963,7 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="], "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], @@ -1063,7 +1063,7 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1071,9 +1071,9 @@ "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], - "typescript-eslint": ["typescript-eslint@8.57.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.0", "@typescript-eslint/parser": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA=="], + "typescript-eslint": ["typescript-eslint@8.57.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.2", "@typescript-eslint/parser": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A=="], "uncontrollable": ["uncontrollable@7.2.1", "", { "dependencies": { "@babel/runtime": "^7.6.3", "@types/react": ">=16.9.11", "invariant": "^2.2.4", "react-lifecycles-compat": "^3.0.4" }, "peerDependencies": { "react": ">=15.0.0" } }, "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ=="], @@ -1109,7 +1109,7 @@ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + "yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -1127,6 +1127,8 @@ "@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "@darkruby/assets-web/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -1167,7 +1169,7 @@ "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } diff --git a/package.json b/package.json index 33dd4946..25a45b22 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { "name": "@darkruby/assets", "private": true, - "version": "1.8.0", + "version": "1.8.1", "type": "module", "dependencies": { "date-fns": "^4.1.0", "fp-ts": "^2.16.11", - "io-ts-types": "^0.5.19", - "ms": "^2.1.3", - "prettier": "^3.6.2" + "io-ts": "~2.2.21", + "io-ts-types": "^0.5.19" }, "devDependencies": { + "prettier": "^3.6.2", "@types/bun": "1.3.11", "bun-types": "^1.3.11", - "typescript": "~5.9.3" + "typescript": "~6.0.2" }, "scripts": { "web:dev": "cd packages/web && bun run dev", @@ -23,9 +23,9 @@ "backend:test": "cd packages/backend && bun test", "backend:check": "cd packages/backend && bun run check", "backend:build": "cd packages/backend && bun run build", - "assets-core:check": "cd packages/core && bun run check", - "assets-core:test": "cd packages/core && bun run test", - "check": "bun run assets-core:check && bun run backend:check && bun run web:check", + "core:check": "cd packages/core && bun run check", + "core:test": "cd packages/core && bun run test", + "check": "bun run core:check && bun run backend:check && bun run web:check", "build": "bun run web:build && bun run backend:build" }, "workspaces": [ diff --git a/packages/backend/package.json b/packages/backend/package.json index 3cd78538..b94d2016 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "@darkruby/assets-backend", - "version": "1.8.0", + "version": "1.8.1", "main": "src/index.ts", "type": "module", "scripts": { @@ -11,18 +11,18 @@ }, "dependencies": { "@darkruby/assets-core": "workspace:*", - "@types/ms": "^2.1.0", + "ms": "^2.1.3", "cors": "^2.8.5", "csv-parse": "^6.1.0", "csv-stringify": "^6.6.0", "express": "~4.21.2", "heap-js": "^2.7.1", - "io-ts": "~2.2.21", "jsonwebtoken": "^9.0.2", "lru-cache": "^11.0.2", "nodejs-polars": "^0.24.0" }, "devDependencies": { + "@types/ms": "^2.1.0", "@types/cors": "^2.8.17", "@types/faker": "5", "@types/jsonwebtoken": "^9.0.9", diff --git a/packages/backend/src/enrichment/asset.ts b/packages/backend/src/enrichment/asset.ts index 428f6219..4a39caab 100644 --- a/packages/backend/src/enrichment/asset.ts +++ b/packages/backend/src/enrichment/asset.ts @@ -14,20 +14,19 @@ import * as TE from "fp-ts/lib/TaskEither"; import type { Repository } from "../repository"; import { type YahooApi } from "../yahoo/client"; import { $enrichedAssetBase, $enrichedAssetCcy, txsWithRates } from "./returns"; -import { getTxsEnricher } from "./tx"; +import type { TxEnricher } from "./tx"; -export const getAssetEnricher = - (repo: Repository, yahooApi: YahooApi) => +const getAssetEnricher = + (repo: Repository, yahooApi: YahooApi, { enrichMany }: TxEnricher) => ( asset: GetAsset, range: ChartRange = DEFAULT_CHART_RANGE ): Action => { - const enrichTxs = getTxsEnricher(yahooApi); return pipe( TE.Do, TE.bind("chart", () => yahooApi.chart(asset.ticker, range)), TE.bind("txs", () => repo.tx.getAll(asset.id, asset.user_id, false)), - TE.bind("enrichedTxs", ({ txs }) => enrichTxs(txs)), + TE.bind("enrichedTxs", ({ txs }) => enrichMany(txs)), TE.bind("fxRates", ({ chart }) => yahooApi.fxRates(chart.meta.currency, asset.base_ccy) ), @@ -61,10 +60,10 @@ export const getAssetEnricher = ); }; -export const getAssetsEnricher = - (repo: Repository, yahooApi: YahooApi) => +const getAssetsEnricher = + (repo: Repository, yahooApi: YahooApi, txEnricher: TxEnricher) => (assets: GetAsset[], range?: ChartRange): Action => { - const enrichAsset = getAssetEnricher(repo, yahooApi); + const enrichAsset = getAssetEnricher(repo, yahooApi, txEnricher); return pipe( assets, TE.traverseArray((asset) => enrichAsset(asset, range)), @@ -72,20 +71,20 @@ export const getAssetsEnricher = ) as Action; }; -export const getOptionalAssetEnricher = - (repo: Repository, yahooApi: YahooApi) => +const getOptionalAssetEnricher = + (repo: Repository, yahooApi: YahooApi, txEnricher: TxEnricher) => ( asset: Optional, range?: ChartRange ): Action> => { if (asset) { - const enrichAsset = getAssetEnricher(repo, yahooApi); + const enrichAsset = getAssetEnricher(repo, yahooApi, txEnricher); return enrichAsset(asset, range); } return TE.of(null); }; -export const calcAssetWeights = (assets: EnrichedAsset[]): EnrichedAsset[] => { +const calcAssetWeights = (assets: EnrichedAsset[]): EnrichedAsset[] => { const total = pipe( assets, sum(({ base }) => base.invested) @@ -100,3 +99,18 @@ export const calcAssetWeights = (assets: EnrichedAsset[]): EnrichedAsset[] => { }) ); }; + +export type AssetEnricher = ReturnType; + +export const createAssetEnricher = ( + repo: Repository, + yahooApi: YahooApi, + txEnricher: TxEnricher +) => { + return { + enrich: getAssetEnricher(repo, yahooApi, txEnricher), + enrichMany: getAssetsEnricher(repo, yahooApi, txEnricher), + enrichMaybe: getOptionalAssetEnricher(repo, yahooApi, txEnricher), + calcAssetWeights + }; +}; diff --git a/packages/backend/src/enrichment/cached.ts b/packages/backend/src/enrichment/cached.ts new file mode 100644 index 00000000..f56e7cf6 --- /dev/null +++ b/packages/backend/src/enrichment/cached.ts @@ -0,0 +1,122 @@ +import { + DEFAULT_CHART_RANGE, + type ChartRange, + type GetAsset, + type GetPortfolio, + type GetTx, + type Optional +} from "@darkruby/assets-core"; +import ms from "ms"; +import type { Repository } from "../repository"; +import type { AppCache } from "../services/cache"; +import type { YahooApi } from "../yahoo/client"; +import { createAssetEnricher, type AssetEnricher } from "./asset"; +import { key } from "./key"; +import { createPortfolioEnricher, type PortfolioEnricher } from "./portfolio"; +import { createSummaryEnricher, type SummaryEnricher } from "./summary"; +import { createTxEnricher, type TxEnricher } from "./tx"; + +const txKey = key("enrich-tx"); +const assetKey = key("enrich-asset"); +const portfolioKey = key("enrich-portfolio"); + +export type Enricher = { + tx: TxEnricher; + asset: AssetEnricher; + portfolio: PortfolioEnricher; + summary: SummaryEnricher; +}; + +export const createEnricher = ( + repo: Repository, + yahooApi: YahooApi, + cache: AppCache +): Enricher => { + const ENRICH_TTL = ms("1min"); + + const txEnricher = createTxEnricher(yahooApi); + const cachedTxEnricher = { + ...txEnricher, + enrich: (tx: GetTx) => { + const action = () => txEnricher.enrich(tx); + return cache.cachedAction(txKey(tx), action, ENRICH_TTL); + }, + enrichMany: (txs: GetTx[]) => { + const action = () => txEnricher.enrichMany(txs); + return cache.cachedAction(txKey({ txs }), action, ENRICH_TTL); + } + }; + + const assetEnricher = createAssetEnricher(repo, yahooApi, cachedTxEnricher); + const cachedAssetEnricher = { + ...assetEnricher, + enrich: (asset: GetAsset, range: ChartRange = DEFAULT_CHART_RANGE) => { + const action = () => assetEnricher.enrich(asset, range); + return cache.cachedAction(assetKey({ asset, range }), action, ENRICH_TTL); + }, + enrichMaybe: ( + asset: Optional, + range: ChartRange = DEFAULT_CHART_RANGE + ) => { + const action = () => assetEnricher.enrichMaybe(asset, range); + return cache.cachedAction(assetKey({ asset, range }), action, ENRICH_TTL); + }, + enrichMany: ( + assets: GetAsset[], + range: ChartRange = DEFAULT_CHART_RANGE + ) => { + const action = () => assetEnricher.enrichMany(assets, range); + return cache.cachedAction( + assetKey({ assets, range }), + action, + ENRICH_TTL + ); + } + }; + + const portfolioEnricher = createPortfolioEnricher(repo, cachedAssetEnricher); + const cachedPortfolioEnricher = { + ...portfolioEnricher, + enrich: ( + portfolio: GetPortfolio, + range: ChartRange = DEFAULT_CHART_RANGE + ) => { + const action = () => portfolioEnricher.enrich(portfolio, range); + return cache.cachedAction( + portfolioKey({ portfolio, range }), + action, + ENRICH_TTL + ); + }, + enrichMaybe: ( + portfolio: Optional, + range: ChartRange = DEFAULT_CHART_RANGE + ) => { + const action = () => portfolioEnricher.enrichMaybe(portfolio, range); + return cache.cachedAction( + portfolioKey({ portfolio, range }), + action, + ENRICH_TTL + ); + }, + enrichMany: ( + portfolios: GetPortfolio[], + range: ChartRange = DEFAULT_CHART_RANGE + ) => { + const action = () => portfolioEnricher.enrichMany(portfolios, range); + return cache.cachedAction( + portfolioKey({ portfolios, range }), + action, + ENRICH_TTL + ); + } + }; + const summaryEnricher = createSummaryEnricher(); + + return { + tx: cachedTxEnricher, + asset: cachedAssetEnricher, + portfolio: cachedPortfolioEnricher, + summary: { ...summaryEnricher } + }; +}; diff --git a/packages/backend/src/enrichment/chart.ts b/packages/backend/src/enrichment/chart.ts index f5c73898..3e787714 100644 --- a/packages/backend/src/enrichment/chart.ts +++ b/packages/backend/src/enrichment/chart.ts @@ -307,29 +307,16 @@ const combineMultiChart = ); }; -export const combineAssetsMultiChart = combineMultiChart( +export const portfolioMultiChart = combineMultiChart( ({ name: id, base }) => ({ id, chart: base.chart }) ); -export const combineSummaryMultiChart = combineMultiChart( +export const summaryMultiChart = combineMultiChart( ({ name: id, chart }) => ({ id, chart }) ); - -// export const flattenMultiChart = (chart: MultiChartData): ChartData => { -// return pipe( -// chart, -// R.toEntries, -// A.map(([, chart]) => readRecords(chart, { schema: ChartSchema })), -// (dfs) => concat(dfs) -// ) -// .groupBy("timestamp") -// .agg(col("price").sum(), col("volume").sum()) -// .sort("timestamp") -// .toRecords() as ChartData; -// }; diff --git a/packages/backend/src/enrichment/index.ts b/packages/backend/src/enrichment/index.ts index 43621f1b..d821b867 100644 --- a/packages/backend/src/enrichment/index.ts +++ b/packages/backend/src/enrichment/index.ts @@ -1,4 +1,5 @@ export * from "./asset"; +export * from "./cached"; export * from "./portfolio"; export * from "./summary"; export * from "./tx"; diff --git a/packages/backend/src/enrichment/key.ts b/packages/backend/src/enrichment/key.ts new file mode 100644 index 00000000..b8e6ffa5 --- /dev/null +++ b/packages/backend/src/enrichment/key.ts @@ -0,0 +1,11 @@ +export const key = + (prefix: string) => + (t: NonNullable) => + `${prefix}-${JSON.stringify(t)}`; +// export const keys = (prefix: string) => { +// const maybeKey = (t: Optional) => (defined(t) ? key(t) : `-`); +// const multiKey = (ts: NonNullable[]) => +// `${prefix}s-${ts.map(key).join("-")}`; + +// return { key, maybeKey, multiKey }; +// }; diff --git a/packages/backend/src/enrichment/portfolio.ts b/packages/backend/src/enrichment/portfolio.ts index 6302b0b8..c05695f7 100644 --- a/packages/backend/src/enrichment/portfolio.ts +++ b/packages/backend/src/enrichment/portfolio.ts @@ -22,9 +22,12 @@ import { pipe } from "fp-ts/lib/function"; import * as Ord from "fp-ts/lib/Ord"; import * as TE from "fp-ts/lib/TaskEither"; import type { Repository } from "../repository"; -import type { YahooApi } from "../yahoo/client"; -import { calcAssetWeights, getAssetsEnricher } from "./asset"; -import { combineAssetCharts, commonAssetRanges } from "./chart"; +import type { AssetEnricher } from "./asset"; +import { + combineAssetCharts, + commonAssetRanges, + portfolioMultiChart +} from "./chart"; const sumInvested = sum(({ base }) => base.invested); const sumRealizedPnl = sum(({ base }) => base.realizedPnl); @@ -105,21 +108,19 @@ const portfolioTotals = (assets: EnrichedAsset[]): Totals => { return { returnValue, returnPct }; }; -export const getPortfolioEnricher = - (repo: Repository, yahooApi: YahooApi) => +const getPortfolioEnricher = + (repo: Repository, { enrichMany, calcAssetWeights }: AssetEnricher) => ( portfolio: GetPortfolio, range: ChartRange = DEFAULT_CHART_RANGE ): Action => { - const enrichAssets = getAssetsEnricher(repo, yahooApi); - return pipe( TE.Do, TE.apS("portfolio", TE.of(portfolio)), TE.bind("assets", () => pipe( repo.asset.getAll(portfolio.id, portfolio.user_id), - TE.chain((assets) => enrichAssets(assets, range)), + TE.chain((assets) => enrichMany(assets, range)), TE.map(A.filter((a) => Boolean(a.invested))), TE.map(calcAssetWeights) ) @@ -135,6 +136,7 @@ export const getPortfolioEnricher = const totals = portfolioTotals(assets); const changes = portfolioChanges(assets); const chart = combineAssetCharts(assets); + const multiChart = portfolioMultiChart(assets); return { ...portfolio, @@ -145,6 +147,7 @@ export const getPortfolioEnricher = domestic, changes, chart, + multiChart, invested, breakEven, totals, @@ -155,13 +158,13 @@ export const getPortfolioEnricher = ); }; -export const getPortfoliosEnricher = - (repo: Repository, yahooApi: YahooApi) => +const getPortfoliosEnricher = + (repo: Repository, assetEnricher: AssetEnricher) => ( portfolios: GetPortfolio[], range?: ChartRange ): Action => { - const enrichPortfolio = getPortfolioEnricher(repo, yahooApi); + const enrichPortfolio = getPortfolioEnricher(repo, assetEnricher); return pipe( portfolios, TE.traverseArray((p) => enrichPortfolio(p, range)), @@ -170,19 +173,19 @@ export const getPortfoliosEnricher = }; export const getOptionalPorfolioEnricher = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, assetEnricher: AssetEnricher) => ( portfolio: Optional, range?: ChartRange ): Action> => { if (portfolio) { - const enrichPortfolio = getPortfolioEnricher(repo, yahooApi); + const enrichPortfolio = getPortfolioEnricher(repo, assetEnricher); return enrichPortfolio(portfolio, range); } return TE.of(null); }; -export const calcPortfolioWeights = ( +const calcPortfolioWeights = ( portfolios: EnrichedPortfolio[] ): EnrichedPortfolio[] => { const total = pipe( @@ -199,3 +202,17 @@ export const calcPortfolioWeights = ( }) ); }; + +export type PortfolioEnricher = ReturnType; + +export const createPortfolioEnricher = ( + repo: Repository, + assetEnricher: AssetEnricher +) => { + return { + enrich: getPortfolioEnricher(repo, assetEnricher), + enrichMany: getPortfoliosEnricher(repo, assetEnricher), + enrichMaybe: getOptionalPorfolioEnricher(repo, assetEnricher), + calcPortfolioWeights + }; +}; diff --git a/packages/backend/src/enrichment/summary.ts b/packages/backend/src/enrichment/summary.ts index f3070f8b..5c2b9579 100644 --- a/packages/backend/src/enrichment/summary.ts +++ b/packages/backend/src/enrichment/summary.ts @@ -16,7 +16,11 @@ import { import * as A from "fp-ts/lib/Array"; import { pipe } from "fp-ts/lib/function"; import * as Ord from "fp-ts/lib/Ord"; -import { combinePortfolioCharts, commonPortfolioRanges } from "./chart"; +import { + combinePortfolioCharts, + commonPortfolioRanges, + summaryMultiChart +} from "./chart"; const summaryMeta = ( portfolios: EnrichedPortfolio[] @@ -130,20 +134,27 @@ export const enrichSummary = ( ); const chart = combinePortfolioCharts(portfolios); + const multiChart = summaryMultiChart(portfolios); const meta = summaryMeta(portfolios); const changes = summaryChanges(portfolios); const totals = summaryTotals(portfolios); return { - numPortfolios, - chart, meta, - changes, + chart, totals, + changes, invested, fxImpact, + breakEven, + multiChart, realizedPnl, - breakEven - }; + numPortfolios + } satisfies EnrichedSummary; +}; + +export type SummaryEnricher = ReturnType; +export const createSummaryEnricher = () => { + return { enrich: enrichSummary }; }; diff --git a/packages/backend/src/enrichment/tx.ts b/packages/backend/src/enrichment/tx.ts index 62b023f4..8f4741ce 100644 --- a/packages/backend/src/enrichment/tx.ts +++ b/packages/backend/src/enrichment/tx.ts @@ -9,7 +9,7 @@ import { pipe } from "fp-ts/lib/function"; import * as TE from "fp-ts/lib/TaskEither"; import { type YahooApi } from "../yahoo/client"; -export const getTxEnricher = +const getTxEnricher = (yahooApi: YahooApi) => (tx: GetTx): Action => { return pipe( @@ -25,17 +25,26 @@ export const getTxEnricher = const [pnl, pnlPct] = calcPnl({ before: tx.cost, after: value }); return { ...tx, value, pnl, pnl_pct: pnlPct }; } - // consider using EnrichTx decoder + // todo: use EnrichTx decoder const { pnl, value, pnl_pct, ...rest } = tx; return { ...rest, pnl: pnl!, pnl_pct: pnl_pct!, value: value! }; }) ); }; -export const getTxsEnricher = +const getTxsEnricher = (yahooApi: YahooApi) => (txs: GetTx[]): Action => { const enrichTx = getTxEnricher(yahooApi); - // return pipe(txs, TE.traverseSeqArray(enrichTx)) as Action; // <-- sequential to take advantage of yahoo cache - return pipe(txs, TE.traverseArray(enrichTx)) as Action; // <-- sequential to take advantage of yahoo cache + return pipe(txs, TE.traverseSeqArray(enrichTx)) as Action; // <-- sequential to take advantage of yahoo cache + // return pipe(txs, TE.traverseArray(enrichTx)) as Action; // <-- sequential to take advantage of yahoo cache }; + +export type TxEnricher = ReturnType; + +export const createTxEnricher = (yahooApi: YahooApi) => { + return { + enrich: getTxEnricher(yahooApi), + enrichMany: getTxsEnricher(yahooApi) + }; +}; diff --git a/packages/backend/src/handlers/asset.ts b/packages/backend/src/handlers/asset.ts index b877af14..f8248223 100644 --- a/packages/backend/src/handlers/asset.ts +++ b/packages/backend/src/handlers/asset.ts @@ -45,7 +45,7 @@ export const getAsset: HandlerTask, Context> = ({ export const createAsset: HandlerTask, Context> = ({ params: [req, res], - context: { repo, yahooApi, service } + context: { service } }) => pipe( TE.Do, diff --git a/packages/backend/src/handlers/index.ts b/packages/backend/src/handlers/index.ts index 56da42ca..afda372c 100644 --- a/packages/backend/src/handlers/index.ts +++ b/packages/backend/src/handlers/index.ts @@ -13,6 +13,7 @@ import * as user from "./user"; import * as yahoo from "./yahoo"; export type Handlers = ReturnType; + export const createHandlers = ( expressify: (task: HandlerTask) => express.RequestHandler ) => ({ diff --git a/packages/backend/src/handlers/prefs.ts b/packages/backend/src/handlers/prefs.ts index ab2422f2..c73eafb3 100644 --- a/packages/backend/src/handlers/prefs.ts +++ b/packages/backend/src/handlers/prefs.ts @@ -13,7 +13,7 @@ export const getPrefs: HandlerTask = ({ export const updatePrefs: HandlerTask = ({ params: [req, res], - context: { repo, service } + context: { service } }) => pipe( service.auth.requireUserId(res), diff --git a/packages/backend/src/handlers/profile.ts b/packages/backend/src/handlers/profile.ts index 8b32685f..b685a427 100644 --- a/packages/backend/src/handlers/profile.ts +++ b/packages/backend/src/handlers/profile.ts @@ -43,7 +43,7 @@ export const updatePassword: HandlerTask = ({ export const deleteProfile: HandlerTask, Context> = ({ params: [, res], - context: { repo, service } + context: { service } }) => pipe( TE.Do, diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index f84c43dd..60b1cccc 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -4,20 +4,16 @@ import cors from "cors"; import { default as express } from "express"; import { pipe } from "fp-ts/lib/function"; import * as TE from "fp-ts/lib/TaskEither"; -import { LRUCache } from "lru-cache"; import { Server } from "node:http"; import path from "node:path"; +import { createEnricher } from "./enrichment"; import { createRequestHandler } from "./fp-express"; import { createHandlers } from "./handlers"; import type { Context } from "./handlers/context"; import { createRepository, type Repository } from "./repository"; import { execute } from "./repository/database"; import { createWebService } from "./services"; -import { - createCache, - type AppCache, - type Stringifiable -} from "./services/cache"; +import { createCache, type AppCache } from "./services/cache"; import { env, envDurationMsec, envNumber } from "./services/env"; import { initializeApp } from "./services/init"; import { cachedYahooApi } from "./yahoo/cached"; @@ -48,15 +44,7 @@ const repository = (c: Config): Action => ); const cache = ({ cacheSize, cacheTtl }: Config): Action => - pipe( - TE.of( - new LRUCache({ - max: cacheSize, - ttl: cacheTtl - }) - ), - TE.map(createCache) - ); + pipe(TE.of(createCache(cacheSize, cacheTtl))); const server = ({ port, app }: Config, ctx: Context): Action => { const expressify = createRequestHandler(ctx); @@ -175,9 +163,12 @@ const app = () => TE.Do, TE.bind("repo", () => repository(config)), TE.bind("cache", () => cache(config)), - TE.bind("yahooApi", ({ cache }) => TE.of(cachedYahooApi(cache))), - TE.bind("service", ({ repo, yahooApi }) => - TE.of(createWebService(repo, yahooApi)) + TE.let("yahooApi", ({ cache }) => cachedYahooApi(cache)), + TE.let("enricher", ({ repo, cache, yahooApi }) => + createEnricher(repo, yahooApi, cache) + ), + TE.bind("service", ({ repo, yahooApi, enricher }) => + TE.of(createWebService(repo, yahooApi, enricher)) ) ) ), diff --git a/packages/backend/src/repository/prefs.ts b/packages/backend/src/repository/prefs.ts index e23961e0..ce6322ee 100644 --- a/packages/backend/src/repository/prefs.ts +++ b/packages/backend/src/repository/prefs.ts @@ -1,8 +1,8 @@ import { + DbPrefsDecoder, handleError, - PrefsDecoder, type Action, - type Prefs, + type Prefs } from "@darkruby/assets-core"; import { liftTE } from "@darkruby/assets-core/src/decoders/util"; import type { UserId } from "@darkruby/assets-core/src/domain/user"; @@ -16,8 +16,8 @@ import { getPrefsSql, updatePrefsSql } from "./sql" with { type: "macro" }; const sql = { prefs: { get: TE.of(getPrefsSql()), - update: TE.of(updatePrefsSql()), - }, + update: TE.of(updatePrefsSql()) + } }; export const getPrefs = @@ -27,14 +27,15 @@ export const getPrefs = queryOne({ userId }), ID.ap(sql.prefs.get), ID.ap(db), - TE.chain(liftTE(PrefsDecoder)) + TE.chain(liftTE(DbPrefsDecoder)) ); export const updatePrefs = (db: Database) => (userId: UserId, prefs: Prefs): Action => { + const dbPrefs = DbPrefsDecoder.encode(prefs); return pipe( - execute({ ...prefs, userId }), + execute({ ...dbPrefs, userId }), ID.ap(sql.prefs.update), ID.ap(db), TE.chain(() => getPrefs(db)(userId)), diff --git a/packages/backend/src/repository/sql/asset/update.sql b/packages/backend/src/repository/sql/asset/update.sql index af648bd5..0f5b6886 100644 --- a/packages/backend/src/repository/sql/asset/update.sql +++ b/packages/backend/src/repository/sql/asset/update.sql @@ -1,6 +1,6 @@ UPDATE assets SET name = $name, ticker = $ticker, - modified = CURRENT_TIMESTAMP + modified = datetime('subsec') WHERE id = $assetId and portfolio_id = $portfolioId \ No newline at end of file diff --git a/packages/backend/src/repository/sql/portfolio/update.sql b/packages/backend/src/repository/sql/portfolio/update.sql index 6bed22d2..183a905d 100644 --- a/packages/backend/src/repository/sql/portfolio/update.sql +++ b/packages/backend/src/repository/sql/portfolio/update.sql @@ -1,6 +1,6 @@ UPDATE portfolios SET name = $name, description = $description, - modified = CURRENT_TIMESTAMP + modified = datetime('subsec') WHERE id = $portfolioId AND user_id = $userId; \ No newline at end of file diff --git a/packages/backend/src/repository/sql/prefs/get.sql b/packages/backend/src/repository/sql/prefs/get.sql index 1f1618b0..5a9a39b9 100644 --- a/packages/backend/src/repository/sql/prefs/get.sql +++ b/packages/backend/src/repository/sql/prefs/get.sql @@ -1,4 +1,6 @@ -select id, base_ccy +select id, + base_ccy, + additional from prefs p where p.user_id = $userId limit 1; \ No newline at end of file diff --git a/packages/backend/src/repository/sql/prefs/update.sql b/packages/backend/src/repository/sql/prefs/update.sql index 4615ce2c..decc04a8 100644 --- a/packages/backend/src/repository/sql/prefs/update.sql +++ b/packages/backend/src/repository/sql/prefs/update.sql @@ -1,3 +1,6 @@ update prefs -set base_ccy = $base_ccy +set + base_ccy = $base_ccy, + additional = $additional, + modified = datetime('subsec') where user_id = $userId; \ No newline at end of file diff --git a/packages/backend/src/repository/sql/tx/update.sql b/packages/backend/src/repository/sql/tx/update.sql index cdb3b917..8af8f597 100644 --- a/packages/backend/src/repository/sql/tx/update.sql +++ b/packages/backend/src/repository/sql/tx/update.sql @@ -4,6 +4,6 @@ SET type = $type, price = $price, comments = $comments, date = $date, - modified = CURRENT_TIMESTAMP + modified = datetime('subsec') WHERE id = $txId and asset_id = $assetId; \ No newline at end of file diff --git a/packages/backend/src/repository/sql/user/update-profile-only.sql b/packages/backend/src/repository/sql/user/update-profile-only.sql index 8ceabc7c..321bea43 100644 --- a/packages/backend/src/repository/sql/user/update-profile-only.sql +++ b/packages/backend/src/repository/sql/user/update-profile-only.sql @@ -3,5 +3,5 @@ set username = $username, admin = $admin, login_attempts = $login_attempts, locked = $locked, - modified = CURRENT_TIMESTAMP + modified = datetime('subsec') where id = $userId; \ No newline at end of file diff --git a/packages/backend/src/repository/sql/user/update.sql b/packages/backend/src/repository/sql/user/update.sql index 8533cc60..11553568 100644 --- a/packages/backend/src/repository/sql/user/update.sql +++ b/packages/backend/src/repository/sql/user/update.sql @@ -5,5 +5,5 @@ set username = $username, admin = $admin, login_attempts = $login_attempts, locked = $locked, - modified = CURRENT_TIMESTAMP + modified = datetime('subsec') where id = $userId; \ No newline at end of file diff --git a/packages/backend/src/services/asset.ts b/packages/backend/src/services/asset.ts index 6ccf2fa4..e49315b1 100644 --- a/packages/backend/src/services/asset.ts +++ b/packages/backend/src/services/asset.ts @@ -12,11 +12,7 @@ import { liftTE } from "@darkruby/assets-core/src/decoders/util"; import { pipe } from "fp-ts/function"; import * as TE from "fp-ts/TaskEither"; import { mapWebError } from "../domain/error"; -import { - getAssetEnricher, - getAssetsEnricher, - getOptionalAssetEnricher -} from "../enrichment"; +import { type AssetEnricher } from "../enrichment"; import type { WebAction } from "../fp-express"; import type { Repository } from "../repository"; import type { YahooApi } from "../yahoo/client"; @@ -24,34 +20,32 @@ import type { YahooApi } from "../yahoo/client"; const assetDecoder = liftTE(PostAssetDecoder); export const getAsset = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrichMaybe }: AssetEnricher) => ( assetId: AssetId, portfolioId: PortfolioId, userId: UserId, range: ChartRange ): WebAction> => { - const enrichAsset = getOptionalAssetEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("asset", () => repo.asset.get(assetId, portfolioId, userId)), - TE.chain(({ asset }) => enrichAsset(asset, range)), + TE.chain(({ asset }) => enrichMaybe(asset, range)), mapWebError ); }; export const getAssets = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrichMany }: AssetEnricher) => ( userId: UserId, portfolioId: PortfolioId, range: ChartRange ): WebAction => { - const enrichAssets = getAssetsEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("assets", () => repo.asset.getAll(portfolioId, userId)), - TE.chain(({ assets }) => enrichAssets(assets, range)), + TE.chain(({ assets }) => enrichMany(assets, range)), mapWebError ); }; @@ -71,13 +65,12 @@ export const deleteAsset = }; export const createAsset = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, yahooApi: YahooApi, { enrich }: AssetEnricher) => ( portfolioId: PortfolioId, userId: UserId, payload: unknown ): WebAction => { - const enrichAsset = getAssetEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("asset", () => assetDecoder(payload)), @@ -85,20 +78,19 @@ export const createAsset = TE.bind("created", ({ asset }) => repo.asset.create(asset, portfolioId, userId) ), - TE.chain(({ created }) => enrichAsset(created)), + TE.chain(({ created }) => enrich(created)), mapWebError ); }; export const updateAsset = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, yahooApi: YahooApi, { enrich }: AssetEnricher) => ( assetId: AssetId, portfolioId: PortfolioId, userId: UserId, payload: unknown ): WebAction => { - const enrichAsset = getAssetEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("asset", () => assetDecoder(payload)), @@ -106,7 +98,7 @@ export const updateAsset = TE.bind("updated", ({ asset }) => repo.asset.update(assetId, portfolioId, userId, asset) ), - TE.chain(({ updated }) => enrichAsset(updated)), + TE.chain(({ updated }) => enrich(updated)), mapWebError ); }; diff --git a/packages/backend/src/services/cache.ts b/packages/backend/src/services/cache.ts index ccb1bba8..46efd01e 100644 --- a/packages/backend/src/services/cache.ts +++ b/packages/backend/src/services/cache.ts @@ -1,8 +1,8 @@ -import { type Action } from "@darkruby/assets-core"; +import { defined, type Action } from "@darkruby/assets-core"; import * as O from "fp-ts/lib/Option"; import * as TE from "fp-ts/lib/TaskEither"; import { pipe } from "fp-ts/lib/function"; -import { type LRUCache } from "lru-cache"; +import { LRUCache } from "lru-cache"; import { createHash } from "node:crypto"; import { createLogger } from "../fp-express"; @@ -21,7 +21,7 @@ const getter = (key: string): O.Option => { return pipe( O.tryCatch(() => cache.get(toKey(key))), - O.filter((x) => x !== null && x !== undefined) + O.filter(defined /*(x) => x !== null && x !== undefined*/) ); }; @@ -29,8 +29,7 @@ const setter = (cache: Cache) => (key: string, val: T, ttl?: number): O.Option => { return pipe( - O.of(val), - O.chain(() => O.tryCatch(() => cache.set(toKey(key), val, { ttl }))), + O.tryCatch(() => cache.set(toKey(key), val, { ttl })), O.map(() => val) ); }; @@ -41,10 +40,10 @@ const cachedAction = const get = getter(cache); const res = get(key); if (O.isSome(res)) { - // log.debug(`hit for ${key}`); + log.debug(`HIT for ${key.substring(0, 10)}`); return TE.of(res.value as T); } - // log.debug(`miss for ${key}`); + log.debug(`MISS for ${key.substring(0, 10)}`); const set = setter(cache); return pipe( @@ -55,7 +54,8 @@ const cachedAction = export type AppCache = ReturnType; -export const createCache = (cache: Cache) => { +export const createCache = (size: number, ttl: number) => { + const cache = new LRUCache({ max: size, ttl }); return { has: has(cache), getter: getter(cache), @@ -63,3 +63,9 @@ export const createCache = (cache: Cache) => { cachedAction: cachedAction(cache) }; }; + +// const aaa = (cache: AppCache) => { +// return function wrap(f: FunctionN>) { +// return (...a: A) => B; +// } +// }; diff --git a/packages/backend/src/services/index.ts b/packages/backend/src/services/index.ts index 46977e88..e8fc340f 100644 --- a/packages/backend/src/services/index.ts +++ b/packages/backend/src/services/index.ts @@ -1,3 +1,4 @@ +import type { Enricher } from "../enrichment"; import type { Repository } from "../repository"; import type { YahooApi } from "../yahoo/client"; import * as asset from "./asset"; @@ -9,7 +10,11 @@ import * as user from "./user"; export type WebService = ReturnType; -export const createWebService = (repo: Repository, yahooApi: YahooApi) => { +export const createWebService = ( + repo: Repository, + yahooApi: YahooApi, + enricher: Enricher +) => { return { auth: { createToken: auth.createToken, @@ -29,26 +34,26 @@ export const createWebService = (repo: Repository, yahooApi: YahooApi) => { updateOwnPasswordOnly: user.updateOwnPasswordOnly(repo) }, assets: { - get: asset.getAsset(repo, yahooApi), - getMany: asset.getAssets(repo, yahooApi), + get: asset.getAsset(repo, enricher.asset), + getMany: asset.getAssets(repo, enricher.asset), delete: asset.deleteAsset(repo), - create: asset.createAsset(repo, yahooApi), - update: asset.updateAsset(repo, yahooApi), + create: asset.createAsset(repo, yahooApi, enricher.asset), + update: asset.updateAsset(repo, yahooApi, enricher.asset), move: asset.moveAsset(repo) }, portfolio: { - get: portfolio.getPortfolio(repo, yahooApi), - getMany: portfolio.getPortfolios(repo, yahooApi), + get: portfolio.getPortfolio(repo, enricher.portfolio), + getMany: portfolio.getPortfolios(repo, enricher.portfolio), delete: portfolio.deletePortfolio(repo), - create: portfolio.createPortfolio(repo, yahooApi), - update: portfolio.updatePortfolio(repo, yahooApi) + create: portfolio.createPortfolio(repo, enricher.portfolio), + update: portfolio.updatePortfolio(repo, enricher.portfolio) }, tx: { - get: tx.getTx(repo, yahooApi), - getMany: tx.getTxs(repo, yahooApi), + get: tx.getTx(repo, enricher.tx), + getMany: tx.getTxs(repo, enricher.tx), delete: tx.deleteTx(repo), - create: tx.createTx(repo, yahooApi), - update: tx.updateTx(repo, yahooApi), + create: tx.createTx(repo, enricher.tx), + update: tx.updateTx(repo, enricher.tx), uploadAssetTxs: tx.uploadAssetTxs(repo), deleteAllAsset: tx.deleteAllAssetTxs(repo) }, diff --git a/packages/backend/src/services/portfolio.ts b/packages/backend/src/services/portfolio.ts index 495b0e12..7d9c31b6 100644 --- a/packages/backend/src/services/portfolio.ts +++ b/packages/backend/src/services/portfolio.ts @@ -11,79 +11,69 @@ import { liftTE } from "@darkruby/assets-core/src/decoders/util"; import { pipe } from "fp-ts/function"; import * as TE from "fp-ts/TaskEither"; import { mapWebError } from "../domain/error"; -import { - getOptionalPorfolioEnricher, - getPortfolioEnricher, - getPortfoliosEnricher -} from "../enrichment"; +import { type PortfolioEnricher } from "../enrichment"; import type { WebAction } from "../fp-express"; import type { Repository } from "../repository"; -import type { YahooApi } from "../yahoo/client"; -// import { getTxs as enrichedTxsGetter } from "./tx"; const portfolioDecoder = liftTE(PostPortfolioDecoder); export const getPortfolio = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrichMaybe }: PortfolioEnricher) => ( portfolioId: PortfolioId, userId: UserId, range: ChartRange ): WebAction> => { - const enrichPortfolio = getOptionalPorfolioEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("portfolio", () => repo.portfolio.get(portfolioId, userId)), - TE.chain(({ portfolio }) => enrichPortfolio(portfolio, range)), + TE.chain(({ portfolio }) => enrichMaybe(portfolio, range)), mapWebError ); }; export const getPortfolios = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrichMany }: PortfolioEnricher) => ( userId: UserId, range: ChartRange ): WebAction => { - const enrichPortfolios = getPortfoliosEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("portfolios", () => repo.portfolio.getAll(userId)), - TE.chain(({ portfolios }) => enrichPortfolios(portfolios, range)), + TE.chain(({ portfolios }) => enrichMany(portfolios, range)), mapWebError ); }; export const createPortfolio = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrich }: PortfolioEnricher) => (userId: UserId, payload: unknown): WebAction => { - const enrichPortfolio = getPortfolioEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("portfolio", () => portfolioDecoder(payload)), TE.bind("created", ({ portfolio }) => repo.portfolio.create(portfolio, userId) ), - TE.chain(({ created }) => enrichPortfolio(created)), + TE.chain(({ created }) => enrich(created)), mapWebError ); }; export const updatePortfolio = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrich }: PortfolioEnricher) => ( portfolioId: PortfolioId, userId: UserId, payload: unknown ): WebAction => { - const enrichPortfolio = getPortfolioEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("portfolio", () => portfolioDecoder(payload)), TE.bind("updated", ({ portfolio }) => repo.portfolio.update(portfolioId, portfolio, userId) ), - TE.chain(({ updated }) => enrichPortfolio(updated)), + TE.chain(({ updated }) => enrich(updated)), mapWebError ); }; diff --git a/packages/backend/src/services/tx.ts b/packages/backend/src/services/tx.ts index 89da725e..c331245d 100644 --- a/packages/backend/src/services/tx.ts +++ b/packages/backend/src/services/tx.ts @@ -13,66 +13,61 @@ import { liftTE } from "@darkruby/assets-core/src/decoders/util"; import { pipe } from "fp-ts/lib/function"; import * as TE from "fp-ts/TaskEither"; import { mapWebError } from "../domain/error"; -import { getTxEnricher, getTxsEnricher } from "../enrichment"; +import type { TxEnricher } from "../enrichment"; import type { WebAction } from "../fp-express"; import type { Repository } from "../repository"; -import type { YahooApi } from "../yahoo/client"; const txDecoder = liftTE(PostTxDecoder); const txUploadDecoder = liftTE(PostTxsUploadDecoder); export const getTx = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrich }: TxEnricher) => ( txId: TxId, assetId: AssetId, _portfolioId: PortfolioId, userId: UserId ): WebAction> => { - const enrichTx = getTxEnricher(yahooApi); return pipe( repo.tx.get(txId, assetId, userId), - TE.chain((tx) => (tx ? enrichTx(tx) : TE.of(null))), + TE.chain((tx) => (tx ? enrich(tx) : TE.of(null))), mapWebError ); }; export const getTxs = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrichMany }: TxEnricher) => ( assetId: AssetId, _portfolioId: PortfolioId, userId: UserId, finalStretch: boolean = false ): WebAction => { - const enrichTxs = getTxsEnricher(yahooApi); - return pipe( repo.tx.getAll(assetId, userId, finalStretch), - TE.chain(enrichTxs), + TE.chain(enrichMany), mapWebError ); }; export const createTx = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrich }: TxEnricher) => ( assetId: AssetId, _portfolioId: PortfolioId, userId: UserId, payload: unknown ): WebAction => { - const enrichTx = getTxEnricher(yahooApi); return pipe( txDecoder(payload), TE.chain((tx) => repo.tx.create(tx, assetId, userId)), - TE.chain(enrichTx), + TE.chain(enrich), mapWebError ); }; export const updateTx = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrich }: TxEnricher) => ( txId: TxId, assetId: AssetId, @@ -80,11 +75,10 @@ export const updateTx = userId: UserId, payload: unknown ): WebAction => { - const enrichTx = getTxEnricher(yahooApi); return pipe( txDecoder(payload), TE.chain((tx) => repo.tx.update(txId, tx, assetId, userId)), - TE.chain(enrichTx), + TE.chain(enrich), mapWebError ); }; diff --git a/packages/backend/src/yahoo/cached.ts b/packages/backend/src/yahoo/cached.ts index f648750e..0fa1df19 100644 --- a/packages/backend/src/yahoo/cached.ts +++ b/packages/backend/src/yahoo/cached.ts @@ -5,56 +5,57 @@ import { } from "@darkruby/assets-core/src/decoders/yahoo/meta"; import { createLogger } from "../fp-express"; +import ms from "ms"; import type { AppCache } from "../services/cache"; import { yahooApi as rawYahooApi, type YahooApi } from "./client"; const logger = createLogger("cached yahoo"); export const cachedYahooApi = (cache: AppCache): YahooApi => { - const CHART_TTL = 1000 * 60 * 10; // 1 minutes - const SEARCH_TTL = 1000 * 60 * 10; // 10 minutes - const LOOKUP_TTL = 1000 * 60 * 60; // 1 hr + const MIN_1 = ms("1min"); + const MIN_10 = ms("10min"); + const HOUR_1 = ms("1hr"); const search = (term: string) => cache.cachedAction( `yahoo-search-${term}`, () => rawYahooApi.search(term), - SEARCH_TTL + MIN_10 ); const chart = (symbol: string, range?: ChartRange) => cache.cachedAction( `yahoo-chart-${symbol}-${range ?? DEFAULT_CHART_RANGE}`, () => rawYahooApi.chart(symbol, range), - CHART_TTL + MIN_1 ); const meta = (symbol: string) => cache.cachedAction( `yahoo-meta-${symbol}`, () => rawYahooApi.meta(symbol), - LOOKUP_TTL + HOUR_1 ); const fxRate = (ccy: string, base: Ccy, date?: Optional) => cache.cachedAction( `yahoo-ccy-lookup-${ccy}-${base}-${date?.getTime() ?? "latest"}`, () => rawYahooApi.fxRate(ccy, base, date), - LOOKUP_TTL + HOUR_1 ); const checkTickerExists = (symbol: string) => cache.cachedAction( `yahoo-check-ticker-${symbol}`, () => rawYahooApi.checkTickerExists(symbol), - LOOKUP_TTL + HOUR_1 ); const fxRates = (ccy: string, base: Ccy) => cache.cachedAction( `yahoo-fx-rates-${ccy}-${base}`, () => rawYahooApi.fxRates(ccy, base), - LOOKUP_TTL + HOUR_1 ); return { diff --git a/packages/backend/test/chart.spec.ts b/packages/backend/test/chart.spec.ts index 0fa38a5d..501ddb06 100644 --- a/packages/backend/test/chart.spec.ts +++ b/packages/backend/test/chart.spec.ts @@ -4,7 +4,7 @@ import type { UnixDate } from "@darkruby/assets-core"; import { expect, test } from "bun:test"; -import { combineAssetsMultiChart } from "../src/enrichment/chart"; +import { portfolioMultiChart } from "../src/enrichment/chart"; const createMockChartPoint = ( timestamp: number, @@ -24,14 +24,14 @@ const createMockAsset = (name: string, ts: number[]): EnrichedAsset => { } as unknown as EnrichedAsset; }; -test.failing("combineMultiChart with asset containers", () => { +test.failing("portfolioMultiChart with asset containers", () => { const assets = [ createMockAsset("aapl", [1, 3, 5]), createMockAsset("googl", [2, 4, 6]), createMockAsset("msft", [1, 4, 5]) ] as EnrichedAsset[]; - const { aapl, googl, msft } = combineAssetsMultiChart(assets); + const { aapl, googl, msft } = portfolioMultiChart(assets); expect(aapl.length).toBe(googl.length); expect(googl.length).toBe(msft.length); diff --git a/packages/backend/test/helper.ts b/packages/backend/test/helper.ts index bd381f1e..677eb80e 100644 --- a/packages/backend/test/helper.ts +++ b/packages/backend/test/helper.ts @@ -21,7 +21,8 @@ import * as TE from "fp-ts/lib/TaskEither"; const BASE_URL = `http://${process.env.URL ?? "localhost:4020"}`; export const fakePrefs = (): Prefs => ({ - base_ccy: faker.random.arrayElement(BASE_CCYS) + base_ccy: faker.random.arrayElement(BASE_CCYS), + additional: { altChart: false } }); export const fakeNewUser = (admin = false): NewUser => ({ diff --git a/packages/backend/test/portfolio.spec.ts b/packages/backend/test/portfolio.spec.ts index b7ee3afb..2b38b3c8 100644 --- a/packages/backend/test/portfolio.spec.ts +++ b/packages/backend/test/portfolio.spec.ts @@ -33,6 +33,9 @@ test("Create portfolio", async () => { }); test("Get multiple portfolios", async () => { + for (let _ in [1, 2, 3]) { + await run(api.portfolio.create(fakePortfolio())); + } const portfolios = await run(api.portfolio.getMany()); expect(portfolios).toSatisfy((a) => Array.isArray(a) && a.length > 0); }); diff --git a/packages/core/package.json b/packages/core/package.json index de0bfad2..e7856f55 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@darkruby/assets-core", - "version": "1.8.0", + "version": "1.8.1", "main": "src/index.ts", "type": "module", "scripts": { diff --git a/packages/core/src/decoders/portfolio.ts b/packages/core/src/decoders/portfolio.ts index b2c89462..d5120d72 100644 --- a/packages/core/src/decoders/portfolio.ts +++ b/packages/core/src/decoders/portfolio.ts @@ -2,7 +2,7 @@ import * as t from "io-ts"; import { dateDecoder } from "./date"; import { UserIdDecoder } from "./user"; import { nullableDecoder } from "./util"; -import { ChartDataDecoder } from "./yahoo/chart"; +import { ChartDataDecoder, MultiChartDataDecoder } from "./yahoo/chart"; import { RangeDecoder } from "./yahoo/meta"; import { PeriodChangesDecoder, TotalsDecoder } from "./yahoo/period"; @@ -42,7 +42,7 @@ export const EnrichedPortfolioDecoder = t.type({ weight: nullableDecoder(t.number), domestic: t.boolean, chart: ChartDataDecoder, - // multiChart: MultiChartDataDecoder, + multiChart: MultiChartDataDecoder, changes: PeriodChangesDecoder, totals: TotalsDecoder, invested: t.number, diff --git a/packages/core/src/decoders/prefs.ts b/packages/core/src/decoders/prefs.ts index 2f3bd60b..a15556a3 100644 --- a/packages/core/src/decoders/prefs.ts +++ b/packages/core/src/decoders/prefs.ts @@ -1,6 +1,7 @@ import * as A from "fp-ts/lib/Array"; import { pipe } from "fp-ts/lib/function"; import * as t from "io-ts"; +import { JsonFromString, withFallback } from "io-ts-types"; export const BASE_CCYS = [ "USD", @@ -14,6 +15,7 @@ export const BASE_CCYS = [ "DKK", "NZD", "JPY", + "INR" ] as const; export type Ccy = (typeof BASE_CCYS)[number] | "GBp"; @@ -26,16 +28,34 @@ export const CcyDecoder = pipe( codecs as [ t.LiteralC, t.LiteralC, - ...t.LiteralC[], + ...t.LiteralC[] ] ) ) as t.Type; +const additionqlPrefsTypes = { + altChart: withFallback(t.boolean, false) +}; + +export const AdditionalPrefsDecoder = t.type(additionqlPrefsTypes); +export const defaultAdditionalPrefs = (): t.TypeOf< + typeof AdditionalPrefsDecoder +> => ({ + altChart: false +}); + const prefsTypes = { base_ccy: CcyDecoder, + additional: withFallback(AdditionalPrefsDecoder, defaultAdditionalPrefs()) +}; + +const dbPrefsTypes = { + ...prefsTypes, + additional: t.string.pipe(JsonFromString).pipe(prefsTypes.additional) }; export const PrefsDecoder = t.type(prefsTypes); +export const DbPrefsDecoder = t.type(dbPrefsTypes); export const ccyToLocale = (ccy: Ccy): string => { switch (ccy) { @@ -59,6 +79,8 @@ export const ccyToLocale = (ccy: Ccy): string => { return "en-NZ"; case "JPY": return "ja-JP"; + case "INR": + return "en-IN"; case "USD": default: return "en-US"; diff --git a/packages/core/src/decoders/summary.ts b/packages/core/src/decoders/summary.ts index 67a01a22..8aaa7580 100644 --- a/packages/core/src/decoders/summary.ts +++ b/packages/core/src/decoders/summary.ts @@ -1,12 +1,12 @@ import * as t from "io-ts"; import { PortfolioMetaDecoder } from "./portfolio"; -import { ChartDataDecoder } from "./yahoo/chart"; +import { ChartDataDecoder, MultiChartDataDecoder } from "./yahoo/chart"; import { PeriodChangesDecoder, TotalsDecoder } from "./yahoo/period"; const summaryTypes = { numPortfolios: t.number, chart: ChartDataDecoder, - // multiChart: MultiChartDataDecoder, + multiChart: MultiChartDataDecoder, changes: PeriodChangesDecoder, totals: TotalsDecoder, meta: PortfolioMetaDecoder, diff --git a/packages/core/src/decoders/user.ts b/packages/core/src/decoders/user.ts index d3864a06..da862b00 100644 --- a/packages/core/src/decoders/user.ts +++ b/packages/core/src/decoders/user.ts @@ -1,9 +1,9 @@ import { pipe } from "fp-ts/lib/function"; import type { Refinement } from "fp-ts/lib/Refinement"; import * as t from "io-ts"; +import { nonEmptyField } from "../validation/util"; import { BooleanDecoder } from "./boolean"; import { dateDecoder } from "./date"; -import { nonEmptyString } from "./string"; export const UserIdDecoder = t.brand( t.number, @@ -15,14 +15,14 @@ export const UserIdDecoder = t.brand( ); const credentialsTypes = { - username: nonEmptyString, - password: nonEmptyString + username: nonEmptyField("username"), + password: nonEmptyField("password") }; const passwordChangeTypes = { - oldPassword: nonEmptyString, - newPassword: nonEmptyString, - repeat: nonEmptyString + oldPassword: nonEmptyField("old password"), + newPassword: nonEmptyField("new password"), + repeat: nonEmptyField("repeat password") }; const newUserTypes = { diff --git a/packages/core/src/domain/prefs.ts b/packages/core/src/domain/prefs.ts index 67b8db5b..5c907c41 100644 --- a/packages/core/src/domain/prefs.ts +++ b/packages/core/src/domain/prefs.ts @@ -1,8 +1,18 @@ import * as t from "io-ts"; -import { BASE_CCYS, type PrefsDecoder } from "../decoders"; +import { + AdditionalPrefsDecoder, + BASE_CCYS, + type PrefsDecoder +} from "../decoders"; export type Prefs = t.TypeOf; +export type AdditionalPrefs = t.TypeOf; + +export const defaultAdditional = (): AdditionalPrefs => ({ + altChart: false +}); export const defaultPrefs = (): Prefs => ({ base_ccy: BASE_CCYS[0], + additional: defaultAdditional() }); diff --git a/packages/core/src/domain/tx.ts b/packages/core/src/domain/tx.ts index 82d475d7..423154e5 100644 --- a/packages/core/src/domain/tx.ts +++ b/packages/core/src/domain/tx.ts @@ -40,6 +40,10 @@ export const byDateAsc = pipe( export const isBuy = ({ type }: T) => type == "buy"; export const isSell = (tx: T) => !isBuy(tx); +export const toKey = (tx: T) => + `tx-${tx.id}-${tx.modified.getTime()}`; +export const toKeys = (tx: T[]) => tx.map(toKey).join(`-`); + export const cloneTx = ({ type, quantity, diff --git a/packages/core/src/utils/utils.ts b/packages/core/src/utils/utils.ts index b13d6d62..8b821a70 100644 --- a/packages/core/src/utils/utils.ts +++ b/packages/core/src/utils/utils.ts @@ -9,7 +9,7 @@ export type Replace = Identity< Omit & { [key in K]: R } >; -export const defined = (t: Optional): t is T => +export const defined = (t: Optional): t is NonNullable => t !== null && t !== undefined; export type Result = E.Either; diff --git a/packages/core/src/validation/util.ts b/packages/core/src/validation/util.ts index 5c550a05..625cbaae 100644 --- a/packages/core/src/validation/util.ts +++ b/packages/core/src/validation/util.ts @@ -1,7 +1,7 @@ import * as E from "fp-ts/lib/Either"; import { pipe } from "fp-ts/lib/function"; import * as t from "io-ts"; -import { nonEmptyString } from "../decoders"; +import { nonEmptyString } from "../decoders/string"; import { validationErr, withErrorMessage } from "../decoders/util"; export type Validator = ReturnType; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 0f02816f..a979c738 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { + "types": [ + "bun-types" + ], // Enable latest features "lib": [ "ESNext", diff --git a/packages/web/package.json b/packages/web/package.json index 5a87f71d..05d755d7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@darkruby/assets-web", "private": true, - "version": "1.8.0", + "version": "1.8.1", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", @@ -29,7 +29,7 @@ "react-dropzone": "^14.3.8", "react-router": "^7.3.0", "react-select": "^5.10.1", - "recharts": "^3.8.0" + "recharts": "3.8.0" }, "devDependencies": { "sass-embedded": "^1.93.2", diff --git a/packages/web/src/components/Charts/AutoChart.tsx b/packages/web/src/components/Charts/AutoChart.tsx new file mode 100644 index 00000000..12903d9a --- /dev/null +++ b/packages/web/src/components/Charts/AutoChart.tsx @@ -0,0 +1,46 @@ +import type { Identity } from "@darkruby/assets-core"; +import { usePrefs } from "../../hooks/prefs"; +import type { PropsOf } from "../../util/props"; +import { AssetChart } from "./AssetChart"; +import { MultiAssetChart } from "./MultiAssetChart"; + +type AssetChartProps = PropsOf; +type MultiAssetChartProps = PropsOf; + +type AutoChartProps = Identity< + Omit | Omit +> & { + chart: AssetChartProps["data"]; + multiChart: MultiAssetChartProps["data"]; +}; + +export const AutoChart: React.FC = ({ + chart, + multiChart, + onChange, + range, + ranges, + hidden +}: AutoChartProps) => { + const { additional } = usePrefs(); + if (additional.altChart) { + return ( +