From c1629638609106cbc27281c1987a3d28a4fe672b Mon Sep 17 00:00:00 2001 From: sofiesty Date: Thu, 15 Oct 2026 09:04:08 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Dokumenter=20installasjon=20og=20kj=C3=B8ri?= =?UTF-8?q?ng=20av=20database,=20backend=20og=20frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 189 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 167 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 757c264..377c928 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ Målet er å lage en oversiktlig og visuelt interessant vinkatalog hvor brukeren både kan finne bestemte viner og oppdage nye gjennom søk, filtrering og mer utforskende funksjoner basert på smak, mat og lignende viner. +Hvordan prosjektet installeres og kjøres, lokalt og på VM-en, står i [Installasjon og kjøring](#installasjon-og-kjøring). + + --- @@ -457,37 +460,179 @@ Dette kan blant annet innebære: - unngå unødvendig tunge biblioteker - gjenbruke komponenter -# Lagt ttil av Sofie -## Database og backend på VM +## Installasjon og kjøring + +Denne delen beskriver hvordan database, backend og frontend installeres og startes, både lokalt og på gruppas VM. + +```text +Nettleser ──► Apache (port 80) ──► /var/www/html/project2 bygget frontend (statiske filer) + │ + └──────► Backend (Node.js, port 3001) ──► PostgreSQL (databasen winenot) +``` + +| Del | Teknologi | Mappe | +|---|---|---| +| Database | PostgreSQL | `backend/db/migrations/` (tabeller og indekser), `backend/db/seeds/` (matkategorier) | +| Dataimport | Node.js-skript som leser X-Wines-filene (CSV) | `backend/scripts/import.ts` | +| Backend | Node.js, TypeScript, Apollo Server (GraphQL) | `backend/` | +| Frontend | React, TypeScript, Vite, Apollo Client, React Router | `client/` | +| Drift på VM | Bash-skript, systemd og Apache | `deploy/` | + +### Kjøre lokalt + +Du trenger Git, Node.js 22 eller nyere og PostgreSQL (vi bruker versjon 18). Kommandoene er for macOS og Linux; på Windows kan de kjøres i Git Bash. + +**1. Hent koden og installer pakker** + +```bash +git clone git@git.ntnu.no:IT2810-H26/T01-Project-2.git +cd T01-Project-2 +(cd backend && npm ci) +(cd client && npm ci) +``` + +**2. Opprett databasen og tabellene** + +```bash +createdb winenot +cd backend +for f in db/migrations/*.sql; do psql -d winenot -v ON_ERROR_STOP=1 -f "$f" || break; done +``` + +Migrasjonene kjøres i nummerert rekkefølge. En migrasjon som er merget, endres ikke; nye endringer legges i en ny, nummerert fil. Kommandoene bruker din egen PostgreSQL-bruker, som er standard med Homebrew på Mac. Bruker du en annen databasebruker, legg til `-U `. + +**3. Sett opp tilkoblingen til databasen** (fortsatt i `backend/`) + +```bash +cp .env.example .env +``` + +Åpne `backend/.env` og fyll inn `DB_USER`, og `DB_PASSWORD` hvis databasen krever passord. `.env` ligger i `.gitignore` og skal aldri committes. Backend kjører på port 3001 med mindre `PORT` er satt. + +**4. Last ned og importer data** (fortsatt i `backend/`) + +X-Wines-filene ligger ikke i Git, fordi `backend/data/` er i `.gitignore`. Testversjonen med 100 viner holder til utvikling: + +```bash +mkdir -p data +curl -L -o data/XWines_Test_100_wines.csv https://raw.githubusercontent.com/rogerioxavier/X-Wines/main/Dataset/last/XWines_Test_100_wines.csv +curl -L -o data/XWines_Test_1K_ratings.csv https://raw.githubusercontent.com/rogerioxavier/X-Wines/main/Dataset/last/XWines_Test_1K_ratings.csv +npm run import +``` + +Importskriptet fyller tabellene og setter matkategorier fra `db/seeds/food_categories.sql`. Det kan kjøres flere ganger, og favoritter beholdes. For hele datasettet (100 646 viner) laster du ned `XWines_Full_100K_wines.csv` og `XWines_Full_21M_ratings.csv` fra Google Drive-lenken i [X-Wines-repoet](https://github.com/rogerioxavier/X-Wines), legger dem i `backend/data/` og kjører: + +```bash +npm run import -- data/XWines_Full_100K_wines.csv data/XWines_Full_21M_ratings.csv +``` + +**5. Start backend** (fortsatt i `backend/`) -PostgreSQL 18 er installert direkte på VM-en (it2810-01.idi.ntnu.no) med apt, uten Docker. Databasen lytter kun på localhost og er ikke tilgjengelig utenfra. +```bash +npm run dev +``` + +Backend kjører nå på http://localhost:3001/. Åpner du adressen i nettleseren, får du Apollo Sandbox, der du kan prøve spørringer, for eksempel `{ wines(limit: 5) { totalCount items { name } } }`. + +**6. Start frontend** i en ny terminal, fra rotmappen i repoet: + +```bash +cd client +npm run dev +``` + +Åpne http://localhost:5173/project2/. Frontend henter data fra http://localhost:3001/. En annen adresse kan settes med `VITE_GRAPHQL_URL` (se `client/.env.example`). + +### Kjøring på VM-en + +Applikasjonen kjører på http://it2810-01.idi.ntnu.no/project2, og GraphQL-backend på http://it2810-01.idi.ntnu.no:3001/. VM-en kan bare nås fra NTNU-nettet eller via VPN. Hvilken commit som kjører, står i http://it2810-01.idi.ntnu.no/project2/version.txt. + +Alt kjører direkte på VM-en uten Docker, og starter av seg selv etter en omstart. + +| Hva | Hvor på VM-en | +|---|---| +| Koden (siste deploy av `main`) | `/opt/wine-not` | +| Bygget frontend, servert av Apache | `/var/www/html/project2` | +| Backend | systemd-tjenesten `wine-not-api`, som kjører `node dist/index.js` som brukeren `winenot` (oppsett i `deploy/wine-not-api.service`) | +| Tilkoblingsinfo og passord til databasen | `/etc/wine-not/backend.env`, kun lesbar med sudo (mal i `deploy/backend.env.example`) | +| Database | PostgreSQL 18, databasen `winenot` eid av brukeren `winenot_app`, lytter kun på localhost | +| X-Wines-data (full versjon) | `/opt/wine-not/backend/data` | +| Apache-oppsett for `/project2` | `/etc/apache2/conf-available/project2.conf` (fra `deploy/apache-project2.conf`) | + +**Status, omstart og logger for backend** (på VM-en): + +```bash +sudo systemctl status wine-not-api +sudo systemctl restart wine-not-api +sudo journalctl -u wine-not-api -n 50 +``` + +**Deploye en ny versjon.** VM-en har ikke tilgang til Git-serveren, så deploy gjøres fra egen maskin etter at endringene er merget til `main`. Kjør fra repoet, på NTNU-nettet eller VPN: -- Kode: `/srv/winenot` (klonet fra dette repoet, branch `dev`) -- Database: `winenot`, eid av brukeren `winenot_app` -- Tilkoblingsinfo og passord: `/srv/winenot/backend/.env` (kun lesbar med sudo). Se `backend/.env.example` for hvilke variabler som trengs. -- Tabeller opprettes med migrasjonene i `backend/db/migrations/`, kjørt i nummerert rekkefølge: - `psql -h localhost -U winenot_app -d winenot -v ON_ERROR_STOP=1 -f backend/db/migrations/.sql` +```bash +bash deploy/push-to-vm.sh +``` + +Skriptet pakker det som ligger på `main` (bare committede filer), kopierer det til VM-en og kjører `deploy/deploy.sh` der. Det skriptet: + +1. kopierer koden til `/opt/wine-not`, +2. installerer pakker og bygger backend (`tsc`) og frontend (`vite build`), +3. installerer systemd-tjenesten og Apache-oppsettet, +4. starter backend på nytt og kopierer frontend til `/var/www/html/project2`. + +Begge delene bygges før noe byttes ut, så hvis et bygg feiler, fortsetter den gamle versjonen å kjøre. Du trenger sudo på VM-en. For å bygge på nytt uten ny kode kjører du `sudo bash /opt/wine-not/deploy/deploy.sh` på VM-en. -### Starte backend +Frontend bygges som minifiserte, statiske filer med hash i filnavnet, som nettleseren kan cache i ett år (se `deploy/apache-project2.conf`). En deploy sender bare kildekoden, ikke `node_modules` eller data. + +**Migrasjoner og import på VM-en.** Migrasjoner kjøres aldri automatisk. Etter at en ny migrasjon er deployet: ```bash -cd /srv/winenot/backend -npm ci -npm start +cd /opt/wine-not/backend +sudo bash ../deploy/with-env.sh psql -v ON_ERROR_STOP=1 -f db/migrations/.sql +sudo systemctl restart wine-not-api ``` -Backend kjører på port 3001. +`deploy/with-env.sh` kjører kommandoen med innstillingene fra `/etc/wine-not/backend.env`. Importen kjøres på samme måte: + +```bash +sudo bash ../deploy/with-env.sh npm run import -- data/XWines_Full_100K_wines.csv data/XWines_Full_21M_ratings.csv +``` -### Importere data (X-Wines) +**Sette opp en ny VM.** Dette er allerede gjort på vår VM, men slik settes alt opp fra bunnen av på Ubuntu: -1. Last ned `XWines_Full_100K_wines.csv` og `XWines_Full_21M_ratings.csv` fra Google Drive-lenken i Dataset-mappen på https://github.com/rogerioxavier/X-Wines -2. Kopier filene til `/srv/winenot/backend/data/` (mappen ligger i `.gitignore`) -3. Kjør migrasjonene i `backend/db/migrations/` som ikke er kjørt, og deretter: ```bash - cd /srv/winenot/backend - npm ci - npm run import -- data/XWines_Full_100K_wines.csv data/XWines_Full_21M_ratings.csv +sudo apt update +sudo apt install -y rsync curl apache2 postgresql +curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - +sudo apt install -y nodejs +sudo useradd --system --no-create-home --shell /usr/sbin/nologin winenot +sudo -u postgres psql -c "CREATE USER winenot_app WITH PASSWORD '';" +sudo -u postgres psql -c "CREATE DATABASE winenot OWNER winenot_app;" +sudo mkdir -p /etc/wine-not +sudo nano /etc/wine-not/backend.env +sudo chmod 600 /etc/wine-not/backend.env ``` -Skriptet kan kjøres flere ganger, og favoritter beholdes. Uten argumenter bruker det testfilene (100 viner), som er nok til lokal utvikling. -Datasett: de Azambuja, R. X., Morais, A. J., & Filipe, V. (2023). X-Wines: A Wine Dataset for Recommender Systems and Machine Learning. *Big Data and Cognitive Computing*, 7(1), 20. https://doi.org/10.3390/bdcc7010020 \ No newline at end of file +`/etc/wine-not/backend.env` får samme innhold som `deploy/backend.env.example`, med det ekte passordet. Er brannmuren på (`sudo ufw status`), åpnes portene med `sudo ufw allow 80/tcp` og `sudo ufw allow 3001/tcp`. + +Deretter deployes koden med `deploy/push-to-vm.sh` fra egen maskin. Til slutt legges CSV-filene i `/opt/wine-not/backend/data` (for eksempel med `scp`), og migrasjonene og importen kjøres på VM-en: + +```bash +cd /opt/wine-not/backend +for f in db/migrations/*.sql; do sudo bash ../deploy/with-env.sh psql -v ON_ERROR_STOP=1 -f "$f" || break; done +sudo bash ../deploy/with-env.sh npm run import -- data/XWines_Full_100K_wines.csv data/XWines_Full_21M_ratings.csv +``` + +### Feilsøking + +| Problem | Løsning | +|---|---| +| Siden laster, men ingen viner vises | Sjekk at backend kjører (`sudo systemctl status wine-not-api`), og at port 3001 er åpen i brannmuren. | +| 404 når en side lastes på nytt | Apache-oppsettet er ikke aktivt: `sudo a2enconf project2 && sudo systemctl restart apache2`. | +| `Could not resolve hostname` ved deploy | Du er ikke på NTNU-nettet eller VPN. | +| `password authentication failed` | Passordet i `/etc/wine-not/backend.env` stemmer ikke med databasebrukeren. | +| Bygget feiler på VM-en, men fungerer lokalt | Noe er ikke committet eller merget til `main`. VM-en får bare det som ligger på `main`. | + +## Datasett + +de Azambuja, R. X., Morais, A. J., & Filipe, V. (2023). X-Wines: A Wine Dataset for Recommender Systems and Machine Learning. *Big Data and Cognitive Computing*, 7(1), 20. https://doi.org/10.3390/bdcc7010020 From 91a7c575c23481355606c203995af35435549152 Mon Sep 17 00:00:00 2001 From: sofiesty Date: Thu, 15 Oct 2026 12:54:49 +0200 Subject: [PATCH 2/3] =?UTF-8?q?Oppdater=20status=20i=20README,=20sidetitte?= =?UTF-8?q?l=20og=20spr=C3=A5k?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 +++++++++++++++----- client/index.html | 4 ++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 377c928..43e1a49 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,20 @@ Hvordan prosjektet installeres og kjøres, lokalt og på VM-en, står i [Install --- -## Status: +## Status (andre underveisinnlevering) -Denne innleveringen består av en spesifikasjon av den planlagte løsningen. Vi beskriver hvilke data applikasjonen skal bruke, hvordan brukergrensesnitt og funksjonaliteten er tenkt utformet og hvordan vi planlegger database, backend og GraphQL-løsning. +Applikasjonen kjører på gruppas VM: http://it2810-01.idi.ntnu.no/project2 (krever NTNU-nett eller VPN). + + +Dette fungerer nå: +- **Database:** PostgreSQL på VM-en med hele X-Wines-datasettet (100 646 viner), lastet inn med vårt eget importskript. +- **Backend:** GraphQL-backend i Node.js, TypeScript og Apollo Server på port 3001. Den kjører som en systemd-tjeneste og starter av seg selv etter omstart. +- **Klient:** en vinkatalog i React som henter viner fra backend med Apollo Client og viser 20 om gangen. Brukeren kan bla mellom sider med «Forrige» og «Neste». Hver side hentes fra backend med `limit` og `offset`, så klienten laster aldri hele datasettet. +- Backend har også spørringer for detaljer om én vin (`wine`) og for matkategorier (`foodCategories`, `winesByFoodCategory`), som klienten ikke bruker ennå. + + +Neste steg er søk, sortering og filtrering, detaljvisning av hver vin og resten av funksjonaliteten som er beskrevet under. Resten av README beskriver planen for hele applikasjonen. ## Idé @@ -344,7 +354,7 @@ React-klient ## Foreløpig GraphQL-design -GraphQL-backend er ikke implementert ennå, men vi planlegger at frontend skal hente vinene gjennom vår egen backend. +Frontend henter vinene gjennom vår egen GraphQL-backend. Skjemaet under er planen for hele applikasjonen; foreløpig er `wines`, `wine`, `foodCategories` og `winesByFoodCategory` implementert (se `backend/src/index.ts`). Søk, filtrering, sortering og paginering skal utføres i backend slik at funksjonaliteten gjelder hele datasettet og ikke bare vinene som allerede er lastet inn i klienten. @@ -398,8 +408,8 @@ type WineMatch { | Frontend | React, TypeScript, Vite | Krav i oppgaven | | Ruting | React Router | Egne URL-er for katalog, detaljsider og undersider | | Visualisering | React-basert visualisering / lett grafbibliotek | Brukes til Explore by taste | -| State management | Apollo Client (planlagt) | Cache for GraphQL-svar og lokal state | -| Backend | Node.js, TypeScript, Apollo Server (planlagt) | GraphQL er krav i oppgaven, og Apollo Server er godt dokumentert | +| State management | Apollo Client | Cache for GraphQL-svar og lokal state | +| Backend | Node.js, TypeScript, Apollo Server | GraphQL er krav i oppgaven, og Apollo Server er godt dokumentert | | Database | PostgreSQL | Relasjonsdatabase som passer godt til viner, brukere og favoritter. Støtter også effektiv søking, filtrering og sortering | | Testing | Vitest | Vitest er krav i oppgaven | | Kodekvalitet | oxlint og Prettier | Linting og felles formatering | diff --git a/client/index.html b/client/index.html index 3269aca..cb93dc2 100644 --- a/client/index.html +++ b/client/index.html @@ -1,10 +1,10 @@ - + - client + Wine not?
From 0b13d7d098eb7aaae331b3ff6a49679d3ece422d Mon Sep 17 00:00:00 2001 From: sofiesty Date: Thu, 15 Oct 2026 13:15:15 +0200 Subject: [PATCH 3/3] Sett opp ESLint og Prettier i hele prosjektet --- .prettierrc.json | 6 + .vscode/extensions.json | 3 + .vscode/settings.json | 11 + README.md | 22 +- backend/.prettierignore | 5 + backend/eslint.config.js | 21 + backend/package-lock.json | 1626 ++++++++++++++++++++----- backend/package.json | 13 +- backend/scripts/import.ts | 166 ++- backend/src/db.ts | 6 +- backend/src/index.ts | 64 +- client/.prettierignore | 3 + client/README.md | 10 +- client/eslint.config.js | 20 +- client/package-lock.json | 34 + client/package.json | 6 +- client/src/apollo/client.ts | 2 +- client/src/components/WineCatalog.tsx | 30 +- client/src/graphql/queries.ts | 37 + client/src/index.css | 7 +- client/vite.config.ts | 6 +- 21 files changed, 1619 insertions(+), 479 deletions(-) create mode 100644 .prettierrc.json create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 backend/.prettierignore create mode 100644 backend/eslint.config.js create mode 100644 client/.prettierignore create mode 100644 client/src/graphql/queries.ts diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..ebd8881 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "all", + "printWidth": 80 +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..d7df89c --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..519b35f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "eslint.workingDirectories": ["./client", "./backend"], + "[markdown]": { + "editor.formatOnSave": false + } +} diff --git a/README.md b/README.md index 43e1a49..cb2221d 100644 --- a/README.md +++ b/README.md @@ -412,7 +412,7 @@ type WineMatch { | Backend | Node.js, TypeScript, Apollo Server | GraphQL er krav i oppgaven, og Apollo Server er godt dokumentert | | Database | PostgreSQL | Relasjonsdatabase som passer godt til viner, brukere og favoritter. Støtter også effektiv søking, filtrering og sortering | | Testing | Vitest | Vitest er krav i oppgaven | -| Kodekvalitet | oxlint og Prettier | Linting og felles formatering | +| Kodekvalitet | ESLint og Prettier | ESLint finner feil og feil bruk av React-hooks, og Prettier gir lik formatering i hele prosjektet. ESLint fulgte med Vite-malen, så vi bruker det samme oppsettet i backend | @@ -553,6 +553,26 @@ npm run dev Åpne http://localhost:5173/project2/. Frontend henter data fra http://localhost:3001/. En annen adresse kan settes med `VITE_GRAPHQL_URL` (se `client/.env.example`). +### Linting og formatering + +Hele prosjektet bruker ESLint og Prettier, med samme oppsett i `client/` og `backend/`: + +- **ESLint** finner feil og risikable mønstre i koden, for eksempel ubrukte variabler og feil bruk av React-hooks. Oppsettet ligger i `eslint.config.js` i hver mappe. +- **Prettier** formaterer koden likt for alle, så kodegjennomgang handler om innholdet og ikke om mellomrom og anførselstegn. Reglene ligger i `.prettierrc.json` i rotmappen og gjelder begge mappene. +- **eslint-config-prettier** slår av ESLint-reglene som ellers ville vært uenige med Prettier. + +Kommandoene kjøres i `client/` eller `backend/`: + +| Kommando | Hva den gjør | +|---|---| +| `npm run lint` | Finner feil med ESLint | +| `npm run format` | Formaterer alle filene med Prettier | +| `npm run format:check` | Sjekker formateringen uten å endre noe | + +Repoet har VS Code-innstillinger i `.vscode/` som formaterer med Prettier hver gang du lagrer, og retter det ESLint kan rette automatisk. VS Code foreslår utvidelsene Prettier og ESLint når du åpner prosjektet. Kjør `npm run lint` og `npm run format:check` i begge mappene før du lager en pull request. + +Backend bruker TypeScript 6, samme versjon som klienten, fordi typescript-eslint ennå ikke støtter TypeScript 7. + ### Kjøring på VM-en Applikasjonen kjører på http://it2810-01.idi.ntnu.no/project2, og GraphQL-backend på http://it2810-01.idi.ntnu.no:3001/. VM-en kan bare nås fra NTNU-nettet eller via VPN. Hvilken commit som kjører, står i http://it2810-01.idi.ntnu.no/project2/version.txt. diff --git a/backend/.prettierignore b/backend/.prettierignore new file mode 100644 index 0000000..0cb1fb6 --- /dev/null +++ b/backend/.prettierignore @@ -0,0 +1,5 @@ +# Build output and the X-Wines data are not our code. +dist +data +# Generated by npm, so it should not be reformatted. +package-lock.json diff --git a/backend/eslint.config.js b/backend/eslint.config.js new file mode 100644 index 0000000..689f5d8 --- /dev/null +++ b/backend/eslint.config.js @@ -0,0 +1,21 @@ +// ESLint finds bugs and risky patterns in the backend code. +// Formatting is left to Prettier: eslint-config-prettier (last in the list) +// turns off the ESLint rules that would disagree with it. +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; +import eslintConfigPrettier from 'eslint-config-prettier/flat'; +import { defineConfig, globalIgnores } from 'eslint/config'; + +export default defineConfig([ + globalIgnores(['dist', 'data']), + { + files: ['**/*.ts'], + extends: [js.configs.recommended, tseslint.configs.recommended], + languageOptions: { + // The backend runs in Node.js, not in a browser + globals: globals.node, + }, + }, + eslintConfigPrettier, +]); diff --git a/backend/package-lock.json b/backend/package-lock.json index 1e0229a..a26751f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,10 +16,16 @@ "pg": "^8.23.1" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/node": "^26.6.4", "@types/pg": "^8.23.1", + "eslint": "^10.12.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.13.0", + "prettier": "^3.9.10", "tsx": "^4.23.15", - "typescript": "^7.0.2" + "typescript": "~6.0.2", + "typescript-eslint": "^8.71.2" } }, "node_modules/@apollo/cache-control-types": { @@ -259,6 +265,30 @@ "node": ">=16" } }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -701,6 +731,134 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@graphql-tools/merge": { "version": "9.2.6", "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.2.6.tgz", @@ -761,6 +919,96 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -824,6 +1072,27 @@ "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "license": "BSD-3-Clause" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -852,356 +1121,286 @@ "pg-types": "^2.2.0" } }, - "node_modules/@typescript/typescript-aix-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", - "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", + "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "aix" - ], + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/type-utils": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.71.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", - "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", "engines": { - "node": ">=16.20.0" + "node": ">= 4" } }, - "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", - "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/parser": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz", + "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3" + }, "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript/typescript-freebsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", - "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz", + "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.2", + "@typescript-eslint/types": "^8.71.2", + "debug": "^4.4.3" + }, "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript/typescript-freebsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", - "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz", + "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2" + }, "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript/typescript-linux-arm": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", - "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", - "cpu": [ - "arm" - ], + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz", + "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", - "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz", + "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript/typescript-linux-loong64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", - "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", - "cpu": [ - "loong64" - ], + "node_modules/@typescript-eslint/types": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz", + "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript/typescript-linux-mips64el": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", - "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", - "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", - "cpu": [ - "ppc64" - ], + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz", + "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-riscv64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", - "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-s390x": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", - "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", - "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", - "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.2", + "@typescript-eslint/tsconfig-utils": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript/typescript-netbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", - "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz", + "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2" + }, "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript/typescript-openbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", - "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz", + "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "eslint-visitor-keys": "^5.0.0" + }, "engines": { - "node": ">=16.20.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript/typescript-openbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", - "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], + "node_modules/@whatwg-node/promise-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-2.0.0.tgz", + "integrity": "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.3" + }, "engines": { - "node": ">=16.20.0" + "node": ">=22.15.0" } }, - "node_modules/@typescript/typescript-sunos-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", - "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", - "cpu": [ - "x64" - ], + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "sunos" - ], + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=16.20.0" + "node": ">=0.4.0" } }, - "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", - "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", - "cpu": [ - "arm64" - ], + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript/typescript-win32-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", - "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", - "cpu": [ - "x64" - ], + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@whatwg-node/promise-helpers": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-2.0.0.tgz", - "integrity": "sha512-/i9CS0Z96o+cPwaYoAyVxAThDiUX1wV3tDmNWPJbn+vtdR+JftuCDg1NGCXBrzIYVDGkJ8uT6ctwmrBOUzOpKQ==", "license": "MIT", "dependencies": { - "tslib": "^2.6.3" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "engines": { - "node": ">=22.15.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/async-retry": { @@ -1228,6 +1427,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -1265,6 +1474,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1274,6 +1496,20 @@ "node": ">= 0.8" } }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -1359,6 +1595,21 @@ "node": ">=16.0.0" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/csv-parse": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-7.0.3.tgz", @@ -1382,6 +1633,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1530,6 +1788,239 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz", + "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -1551,6 +2042,42 @@ "url": "https://opencollective.com/express" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -1614,17 +2141,43 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", + "dev": true, "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/gopd": { @@ -1687,6 +2240,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -1699,6 +2265,13 @@ "node": ">= 0.4" } }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -1735,6 +2308,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1753,6 +2346,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", @@ -1774,6 +2390,67 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -1855,12 +2532,35 @@ "url": "https://opencollective.com/express" } }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", @@ -1923,6 +2623,56 @@ "node": ">= 0.8" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1932,6 +2682,26 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pg": { "version": "8.23.1", "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz", @@ -2021,6 +2791,19 @@ "split2": "^4.1.0" } }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -2069,6 +2852,62 @@ "node": ">=0.10.0" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, "node_modules/qs": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", @@ -2135,6 +2974,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -2178,6 +3030,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -2268,6 +3143,23 @@ "node": ">= 0.8" } }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", @@ -2291,6 +3183,19 @@ "node": ">=0.6" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2316,6 +3221,19 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-is": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", @@ -2362,38 +3280,41 @@ } }, "node_modules/typescript": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", - "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=16.20.0" + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.2.tgz", + "integrity": "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.71.2", + "@typescript-eslint/parser": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2" }, - "optionalDependencies": { - "@typescript/typescript-aix-ppc64": "7.0.2", - "@typescript/typescript-darwin-arm64": "7.0.2", - "@typescript/typescript-darwin-x64": "7.0.2", - "@typescript/typescript-freebsd-arm64": "7.0.2", - "@typescript/typescript-freebsd-x64": "7.0.2", - "@typescript/typescript-linux-arm": "7.0.2", - "@typescript/typescript-linux-arm64": "7.0.2", - "@typescript/typescript-linux-loong64": "7.0.2", - "@typescript/typescript-linux-mips64el": "7.0.2", - "@typescript/typescript-linux-ppc64": "7.0.2", - "@typescript/typescript-linux-riscv64": "7.0.2", - "@typescript/typescript-linux-s390x": "7.0.2", - "@typescript/typescript-linux-x64": "7.0.2", - "@typescript/typescript-netbsd-arm64": "7.0.2", - "@typescript/typescript-netbsd-x64": "7.0.2", - "@typescript/typescript-openbsd-arm64": "7.0.2", - "@typescript/typescript-openbsd-x64": "7.0.2", - "@typescript/typescript-sunos-x64": "7.0.2", - "@typescript/typescript-win32-arm64": "7.0.2", - "@typescript/typescript-win32-x64": "7.0.2" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/undici-types": { @@ -2412,6 +3333,16 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2430,6 +3361,22 @@ "node": ">=18" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-typed-array": { "version": "1.1.24", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", @@ -2451,6 +3398,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -2459,6 +3416,19 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/backend/package.json b/backend/package.json index 1abc594..b728e05 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,10 @@ "dev": "tsx watch src/index.ts", "build": "tsc", "start": "node dist/index.js", - "import": "tsx scripts/import.ts" + "import": "tsx scripts/import.ts", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "keywords": [], "author": "", @@ -21,9 +24,15 @@ "pg": "^8.23.1" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/node": "^26.6.4", "@types/pg": "^8.23.1", + "eslint": "^10.12.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.13.0", + "prettier": "^3.9.10", "tsx": "^4.23.15", - "typescript": "^7.0.2" + "typescript": "~6.0.2", + "typescript-eslint": "^8.71.2" } } diff --git a/backend/scripts/import.ts b/backend/scripts/import.ts index bfacfe3..962814b 100644 --- a/backend/scripts/import.ts +++ b/backend/scripts/import.ts @@ -6,25 +6,25 @@ // // Skriptet kan kjøres flere ganger. Viner oppdateres, og favoritter beholdes. -import { createReadStream, readFileSync } from "node:fs"; -import { parse } from "csv-parse"; -import type { PoolClient } from "pg"; -import { pool } from "../src/db.js"; +import { createReadStream, readFileSync } from 'node:fs'; +import { parse } from 'csv-parse'; +import type { PoolClient } from 'pg'; +import { pool } from '../src/db.js'; -const WINES_FILE = process.argv[2] ?? "data/XWines_Test_100_wines.csv"; -const RATINGS_FILE = process.argv[3] ?? "data/XWines_Test_1K_ratings.csv"; +const WINES_FILE = process.argv[2] ?? 'data/XWines_Test_100_wines.csv'; +const RATINGS_FILE = process.argv[3] ?? 'data/XWines_Test_1K_ratings.csv'; const BATCH_SIZE = 1000; -const FOOD_CATEGORIES_FILE = "db/seeds/food_categories.sql"; +const FOOD_CATEGORIES_FILE = 'db/seeds/food_categories.sql'; const CURRENT_YEAR = new Date().getFullYear(); const BODY_LEVELS = new Set([ - "Very light-bodied", - "Light-bodied", - "Medium-bodied", - "Full-bodied", - "Very full-bodied", + 'Very light-bodied', + 'Light-bodied', + 'Medium-bodied', + 'Full-bodied', + 'Very full-bodied', ]); -const ACIDITY_LEVELS = new Set(["Low", "Medium", "High"]); +const ACIDITY_LEVELS = new Set(['Low', 'Medium', 'High']); type CsvRow = Record; @@ -48,16 +48,17 @@ interface Wine { vintages: number[]; } -type Validation = { ok: true; wine: Wine; warnings: string[] } | { ok: false; reason: string }; +type Validation = + { ok: true; wine: Wine; warnings: string[] } | { ok: false; reason: string }; // ---------- Hjelpefunksjoner for rensing ---------- function text(value: string | undefined): string { - return (value ?? "").trim(); + return (value ?? '').trim(); } function orNull(value: string): string | null { - return value === "" ? null : value; + return value === '' ? null : value; } function toId(value: string | undefined): number | null { @@ -69,8 +70,8 @@ function toId(value: string | undefined): number | null { function parseList(value: string): string[] { const items: string[] = []; for (const match of value.matchAll(/'([^']*)'|"([^"]*)"/g)) { - const item = (match[1] ?? match[2] ?? "").trim(); - if (item !== "") items.push(item); + const item = (match[1] ?? match[2] ?? '').trim(); + if (item !== '') items.push(item); } return [...new Set(items)]; } @@ -90,45 +91,45 @@ function validateWine(row: CsvRow): Validation { const warnings: string[] = []; const id = toId(row.WineID); - if (id === null) return fail("ugyldig WineID"); + if (id === null) return fail('ugyldig WineID'); const name = text(row.WineName); - if (name === "") return fail("mangler WineName"); + if (name === '') return fail('mangler WineName'); const type = text(row.Type); - if (type === "") return fail("mangler Type"); + if (type === '') return fail('mangler Type'); const wineryId = toId(row.WineryID); const wineryName = text(row.WineryName); - if (wineryId === null || wineryName === "") return fail("mangler produsent"); + if (wineryId === null || wineryName === '') return fail('mangler produsent'); const regionId = toId(row.RegionID); const regionName = text(row.RegionName); - if (regionId === null || regionName === "") return fail("mangler region"); + if (regionId === null || regionName === '') return fail('mangler region'); const countryCode = text(row.Code).toUpperCase(); const countryName = text(row.Country); - if (countryCode === "" || countryName === "") return fail("mangler land"); + if (countryCode === '' || countryName === '') return fail('mangler land'); // Valgfrie felt: ugyldig verdi blir NULL, vinen beholdes. const abvText = text(row.ABV); const abvNumber = Number(abvText); let abv: number | null = null; - if (abvText !== "") { + if (abvText !== '') { if (Number.isFinite(abvNumber) && abvNumber > 0 && abvNumber <= 50) { abv = Math.round(abvNumber * 10) / 10; } else { - warnings.push("ugyldig ABV"); + warnings.push('ugyldig ABV'); } } const bodyText = text(row.Body); const body = BODY_LEVELS.has(bodyText) ? bodyText : null; - if (bodyText !== "" && body === null) warnings.push("ugyldig Body"); + if (bodyText !== '' && body === null) warnings.push('ugyldig Body'); const acidityText = text(row.Acidity); const acidity = ACIDITY_LEVELS.has(acidityText) ? acidityText : null; - if (acidityText !== "" && acidity === null) warnings.push("ugyldig Acidity"); + if (acidityText !== '' && acidity === null) warnings.push('ugyldig Acidity'); return { ok: true, @@ -169,15 +170,21 @@ async function readWines() { const examples: string[] = []; let line = 1; // linje 1 er overskriften - const parser = createReadStream(WINES_FILE).pipe(parse({ columns: true, relax_column_count: true })); + const parser = createReadStream(WINES_FILE).pipe( + parse({ columns: true, relax_column_count: true }), + ); for await (const row of parser as AsyncIterable) { line++; let result = validateWine(row); - if (result.ok && seenIds.has(result.wine.id)) result = { ok: false, reason: "duplikat WineID" }; + if (result.ok && seenIds.has(result.wine.id)) + result = { ok: false, reason: 'duplikat WineID' }; if (!result.ok) { countReason(skipped, result.reason); - if (examples.length < 10) examples.push(`linje ${line}: ${result.reason} (WineID=${text(row.WineID) || "tom"})`); + if (examples.length < 10) + examples.push( + `linje ${line}: ${result.reason} (WineID=${text(row.WineID) || 'tom'})`, + ); continue; } @@ -194,20 +201,23 @@ async function readRatings(wineIds: Set) { const skipped = new Map(); let read = 0; - const parser = createReadStream(RATINGS_FILE).pipe(parse({ columns: true, relax_column_count: true })); + const parser = createReadStream(RATINGS_FILE).pipe( + parse({ columns: true, relax_column_count: true }), + ); for await (const row of parser as AsyncIterable) { read++; - if (read % 1_000_000 === 0) console.log(` ... ${read.toLocaleString("nb-NO")} vurderinger lest`); + if (read % 1_000_000 === 0) + console.log(` ... ${read.toLocaleString('nb-NO')} vurderinger lest`); const wineId = toId(row.WineID); const rating = Number(text(row.Rating)); if (wineId === null || !wineIds.has(wineId)) { - countReason(skipped, "ukjent vin"); + countReason(skipped, 'ukjent vin'); continue; } if (!Number.isFinite(rating) || rating < 1 || rating > 5) { - countReason(skipped, "ugyldig rating"); + countReason(skipped, 'ugyldig rating'); continue; } @@ -223,7 +233,11 @@ async function readRatings(wineIds: Set) { // ---------- Skriving til databasen ---------- // Sender radene som JSON i bolker på BATCH_SIZE, i stedet for én spørring per rad. -async function insertInBatches(client: PoolClient, sql: string, rows: object[]) { +async function insertInBatches( + client: PoolClient, + sql: string, + rows: object[], +) { for (let i = 0; i < rows.length; i += BATCH_SIZE) { await client.query(sql, [JSON.stringify(rows.slice(i, i + BATCH_SIZE))]); } @@ -235,14 +249,21 @@ function uniqueBy(items: T[], key: (item: T) => string | number): T[] { return [...map.values()]; } -async function writeToDatabase(client: PoolClient, wines: Wine[], totals: Map) { +async function writeToDatabase( + client: PoolClient, + wines: Wine[], + totals: Map, +) { // 1. Oppslagstabellene, i den rekkefølgen fremmednøklene krever await insertInBatches( client, `INSERT INTO countries (code, name) SELECT code, name FROM jsonb_to_recordset($1::jsonb) AS x(code text, name text) ON CONFLICT DO NOTHING`, - uniqueBy(wines, (w) => w.countryCode).map((w) => ({ code: w.countryCode, name: w.countryName })) + uniqueBy(wines, (w) => w.countryCode).map((w) => ({ + code: w.countryCode, + name: w.countryName, + })), ); await insertInBatches( @@ -250,7 +271,11 @@ async function writeToDatabase(client: PoolClient, wines: Wine[], totals: Map w.regionId).map((w) => ({ id: w.regionId, name: w.regionName, country_code: w.countryCode })) + uniqueBy(wines, (w) => w.regionId).map((w) => ({ + id: w.regionId, + name: w.regionName, + country_code: w.countryCode, + })), ); await insertInBatches( @@ -258,7 +283,11 @@ async function writeToDatabase(client: PoolClient, wines: Wine[], totals: Map w.wineryId).map((w) => ({ id: w.wineryId, name: w.wineryName, website: w.website })) + uniqueBy(wines, (w) => w.wineryId).map((w) => ({ + id: w.wineryId, + name: w.wineryName, + website: w.website, + })), ); // 2. Vinene. ON CONFLICT DO UPDATE gjør at en ny kjøring oppdaterer vinene @@ -286,22 +315,26 @@ async function writeToDatabase(client: PoolClient, wines: Wine[], totals: Map w.grapes.map((name) => ({ wine_id: w.id, name }))); - const foodLinks = wines.flatMap((w) => w.foods.map((name) => ({ wine_id: w.id, name }))); + const grapeLinks = wines.flatMap((w) => + w.grapes.map((name) => ({ wine_id: w.id, name })), + ); + const foodLinks = wines.flatMap((w) => + w.foods.map((name) => ({ wine_id: w.id, name })), + ); await insertInBatches( client, `INSERT INTO grapes (name) SELECT name FROM jsonb_to_recordset($1::jsonb) AS x(name text) ON CONFLICT DO NOTHING`, - uniqueBy(grapeLinks, (l) => l.name).map((l) => ({ name: l.name })) + uniqueBy(grapeLinks, (l) => l.name).map((l) => ({ name: l.name })), ); await insertInBatches( client, `INSERT INTO foods (name) SELECT name FROM jsonb_to_recordset($1::jsonb) AS x(name text) ON CONFLICT DO NOTHING`, - uniqueBy(foodLinks, (l) => l.name).map((l) => ({ name: l.name })) + uniqueBy(foodLinks, (l) => l.name).map((l) => ({ name: l.name })), ); await insertInBatches( @@ -310,7 +343,7 @@ async function writeToDatabase(client: PoolClient, wines: Wine[], totals: Map ({ id, sum: t.sum, count: t.count })) + [...totals].map(([id, t]) => ({ id, sum: t.sum, count: t.count })), ); // 5. Matkategorier: grupperingen ligger i en egen SQL-fil, så den er lett å endre - await client.query(readFileSync(FOOD_CATEGORIES_FILE, "utf8")); + await client.query(readFileSync(FOOD_CATEGORIES_FILE, 'utf8')); return { grapeLinks: grapeLinks.length, foodLinks: foodLinks.length }; } @@ -354,41 +387,46 @@ async function main() { console.log(`Leser vurderinger fra ${RATINGS_FILE}`); const ratings = await readRatings(new Set(wines.map((w) => w.id))); - console.log(` ${ratings.read} vurderinger lest, ${ratings.totals.size} viner har rating`); + console.log( + ` ${ratings.read} vurderinger lest, ${ratings.totals.size} viner har rating`, + ); - console.log("Skriver til databasen"); + console.log('Skriver til databasen'); const client = await pool.connect(); try { - await client.query("BEGIN"); + await client.query('BEGIN'); const links = await writeToDatabase(client, wines, ratings.totals); - await client.query("COMMIT"); - console.log(` ${links.grapeLinks} drue-koblinger og ${links.foodLinks} mat-koblinger`); + await client.query('COMMIT'); + console.log( + ` ${links.grapeLinks} drue-koblinger og ${links.foodLinks} mat-koblinger`, + ); } catch (error) { - await client.query("ROLLBACK"); - console.error("Importen feilet, ingenting ble lagret."); + await client.query('ROLLBACK'); + console.error('Importen feilet, ingenting ble lagret.'); throw error; } finally { client.release(); } - console.log(""); - printCounts("Viner som ble hoppet over:", skipped); + console.log(''); + printCounts('Viner som ble hoppet over:', skipped); if (examples.length > 0) { - console.log("Eksempler:"); + console.log('Eksempler:'); for (const example of examples) console.log(` ${example}`); } - printCounts("Felt satt til NULL på grunn av ugyldig verdi:", warnings); - printCounts("Vurderinger som ble hoppet over:", ratings.skipped); + printCounts('Felt satt til NULL på grunn av ugyldig verdi:', warnings); + printCounts('Vurderinger som ble hoppet over:', ratings.skipped); const uncategorized = await pool.query( `SELECT f.name, count(*)::int AS wines FROM foods f JOIN wine_foods wf ON wf.food_id = f.id WHERE f.category IS NULL - GROUP BY f.name ORDER BY wines DESC` + GROUP BY f.name ORDER BY wines DESC`, ); if (uncategorized.rows.length > 0) { console.log(`Mat uten kategori (legg dem til i ${FOOD_CATEGORIES_FILE}):`); - for (const row of uncategorized.rows) console.log(` ${row.name}: ${row.wines} viner`); + for (const row of uncategorized.rows) + console.log(` ${row.name}: ${row.wines} viner`); } console.log(`Ferdig på ${((Date.now() - started) / 1000).toFixed(1)} s`); } @@ -398,4 +436,4 @@ main() console.error(error); process.exitCode = 1; }) - .finally(() => pool.end()); \ No newline at end of file + .finally(() => pool.end()); diff --git a/backend/src/db.ts b/backend/src/db.ts index 46ab7b4..dbcdc46 100644 --- a/backend/src/db.ts +++ b/backend/src/db.ts @@ -1,5 +1,5 @@ -import pg from "pg"; -import "dotenv/config"; +import pg from 'pg'; +import 'dotenv/config'; const { Pool } = pg; @@ -9,4 +9,4 @@ export const pool = new Pool({ database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, -}); \ No newline at end of file +}); diff --git a/backend/src/index.ts b/backend/src/index.ts index 8e06108..bb7ebb6 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,7 +1,7 @@ -import { ApolloServer } from "@apollo/server"; -import { startStandaloneServer } from "@apollo/server/standalone"; -import {GraphQLError} from "graphql"; -import { pool } from "./db.js"; +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { GraphQLError } from 'graphql'; +import { pool } from './db.js'; const MAX_LIMIT = 100; @@ -11,12 +11,12 @@ function readPaging(args: { limit?: number; offset?: number }) { if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) { throw new GraphQLError(`limit må være mellom 1 og ${MAX_LIMIT}`, { - extensions: { code: "BAD_USER_INPUT" }, + extensions: { code: 'BAD_USER_INPUT' }, }); } if (!Number.isInteger(offset) || offset < 0) { - throw new GraphQLError("offset kan ikke være negativ", { - extensions: { code: "BAD_USER_INPUT" }, + throw new GraphQLError('offset kan ikke være negativ', { + extensions: { code: 'BAD_USER_INPUT' }, }); } return { limit, offset }; @@ -89,12 +89,9 @@ const typeDefs = `#graphql const resolvers = { Query: { - health: () => "OK", - - wines: async ( - _: unknown, - args: { limit?: number; offset?: number } - ) => { + health: () => 'OK', + + wines: async (_: unknown, args: { limit?: number; offset?: number }) => { const { limit, offset } = readPaging(args); const winesResult = await pool.query( @@ -119,12 +116,10 @@ const resolvers = { LIMIT $1 OFFSET $2 `, - [limit, offset] + [limit, offset], ); - const countResult = await pool.query( - `SELECT COUNT(*) FROM wines` - ); + const countResult = await pool.query(`SELECT COUNT(*) FROM wines`); return { items: winesResult.rows, @@ -132,18 +127,15 @@ const resolvers = { }; }, - wine: async ( - _: unknown, - args: { id: string } -) => { - const wineId = Number(args.id); + wine: async (_: unknown, args: { id: string }) => { + const wineId = Number(args.id); - if (!Number.isInteger(wineId)) { - return null; - } + if (!Number.isInteger(wineId)) { + return null; + } - const result = await pool.query( - ` + const result = await pool.query( + ` SELECT w.id, w.name, @@ -186,11 +178,11 @@ const resolvers = { JOIN countries c ON w.country_code = c.code WHERE w.id = $1 `, - [wineId] - ); + [wineId], + ); - return result.rows[0] ?? null; - }, + return result.rows[0] ?? null; + }, foodCategories: async () => { const result = await pool.query( @@ -201,14 +193,14 @@ const resolvers = { WHERE f.category IS NOT NULL GROUP BY f.category ORDER BY f.category - ` + `, ); return result.rows; }, winesByFoodCategory: async ( _: unknown, - args: { category: string; limit?: number; offset?: number } + args: { category: string; limit?: number; offset?: number }, ) => { const { limit, offset } = readPaging(args); const inCategory = ` @@ -242,12 +234,12 @@ const resolvers = { LIMIT $2 OFFSET $3 `, - [args.category, limit, offset] + [args.category, limit, offset], ); const countResult = await pool.query( `SELECT count(*)::int AS count FROM wines w WHERE ${inCategory}`, - [args.category] + [args.category], ); return { @@ -267,4 +259,4 @@ const { url } = await startStandaloneServer(server, { listen: { port: Number(process.env.PORT ?? 3001) }, }); -console.log(`GraphQL server running at ${url}`); \ No newline at end of file +console.log(`GraphQL server running at ${url}`); diff --git a/client/.prettierignore b/client/.prettierignore new file mode 100644 index 0000000..38175d0 --- /dev/null +++ b/client/.prettierignore @@ -0,0 +1,3 @@ +# Prettier also skips everything in .gitignore (dist, node_modules ...). +# Generated by npm, so it should not be reformatted. +package-lock.json diff --git a/client/README.md b/client/README.md index 2d31cb6..d8cb914 100644 --- a/client/README.md +++ b/client/README.md @@ -40,16 +40,15 @@ export default defineConfig([ // other options... }, }, -]) - +]); ``` You can also install [eslint-plugin-react-x](https://npmx.dev/package/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://npmx.dev/package/eslint-plugin-react-dom) for React-specific lint rules: ```js // eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +import reactX from 'eslint-plugin-react-x'; +import reactDom from 'eslint-plugin-react-dom'; export default defineConfig([ globalIgnores(['dist']), @@ -70,6 +69,5 @@ export default defineConfig([ // other options... }, }, -]) - +]); ``` diff --git a/client/eslint.config.js b/client/eslint.config.js index ef614d2..e3927d1 100644 --- a/client/eslint.config.js +++ b/client/eslint.config.js @@ -1,9 +1,14 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' +// ESLint finds bugs and risky patterns in the client code, including +// wrong use of React hooks. Formatting is left to Prettier: +// eslint-config-prettier (last in the list) turns off the ESLint rules +// that would disagree with it. +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import eslintConfigPrettier from 'eslint-config-prettier/flat'; +import { defineConfig, globalIgnores } from 'eslint/config'; export default defineConfig([ globalIgnores(['dist']), @@ -19,4 +24,5 @@ export default defineConfig([ globals: globals.browser, }, }, -]) + eslintConfigPrettier, +]); diff --git a/client/package-lock.json b/client/package-lock.json index cb9fa52..62b26de 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -22,9 +22,11 @@ "@types/react-dom": "^19.2.7", "@vitejs/plugin-react": "^6.1.1", "eslint": "^10.10.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.6", "globals": "^17.12.0", + "prettier": "^3.9.10", "typescript": "~6.0.2", "typescript-eslint": "^8.69.0", "vite": "^8.3.0" @@ -1562,6 +1564,22 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", @@ -2493,6 +2511,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/client/package.json b/client/package.json index a82364f..40d957e 100644 --- a/client/package.json +++ b/client/package.json @@ -7,7 +7,9 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "@apollo/client": "^4.3.2", @@ -24,9 +26,11 @@ "@types/react-dom": "^19.2.7", "@vitejs/plugin-react": "^6.1.1", "eslint": "^10.10.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.6", "globals": "^17.12.0", + "prettier": "^3.9.10", "typescript": "~6.0.2", "typescript-eslint": "^8.69.0", "vite": "^8.3.0" diff --git a/client/src/apollo/client.ts b/client/src/apollo/client.ts index e0a5bff..296653f 100644 --- a/client/src/apollo/client.ts +++ b/client/src/apollo/client.ts @@ -14,4 +14,4 @@ export const client = new ApolloClient({ uri: import.meta.env.VITE_GRAPHQL_URL ?? 'http://localhost:3001/', }), cache: new InMemoryCache(), -}); \ No newline at end of file +}); diff --git a/client/src/components/WineCatalog.tsx b/client/src/components/WineCatalog.tsx index fbb4d69..041d3bb 100644 --- a/client/src/components/WineCatalog.tsx +++ b/client/src/components/WineCatalog.tsx @@ -1,9 +1,8 @@ import { useEffect, useRef, useState } from 'react'; -import { gql } from '@apollo/client'; import { useQuery } from '@apollo/client/react'; import WineList from './WineList'; import Pagination from './Pagination'; -import type { WineListItem } from '../types/wine'; +import { WINES_QUERY, type WinesData } from '../graphql/queries'; export const PAGE_SIZE = 20; @@ -13,31 +12,10 @@ export const PAGE_SIZE = 20; * Data fetching is kept here and display in WineList, so WineList can be * reused and tested without Apollo. * - * We fetch only the fields the list shows, and always with a limit, so we - * never load the whole dataset (scalability and sustainability). - * Pagination comes in issue 10. + * The query itself is in ../graphql/queries.ts. It fetches one page at a + * time with limit and offset, so we never load the whole dataset + * (scalability and sustainability). */ -export const WINES_QUERY = gql` - query Wines($offset: Int, $limit: Int) { - wines(offset: $offset, limit: $limit) { - totalCount - items { - id - name - type - winery - region - country - avgRating - } - } - } -`; - -type WinesData = { - wines: { totalCount: number; items: WineListItem[] }; -}; - export default function WineCatalog() { const [page, setPage] = useState(1); const offset = (page - 1) * PAGE_SIZE; diff --git a/client/src/graphql/queries.ts b/client/src/graphql/queries.ts new file mode 100644 index 0000000..3377ba9 --- /dev/null +++ b/client/src/graphql/queries.ts @@ -0,0 +1,37 @@ +import { gql } from '@apollo/client'; +import type { WineListItem } from '../types/wine'; + +/** + * GraphQL queries used by the client. + * + * They live in their own file instead of next to the components, so that + * React Fast Refresh keeps working (a component file should only export + * components), and so that tests can reuse the same queries. + */ + +/** + * One page of wines for the catalog. + * + * We fetch only the fields the list shows, and always with a limit, so we + * never load the whole dataset (scalability and sustainability). + */ +export const WINES_QUERY = gql` + query Wines($offset: Int, $limit: Int) { + wines(offset: $offset, limit: $limit) { + totalCount + items { + id + name + type + winery + region + country + avgRating + } + } + } +`; + +export type WinesData = { + wines: { totalCount: number; items: WineListItem[] }; +}; diff --git a/client/src/index.css b/client/src/index.css index 148f63a..19b30f9 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -11,7 +11,12 @@ --color-accent: #7b1e2b; /* wine red */ --color-border: #e4dcd9; - font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + font-family: + system-ui, + -apple-system, + 'Segoe UI', + Roboto, + sans-serif; line-height: 1.5; color: var(--color-text); background: var(--color-bg); diff --git a/client/vite.config.ts b/client/vite.config.ts index 2a1e3b1..1af8bed 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,9 +1,9 @@ -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; // https://vite.dev/config/ export default defineConfig({ // The app is served from http://it2810-xx.idi.ntnu.no/project2/ base: '/project2/', plugins: [react()], -}) +});