diff --git a/.github/workflows/deploy-search.yml b/.github/workflows/deploy-search.yml deleted file mode 100644 index b818e9fa..00000000 --- a/.github/workflows/deploy-search.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Deploy Meilisearch -on: - push: - branches: - - main - paths: - - "apps/search/**" - -jobs: - deploy: - name: Deploy app - runs-on: ubuntu-latest - concurrency: deploy-group - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/fly-deploy - with: - dockerfile: apps/search/Dockerfile - fly_config: apps/search/fly.toml - fly_api_token: ${{ secrets.FLY_BAHAR_SEARCH_TOKEN }} diff --git a/.github/workflows/update-indexes.yml b/.github/workflows/update-indexes.yml deleted file mode 100644 index 57a97bc9..00000000 --- a/.github/workflows/update-indexes.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Update Meilisearch Indexes for All Users - -# This workflow updates the settings for all indexes for all users. - -on: workflow_dispatch - -jobs: - update-indexes: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Update Meilisearch Indexes - run: ./scripts/update-indexes.sh - env: - MEILISEARCH_HOST: ${{ secrets.MEILISEARCH_HOST }} - MEILISEARCH_MASTER_KEY: ${{ secrets.MEILISEARCH_MASTER_KEY }} diff --git a/.gitignore b/.gitignore index 19f8430a..05b622c1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ node_modules # testing coverage +# vite +vite.config.ts.timestamp-* + # next.js .next/ out/ diff --git a/CLAUDE.md b/CLAUDE.md index 276b5963..e7a402c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,30 +49,41 @@ Bahar is an Arabic language learning application with these key features: ## Development Environment -### Docker Development Setup (Recommended) +### Local Development -- Start all services: `docker compose up` -- Front-end available at: http://localhost:4000 -- API available at: http://localhost:3000 -- Drizzle Studio available at: http://localhost:4983 -- LibSQL Server available at: http://localhost:8080 (local development database) +Start each service in a separate terminal: -### Local Development +```bash +# Terminal 1: Local database (port 8080) +make local-db + +# Terminal 2: API server (port 3000) +pnpm run dev --filter api + +# Terminal 3: Web app (port 4000) +pnpm run dev --filter web + +# Terminal 4 (optional): Database UI (port 4983) +pnpm run --filter api drizzle:studio +``` + +First-time setup: +```bash +pnpm install +pnpm run --filter api drizzle:migrate +``` + +### Other Commands -- Install packages: `pnpm install` - Build all: `pnpm run build` or `turbo build` -- Dev mode: `pnpm run dev` or `turbo dev` - Lint all: `pnpm run lint` or `turbo lint` - Type check: `pnpm run type-check` or `turbo type-check` -- Local database: `turso dev --db-file apps/api/local.db` or `make local-db` ### Production Setup -- Build and run production: `make prod` - Build production web app: `make build` -- Run production containers: `docker compose -f docker-compose.prod.yaml up -d` -- Serve production web app: `make serve` -- Cleanup production environment: `make cleanup` +- Serve production web app locally: `make serve` +- Delete local data (databases): `make delete-local-data` ## Testing @@ -101,7 +112,6 @@ The monorepo includes shared packages in `/packages`: - `@bahar/fsrs`: FSRS spaced repetition algorithm utilities - `@bahar/i18n`: Internationalization with Lingui - `@bahar/result`: Result type for explicit error handling -- `@bahar/schemas`: Shared Zod validation schemas - `@bahar/search`: Orama search configuration and utilities ## Web App Data Flow @@ -126,7 +136,6 @@ The web app uses a hybrid approach with local-first data handling: 3. **Writing Data**: - Create/update/delete operations write to local database immediately - - API also writes to remote user database (dual-write pattern during migration) - Background sync pushes local changes to remote every 60 seconds 4. **Synchronization**: diff --git a/Makefile b/Makefile index 41c14135..3d2bab7c 100644 --- a/Makefile +++ b/Makefile @@ -10,20 +10,11 @@ serve: build: NODE_ENV=production pnpm turbo build --filter=web -# Run production application -prod: build - docker compose -f docker-compose.prod.yaml up -d --remove-orphans - make serve || (make cleanup && exit 1) - -cleanup: - docker compose -f docker-compose.prod.yaml down - -# Also make sure to clean up the "testing" db group -# in turso as that will have any user dbs that were +# Also make sure to clean up the "testing" db group +# in turso as that will have any user dbs that were # created even locally. delete-local-data: sudo rm -rf ./libsql - sudo rm -rf ./meili_data sudo rm -rf ./apps/api/local.db* -.PHONY: serve build prod cleanup +.PHONY: local-db serve build delete-local-data diff --git a/README.md b/README.md index bdbf4a0e..c799a6fc 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Bahar is an Arabic language learning application built as a monorepo using pnpm 3. Access the web app at `http://localhost:4000` 4. Access the API at `http://localhost:3000` -To run a production setup locally, use `make prod`. +To build and serve the production web app locally, use `make build` followed by `make serve`. ## Projects diff --git a/apps/api/.example.env b/apps/api/.example.env index 4e25c9d1..af99f041 100644 --- a/apps/api/.example.env +++ b/apps/api/.example.env @@ -12,7 +12,7 @@ APP_DOMAIN="" GITHUB_CLIENT_SECRET="" GITHUB_CLIENT_ID="" -SENDGRID_API_KEY="" +RESEND_API_KEY="" UPSTASH_REDIS_REST_URL="" UPSTASH_REDIS_REST_TOKEN="=" @@ -23,9 +23,6 @@ TURSO_PLATFORM_API_KEY="" TURSO_ORG_SLUG="" TURSO_DB_GROUP="testing" -MEILISEARCH_HOST="http://meilisearch:7700" -MEILISEARCH_API_KEY="MASTER_KEY" - BETTER_AUTH_SECRET="" SENTRY_DSN="" diff --git a/apps/api/README.md b/apps/api/README.md index 36ad9c74..3499d594 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -22,6 +22,7 @@ The API uses a database-per-user architecture: - **Per-User Databases**: Each user has their own Turso database for personal data (dictionary entries, flashcards, decks) This design provides: + - Data isolation between users - Independent scaling per user - Simplified backup and restore @@ -53,6 +54,7 @@ pnpm install ### Local Development 1. Start the local database: + ```bash turso dev --db-file local.db # or from project root: @@ -60,17 +62,55 @@ pnpm install ``` 2. Run migrations: + ```bash pnpm run drizzle:migrate ``` -3. Start the dev server: +3. Start the dev servers (from project root): ```bash - pnpm dev + pnpm run dev ``` The API runs on `http://localhost:3000`. +### Setting Up a New User (Fresh Local DB) + +When working with a fresh local database, you need to manually seed the user database migrations: + +1. Start all dev servers from the project root: + + ```bash + pnpm run dev + ``` + +2. In a separate terminal, start the local database: + + ```bash + make local-db + ``` + +3. Run the (local) central database migrations: + + ```bash + pnpm run --filter api drizzle:migrate + ``` + +4. Create a new user through the web app (sign-up flow) + +5. Manually convert the user to an admin in the local database: + + - Open Drizzle Studio: `pnpm run --filter api drizzle:studio` + - Find the user in the `users` table and set the role to `admin` + +6. Seed the user database migrations: + + - Copy the migration SQL from `packages/drizzle-user-db-schemas/drizzle/*.sql` + - Remove the `--> statement-breakpoint` markers and replace with newlines + - Manually insert records into the `migrations` table in the user's database + +7. Log out and log back in for changes to take effect + ## Database Commands ```bash @@ -96,9 +136,9 @@ Utility scripts for database management are in `/scripts`: - `create-user-dbs.ts` - Create individual Turso databases for users - `apply-user-db-migrations.ts` - Apply schema migrations to user databases -- `migrate-settings-decks-to-user-db.ts` - Migrate data from central to user databases Run scripts with: + ```bash npx tsx --env-file=.env scripts/.ts ``` diff --git a/apps/api/drizzle/0011_dazzling_jackal.sql b/apps/api/drizzle/0011_dazzling_jackal.sql new file mode 100644 index 00000000..7b966f7b --- /dev/null +++ b/apps/api/drizzle/0011_dazzling_jackal.sql @@ -0,0 +1,2 @@ +DROP TABLE `decks`;--> statement-breakpoint +DROP TABLE `settings`; \ No newline at end of file diff --git a/apps/api/drizzle/0012_conscious_rhodey.sql b/apps/api/drizzle/0012_conscious_rhodey.sql new file mode 100644 index 00000000..c99ff8f0 --- /dev/null +++ b/apps/api/drizzle/0012_conscious_rhodey.sql @@ -0,0 +1,20 @@ +DROP INDEX "accounts_userId_idx";--> statement-breakpoint +DROP INDEX "sessions_token_unique";--> statement-breakpoint +DROP INDEX "sessions_userId_idx";--> statement-breakpoint +DROP INDEX "users_email_unique";--> statement-breakpoint +DROP INDEX "verifications_identifier_idx";--> statement-breakpoint +DROP INDEX "databases_user_id_unique";--> statement-breakpoint +ALTER TABLE `accounts` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT (cast(unixepoch('subsecond') * 1000 as integer));--> statement-breakpoint +CREATE INDEX `accounts_userId_idx` ON `accounts` (`user_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `sessions_token_unique` ON `sessions` (`token`);--> statement-breakpoint +CREATE INDEX `sessions_userId_idx` ON `sessions` (`user_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint +CREATE INDEX `verifications_identifier_idx` ON `verifications` (`identifier`);--> statement-breakpoint +CREATE UNIQUE INDEX `databases_user_id_unique` ON `databases` (`user_id`);--> statement-breakpoint +ALTER TABLE `sessions` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT (cast(unixepoch('subsecond') * 1000 as integer));--> statement-breakpoint +ALTER TABLE `users` ALTER COLUMN "email_verified" TO "email_verified" integer NOT NULL DEFAULT false;--> statement-breakpoint +ALTER TABLE `users` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT (cast(unixepoch('subsecond') * 1000 as integer));--> statement-breakpoint +ALTER TABLE `users` ALTER COLUMN "updated_at" TO "updated_at" integer NOT NULL DEFAULT (cast(unixepoch('subsecond') * 1000 as integer));--> statement-breakpoint +ALTER TABLE `users` ALTER COLUMN "banned" TO "banned" integer DEFAULT false;--> statement-breakpoint +ALTER TABLE `verifications` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT (cast(unixepoch('subsecond') * 1000 as integer));--> statement-breakpoint +ALTER TABLE `verifications` ALTER COLUMN "updated_at" TO "updated_at" integer NOT NULL DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)); \ No newline at end of file diff --git a/apps/api/drizzle/meta/0011_snapshot.json b/apps/api/drizzle/meta/0011_snapshot.json new file mode 100644 index 00000000..efdd5bcc --- /dev/null +++ b/apps/api/drizzle/meta/0011_snapshot.json @@ -0,0 +1,488 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "eaed323a-906d-4d48-abd3-606870eed334", + "prevId": "47599714-b8e5-49ba-8c13-a47d3d91b374", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "banned": { + "name": "banned", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verifications": { + "name": "verifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "databases": { + "name": "databases", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "db_name": { + "name": "db_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "db_id": { + "name": "db_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "databases_user_id_unique": { + "name": "databases_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "databases_user_id_users_id_fk": { + "name": "databases_user_id_users_id_fk", + "tableFrom": "databases", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "migrations": { + "name": "migrations", + "columns": { + "version": { + "name": "version", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sql_script": { + "name": "sql_script", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/0012_snapshot.json b/apps/api/drizzle/meta/0012_snapshot.json new file mode 100644 index 00000000..b6570173 --- /dev/null +++ b/apps/api/drizzle/meta/0012_snapshot.json @@ -0,0 +1,519 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "fd2acc05-b98c-476c-8e96-7f2973c8ac5b", + "prevId": "eaed323a-906d-4d48-abd3-606870eed334", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "accounts_userId_idx": { + "name": "accounts_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "sessions_userId_idx": { + "name": "sessions_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "banned": { + "name": "banned", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verifications": { + "name": "verifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verifications_identifier_idx": { + "name": "verifications_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "databases": { + "name": "databases", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "db_name": { + "name": "db_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "db_id": { + "name": "db_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "databases_user_id_unique": { + "name": "databases_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "databases_user_id_users_id_fk": { + "name": "databases_user_id_users_id_fk", + "tableFrom": "databases", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "migrations": { + "name": "migrations", + "columns": { + "version": { + "name": "version", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sql_script": { + "name": "sql_script", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index ffc76cb3..3a983abb 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -78,7 +78,20 @@ "when": 1756098050278, "tag": "0010_handy_texas_twister", "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1766630141840, + "tag": "0011_dazzling_jackal", + "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1766689109052, + "tag": "0012_conscious_rhodey", + "breakpoints": true } ] -} - +} \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 2ff61fdc..fc138cfe 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -9,13 +9,9 @@ "prebuild": "npm run clean && npm run typecheck", "build": "node build.mjs", "start": "node --import ./instrument.mjs dist/index.mjs", - "dev": "npm run gen-schema && tsx watch src", + "dev": "tsx watch src", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "gen-schema:dictionary": "json-refs resolve src/dictionary.json | json-schema-to-zod --name 'DictionarySchema' | prettier --parser typescript > src/schemas/dictionary.schema.ts", - "gen-schema:flashcard": "json-refs resolve src/flashcard.json | json-schema-to-zod --name 'FlashcardSchema' | prettier --parser typescript > src/schemas/flashcard.schema.ts", - "gen-schema:full": "json-refs resolve src/schema.json | json-schema-to-zod --name 'WordsSchema' | prettier --parser typescript > src/schemas/words.schema.ts", - "gen-schema": "npm run gen-schema:dictionary && npm run gen-schema:flashcard && npm run gen-schema:full", - "auth:gen": "pnpm dlx @better-auth/cli@1.2.10 generate -y --output src/db/schema/auth.ts && prettier --write src/db/schema/auth.ts", + "auth:gen": "pnpm dlx @better-auth/cli generate -y --output src/db/schema/auth.ts && prettier --write src/db/schema/auth.ts", "drizzle:gen": "drizzle-kit generate", "drizzle:migrate": "drizzle-kit migrate", "drizzle:push": "drizzle-kit push", @@ -23,8 +19,7 @@ "drizzle:drop": "drizzle-kit drop" }, "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.7.2", - "@better-auth/expo": "1.2.10", + "@better-auth/expo": "1.4.9", "@libsql/client": "^0.10.0", "@sentry/esbuild-plugin": "^2.22.7", "@sentry/node": "^8.48.0", @@ -32,29 +27,27 @@ "@trpc/client": "^11.4.3", "@trpc/server": "^11.4.3", "@tursodatabase/api": "^1.9.0", - "@types/cookie-parser": "^1.4.6", - "@upstash/redis": "^1.28.4", - "better-auth": "1.2.10", + "@upstash/redis": "^1.36.0", + "better-auth": "1.4.9", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.4.5", - "drizzle-orm": "^0.36.4", - "drizzle-zod": "^0.5.1", + "drizzle-orm": "^0.45.1", + "drizzle-zod": "^0.8.3", "express": "^4.18.2", - "jose": "^6.0.13", - "meilisearch": "^0.45.0", + "jose": "^6.1.3", "multer": "1.4.5-lts.1", "nanoid": "^5.0.7", "pino": "^9.6.0", "pino-http": "^10.3.0", "resend": "^4.6.0", - "ts-fsrs": "^4.5.1", "tsx": "^4.7.1", - "zod": "^3.24.1" + "zod": "4.0.17" }, "devDependencies": { "@bahar/eslint-config": "workspace:*", "@bahar/typescript-config": "workspace:*", + "@types/cookie-parser": "^1.4.6", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/multer": "^1.4.11", @@ -62,12 +55,10 @@ "@types/pino-http": "^5.8.4", "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", - "drizzle-kit": "^0.28.1", + "drizzle-kit": "^0.31.8", "esbuild": "^0.21.5", "esbuild-plugin-file-path-extensions": "^2.1.2", "eslint": "^8.56.0", - "json-refs": "^3.0.15", - "json-schema-to-zod": "^2.4.1", "pino-pretty": "^13.0.0", "prettier": "^3.1.1", "typescript": "^5.8.3" diff --git a/apps/api/scripts/README.md b/apps/api/scripts/README.md index f768e9e2..76e28a05 100644 --- a/apps/api/scripts/README.md +++ b/apps/api/scripts/README.md @@ -10,12 +10,6 @@ Creates individual Turso databases for users who don't have one and applies all **Purpose**: Sets up user-specific databases in Turso for all users that don't have a user database yet. -### `migrate-settings-decks-to-user-db.ts` - -Migrates settings and decks data from the global database to each user's individual database. - -**Purpose**: Move settings and decks tables from the central database to user-specific databases. This script is idempotent and can be run multiple times safely - it will skip records that already exist in the user database. - ### `apply-user-db-migrations.ts` Applies all pending schema migrations to each user's database. @@ -59,7 +53,6 @@ cd apps/api # Load environment variables when running tsx npx tsx --env-file=.env scripts/create-user-dbs.ts -npx tsx --env-file=.env scripts/migrate-settings-decks-to-user-db.ts npx tsx --env-file=.env scripts/apply-user-db-migrations.ts ``` diff --git a/apps/api/scripts/migrate-meilisearch-to-user-db.ts b/apps/api/scripts/migrate-meilisearch-to-user-db.ts deleted file mode 100644 index 33892210..00000000 --- a/apps/api/scripts/migrate-meilisearch-to-user-db.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { db } from "../src/db"; -import { users } from "../src/db/schema/auth"; -import { databases } from "../src/db/schema/databases"; -import { eq } from "drizzle-orm"; -import { logger } from "../src/utils/logger"; -import { createUserDbClient } from "../src/utils/migration-utils"; -import { meilisearchClient } from "../src/clients/meilisearch"; -import { DictionarySchema } from "../src/schemas/dictionary.schema"; -import { z } from "zod"; -import { createFlashcardStatement } from "../src/routers/dictionary"; -import { InStatement } from "@libsql/client"; - -const BATCH_SIZE = 500; - -const migrateMeilisearchToUserDb = async () => { - logger.info( - "Starting Meilisearch to user database migration for dictionary entries...", - ); - - try { - const usersWithDb = await db - .select() - .from(users) - .innerJoin(databases, eq(users.id, databases.user_id)); - - logger.info(`Found ${usersWithDb.length} users with databases.`); - - let successCount = 0; - let errorCount = 0; - let totalWordsProcessed = 0; - let totalWordsSkipped = 0; - let totalWordsMigrated = 0; - - for (const { users: user, databases: userDb } of usersWithDb) { - logger.info( - { email: user.email, user_id: user.id }, - `Processing user...`, - ); - - try { - const { client: userDbClient } = await createUserDbClient( - userDb.hostname, - userDb.access_token, - userDb.db_name, - userDb.db_id, - ); - - const userIndex = meilisearchClient.index(user.id); - - let userWordsSkipped = 0; - let userWordsMigrated = 0; - let offset = 0; - - try { - // eslint-disable-next-line no-constant-condition - while (true) { - const result = await userIndex.getDocuments< - z.infer - >({ - offset, - limit: BATCH_SIZE, - }); - - if (result.results.length === 0) { - break; - } - - logger.info( - { - email: user.email, - user_id: user.id, - batchSize: result.results.length, - offset, - }, - `Fetched ${result.results.length} words from Meilisearch`, - ); - - const statements: InStatement[] = []; - - for (const word of result.results) { - const existingWord = await userDbClient.execute({ - sql: "SELECT id FROM dictionary_entries WHERE id = ?", - args: [word.id], - }); - - if (existingWord.rows.length > 0) { - userWordsSkipped++; - continue; - } - - const createdAtMs = word.created_at_timestamp - ? word.created_at_timestamp * 1000 - : null; - const updatedAtMs = word.updated_at_timestamp - ? word.updated_at_timestamp * 1000 - : null; - - statements.push({ - sql: `INSERT INTO dictionary_entries ( - id, word, translation, type, definition, root, tags, antonyms, - examples, morphology, created_at, created_at_timestamp_ms, updated_at, updated_at_timestamp_ms - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(id) DO UPDATE SET - word = excluded.word, - translation = excluded.translation, - type = excluded.type, - definition = excluded.definition, - root = excluded.root, - tags = excluded.tags, - antonyms = excluded.antonyms, - examples = excluded.examples, - morphology = excluded.morphology, - updated_at = excluded.updated_at, - updated_at_timestamp_ms = excluded.updated_at_timestamp_ms`, - args: [ - word.id, - word.word, - word.translation, - word.type ?? null, - word.definition ?? null, - word.root ? JSON.stringify(word.root) : null, - word.tags ? JSON.stringify(word.tags) : null, - word.antonyms ? JSON.stringify(word.antonyms) : null, - word.examples ? JSON.stringify(word.examples) : null, - word.morphology ? JSON.stringify(word.morphology) : null, - word.created_at ?? (createdAtMs ? new Date(createdAtMs).toISOString() : null), - createdAtMs, - word.updated_at ?? (updatedAtMs ? new Date(updatedAtMs).toISOString() : null), - updatedAtMs, - ], - }); - - statements.push( - createFlashcardStatement({ - dictionaryEntryId: word.id, - direction: "forward", - flashcardData: word.flashcard, - }), - ); - - statements.push( - createFlashcardStatement({ - dictionaryEntryId: word.id, - direction: "reverse", - flashcardData: word.flashcard_reverse, - }), - ); - - userWordsMigrated++; - } - - if (statements.length > 0) { - await userDbClient.batch(statements); - logger.info( - { - email: user.email, - user_id: user.id, - statementCount: statements.length, - }, - `Wrote batch to Turso`, - ); - } - - offset += BATCH_SIZE; - - if (result.results.length < BATCH_SIZE) { - break; - } - } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : String(err); - if ( - errorMessage.includes("index_not_found") || - errorMessage.includes("Index") || - errorMessage.includes("not found") - ) { - logger.info( - { email: user.email, user_id: user.id }, - "No Meilisearch index found for user, skipping", - ); - continue; - } - throw err; - } - - logger.info( - { - email: user.email, - user_id: user.id, - migratedCount: userWordsMigrated, - skippedCount: userWordsSkipped, - }, - `Migrated ${userWordsMigrated} words, skipped ${userWordsSkipped} words`, - ); - - totalWordsProcessed += userWordsMigrated + userWordsSkipped; - totalWordsSkipped += userWordsSkipped; - totalWordsMigrated += userWordsMigrated; - successCount++; - } catch (err) { - errorCount++; - logger.error( - { - err, - email: user.email, - user_id: user.id, - }, - `Failed to migrate Meilisearch data for user. Skipping.`, - ); - } - } - - logger.info( - { - totalUsers: usersWithDb.length, - successCount, - errorCount, - totalWordsProcessed, - totalWordsMigrated, - totalWordsSkipped, - }, - "Migration completed!", - ); - } catch (error) { - logger.error(error, "Migration failed"); - process.exit(1); - } -}; - -(async () => { - try { - await migrateMeilisearchToUserDb(); - logger.info("Migration script finished successfully"); - } catch (error) { - console.error("Error migrating Meilisearch data:", error); - process.exit(1); - } -})(); diff --git a/apps/api/scripts/migrate-settings-decks-to-user-db.ts b/apps/api/scripts/migrate-settings-decks-to-user-db.ts deleted file mode 100644 index 2424341c..00000000 --- a/apps/api/scripts/migrate-settings-decks-to-user-db.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { db } from "../src/db"; -import { users } from "../src/db/schema/auth"; -import { databases } from "../src/db/schema/databases"; -import { settings } from "../src/db/schema/settings"; -import { decks } from "../src/db/schema/decks"; -import { eq } from "drizzle-orm"; -import { logger } from "../src/utils/logger"; -import { createClient } from "@libsql/client"; -import { tursoPlatformClient } from "../src/clients/turso"; - -/** - * Refreshes an expired access token for a user database - */ -const refreshAccessToken = async ( - dbName: string, - dbId: string, -): Promise => { - logger.info({ dbName, dbId }, "Access token expired, creating new token..."); - - const newToken = await tursoPlatformClient.databases.createToken(dbName, { - authorization: "full-access", - expiration: "2w", - }); - - // Update the token in the databases table - await db - .update(databases) - .set({ access_token: newToken.jwt }) - .where(eq(databases.db_id, dbId)); - - logger.info({ dbName, dbId }, "Created and saved new access token"); - - return newToken.jwt; -}; - -/** - * Creates a database client, refreshing the token if it's expired - */ -const createUserDbClient = async ( - hostname: string, - accessToken: string, - dbName: string, - dbId: string, -) => { - const client = createClient({ - url: `libsql://${hostname}`, - authToken: accessToken, - }); - - // Test the connection - try { - await client.execute("SELECT 1"); - return { client, token: accessToken }; - } catch (err) { - // Check if it's an auth error - const errorMessage = err instanceof Error ? err.message : String(err); - if (errorMessage.includes("status 401")) { - logger.info( - { dbName, dbId }, - "Token appears to be expired, refreshing...", - ); - const newToken = await refreshAccessToken(dbName, dbId); - const newClient = createClient({ - url: `libsql://${hostname}`, - authToken: newToken, - }); - return { client: newClient, token: newToken }; - } - // If it's not an auth error, rethrow - throw err; - } -}; - -/** - * Script that migrates settings and decks data from the global database - * to each user's individual database. - * - * This script is idempotent - it can be run multiple times safely. - * It will skip records that already exist in the user database. - */ -const migrateSettingsAndDecksToUserDb = async () => { - logger.info("Starting settings and decks migration to user databases..."); - - try { - // Get all users who have a user database - const usersWithDb = await db - .select() - .from(users) - .innerJoin(databases, eq(users.id, databases.user_id)); - - logger.info(`Found ${usersWithDb.length} users with databases.`); - - let successCount = 0; - let errorCount = 0; - - for (const { users: user, databases: userDb } of usersWithDb) { - logger.info( - { email: user.email, user_id: user.id }, - `Processing user...`, - ); - - try { - // Connect to user's database (with automatic token refresh if needed) - const { client: userDbClient } = await createUserDbClient( - userDb.hostname, - userDb.access_token, - userDb.db_name, - userDb.db_id, - ); - - // Migrate settings - const globalSettings = await db - .select() - .from(settings) - .where(eq(settings.user_id, user.id)) - .limit(1); - - if (globalSettings.length > 0) { - const setting = globalSettings[0]; - - // Check if settings already exist in user DB - const existingSettings = await userDbClient.execute( - "SELECT id FROM settings LIMIT 1", - ); - - if (existingSettings.rows.length === 0) { - // Insert settings into user DB (without user_id since it's not needed in user-specific DB) - await userDbClient.execute({ - sql: `INSERT INTO settings (id, show_reverse_flashcards, show_antonyms_in_flashcard) - VALUES (?, ?, ?)`, - args: [ - setting.id, - setting.show_reverse_flashcards ? 1 : 0, - setting.show_antonyms_in_flashcard, - ], - }); - - logger.info( - { email: user.email, user_id: user.id }, - "Migrated settings to user DB", - ); - } else { - logger.info( - { email: user.email, user_id: user.id }, - "Settings already exist in user DB, skipping", - ); - } - } else { - logger.info( - { email: user.email, user_id: user.id }, - "No settings found in global DB", - ); - } - - // Migrate decks - const globalDecks = await db - .select() - .from(decks) - .where(eq(decks.user_id, user.id)); - - logger.info( - { - email: user.email, - user_id: user.id, - deckCount: globalDecks.length, - }, - `Found ${globalDecks.length} decks in global DB`, - ); - - let migratedDeckCount = 0; - let skippedDeckCount = 0; - - for (const deck of globalDecks) { - // Check if deck already exists in user DB - const existingDeck = await userDbClient.execute({ - sql: "SELECT id FROM decks WHERE id = ?", - args: [deck.id], - }); - - if (existingDeck.rows.length === 0) { - // Insert deck into user DB (without user_id) - await userDbClient.execute({ - sql: "INSERT INTO decks (id, name, filters) VALUES (?, ?, ?)", - args: [deck.id, deck.name, JSON.stringify(deck.filters)], - }); - - migratedDeckCount++; - } else { - skippedDeckCount++; - } - } - - logger.info( - { - email: user.email, - user_id: user.id, - migratedDeckCount, - skippedDeckCount, - }, - `Migrated ${migratedDeckCount} decks, skipped ${skippedDeckCount} decks`, - ); - - successCount++; - } catch (err) { - errorCount++; - logger.error( - { - err, - email: user.email, - user_id: user.id, - }, - `Failed to migrate data for user. Skipping.`, - ); - } - } - - logger.info( - { - totalUsers: usersWithDb.length, - successCount, - errorCount, - }, - "Migration completed!", - ); - } catch (error) { - logger.error(error, "Migration failed"); - process.exit(1); - } -}; - -(async () => { - try { - await migrateSettingsAndDecksToUserDb(); - logger.info("Migration script finished successfully"); - } catch (error) { - console.error("Error migrating settings and decks:", error); - process.exit(1); - } -})(); diff --git a/apps/api/src/auth.ts b/apps/api/src/auth.ts index cb581e65..4acc2360 100644 --- a/apps/api/src/auth.ts +++ b/apps/api/src/auth.ts @@ -12,7 +12,6 @@ import { sendMail } from "./clients/mail"; import { getAllowedDomains } from "./utils"; import { config } from "./utils/config"; import { redisClient } from "./clients/redis"; -import { createUserIndex } from "./clients/meilisearch"; import { LogCategory, logger } from "./utils/logger"; import { expo } from "@better-auth/expo"; import { applyAllNewMigrations, createNewUserDb } from "./clients/turso"; @@ -167,27 +166,14 @@ export const auth = betterAuth({ disabled: true, level: "debug", log: (level, message, ...args) => { - // TODO: refactor this once the following PR has been addressed - // https://github.com/better-auth/better-auth/issues/1115 - const ANSI_ESCAPE_REGEXP = new RegExp(String.raw`\u001b\[\d+m`); - - /** - * Better auth passes us a formatted message but - * we just want the raw message since pino will - * handle formatting. - */ - const unformattedMessage = message - ?.split?.("[Better Auth]:")?.[1] - ?.replace(ANSI_ESCAPE_REGEXP, "") - ?.trim(); - const mergedArgs = args.reduce((acc, curr) => ({ ...acc, ...curr }), {}); - logger[level](mergedArgs, unformattedMessage); + logger[level](mergedArgs, message); }, }, secondaryStorage: { // TODO: add trace ids to these logs + // Upstash client will automatically deserialize JSON strings get: async (key) => { logger.debug( @@ -379,8 +365,6 @@ export const auth = betterAuth({ "Created user.", ); - await createUserIndex(user.id); - await setUpUserDb(user.id); }, }, diff --git a/apps/api/src/clients/meilisearch.ts b/apps/api/src/clients/meilisearch.ts deleted file mode 100644 index 0a311364..00000000 --- a/apps/api/src/clients/meilisearch.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { MeiliSearch } from "meilisearch"; -import { config } from "../utils/config"; -import { LogCategory, logger } from "../utils/logger"; - -export const meilisearchClient = new MeiliSearch({ - host: config.MEILISEARCH_HOST!, - apiKey: config.MEILISEARCH_API_KEY!, -}); - -// TODO: Create single source of truth between this and script in the root -// Not high priority since I will be removing meilisearch - -const MAX_TOTAL_HITS = 5000; - -/** - * Creates a new index for a user and configures it - * with the necessary universal settings. - * - * @param indexName The name of the index to create. - */ -export const createUserIndex = async (indexName: string) => { - const { taskUid: createTaskUid } = - await meilisearchClient.createIndex(indexName); - - await meilisearchClient.waitForTask(createTaskUid); - - const userIndex = meilisearchClient.index(indexName); - - logger.info( - { indexName, category: LogCategory.DATABASE, event: "index_created.start" }, - "Creating user index...", - ); - - const { taskUid: updateTaskUid } = await userIndex.updateSettings({ - sortableAttributes: [ - "flashcard.due_timestamp", - "flashcard_reverse.due_timestamp", - ], - filterableAttributes: [ - "flashcard.due_timestamp", - "flashcard_reverse.due_timestamp", - "tags", - "type", - ], - pagination: { - maxTotalHits: MAX_TOTAL_HITS, - }, - }); - - const task = await meilisearchClient.waitForTask(updateTaskUid); - - logger.info( - { - task, - indexName, - category: LogCategory.DATABASE, - event: "index_created.end", - }, - "Created user index.", - ); -}; diff --git a/apps/api/src/clients/turso.ts b/apps/api/src/clients/turso.ts index 48695114..640e324b 100644 --- a/apps/api/src/clients/turso.ts +++ b/apps/api/src/clients/turso.ts @@ -226,7 +226,6 @@ const refreshAccessToken = async ( /** * Gets a database client for a user's database, handling token refresh if needed. - * This is used for dual-write operations during migration from global DB to user DBs. * * @param userId - The user ID to get the database client for * @returns The database client or null if the user doesn't have a database yet diff --git a/apps/api/src/db/schema/auth.ts b/apps/api/src/db/schema/auth.ts index 8a5e6b5d..c8395b79 100644 --- a/apps/api/src/db/schema/auth.ts +++ b/apps/api/src/db/schema/auth.ts @@ -1,70 +1,112 @@ -import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; +import { relations, sql } from "drizzle-orm"; +import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; export const users = sqliteTable("users", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), emailVerified: integer("email_verified", { mode: "boolean" }) - .$defaultFn(() => false) + .default(false) .notNull(), image: text("image"), - createdAt: integer("created_at", { mode: "timestamp" }) - .$defaultFn(() => /* @__PURE__ */ new Date()) + createdAt: integer("created_at", { mode: "timestamp_ms" }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .notNull(), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .$defaultFn(() => /* @__PURE__ */ new Date()) + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), role: text("role"), - banned: integer("banned", { mode: "boolean" }), + banned: integer("banned", { mode: "boolean" }).default(false), banReason: text("ban_reason"), - banExpires: integer("ban_expires", { mode: "timestamp" }), + banExpires: integer("ban_expires", { mode: "timestamp_ms" }), }); -export const sessions = sqliteTable("sessions", { - id: text("id").primaryKey(), - expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), - token: text("token").notNull().unique(), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), - ipAddress: text("ip_address"), - userAgent: text("user_agent"), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - impersonatedBy: text("impersonated_by"), -}); +export const sessions = sqliteTable( + "sessions", + { + id: text("id").primaryKey(), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + token: text("token").notNull().unique(), + createdAt: integer("created_at", { mode: "timestamp_ms" }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + impersonatedBy: text("impersonated_by"), + }, + (table) => [index("sessions_userId_idx").on(table.userId)], +); -export const accounts = sqliteTable("accounts", { - id: text("id").primaryKey(), - accountId: text("account_id").notNull(), - providerId: text("provider_id").notNull(), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - accessToken: text("access_token"), - refreshToken: text("refresh_token"), - idToken: text("id_token"), - accessTokenExpiresAt: integer("access_token_expires_at", { - mode: "timestamp", - }), - refreshTokenExpiresAt: integer("refresh_token_expires_at", { - mode: "timestamp", +export const accounts = sqliteTable( + "accounts", + { + id: text("id").primaryKey(), + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: integer("access_token_expires_at", { + mode: "timestamp_ms", + }), + refreshTokenExpiresAt: integer("refresh_token_expires_at", { + mode: "timestamp_ms", + }), + scope: text("scope"), + password: text("password"), + createdAt: integer("created_at", { mode: "timestamp_ms" }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index("accounts_userId_idx").on(table.userId)], +); + +export const verifications = sqliteTable( + "verifications", + { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index("verifications_identifier_idx").on(table.identifier)], +); + +export const usersRelations = relations(users, ({ many }) => ({ + sessions: many(sessions), + accounts: many(accounts), +})); + +export const sessionsRelations = relations(sessions, ({ one }) => ({ + users: one(users, { + fields: [sessions.userId], + references: [users.id], }), - scope: text("scope"), - password: text("password"), - createdAt: integer("created_at", { mode: "timestamp" }).notNull(), - updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), -}); +})); -export const verifications = sqliteTable("verifications", { - id: text("id").primaryKey(), - identifier: text("identifier").notNull(), - value: text("value").notNull(), - expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), - createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn( - () => /* @__PURE__ */ new Date(), - ), - updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn( - () => /* @__PURE__ */ new Date(), - ), -}); +export const accountsRelations = relations(accounts, ({ one }) => ({ + users: one(users, { + fields: [accounts.userId], + references: [users.id], + }), +})); diff --git a/apps/api/src/db/schema/decks.ts b/apps/api/src/db/schema/decks.ts deleted file mode 100644 index ea907683..00000000 --- a/apps/api/src/db/schema/decks.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { text, sqliteTable } from "drizzle-orm/sqlite-core"; -import { createInsertSchema, createSelectSchema } from "drizzle-zod"; -import { users } from "./auth"; -import { z } from "zod"; - -export const decks = sqliteTable("decks", { - id: text("id").notNull().primaryKey(), - user_id: text("user_id") - .notNull() - .references(() => users.id), - name: text("name").notNull(), - filters: text("filters", { mode: "json" }).$type<{ - tags?: string[]; - state?: (0 | 1 | 2 | 3)[]; - types?: ("ism" | "fi'l" | "harf" | "expression")[]; - }>(), -}); - -export type Deck = typeof decks.$inferSelect; -export type InsertDeck = typeof decks.$inferInsert; - -export const FilterSchema = z.object({ - tags: z.array(z.string()).optional(), - state: z - .array(z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)])) - .optional(), - types: z.array(z.enum(["ism", "fi'l", "harf", "expression"])).optional(), -}); - -export const InsertDecksSchema = createInsertSchema(decks, { - filters: FilterSchema, -}); -export const SelectDecksSchema = createSelectSchema(decks, { - filters: FilterSchema, -}); diff --git a/apps/api/src/db/schema/settings.ts b/apps/api/src/db/schema/settings.ts deleted file mode 100644 index c66e0abe..00000000 --- a/apps/api/src/db/schema/settings.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { text, sqliteTable, integer } from "drizzle-orm/sqlite-core"; -import { createInsertSchema, createSelectSchema } from "drizzle-zod"; -import { users } from "./auth"; - -export const settings = sqliteTable("settings", { - id: text("id").notNull().primaryKey(), - user_id: text("user_id") - .notNull() - .unique() - .references(() => users.id), - show_reverse_flashcards: integer("show_reverse_flashcards", { - mode: "boolean", - }).default(false), - show_antonyms_in_flashcard: text("show_antonyms_in_flashcard", { - enum: ["hidden", "answer", "hint"], - }).default("hidden"), -}); - -export type Setting = typeof settings.$inferSelect; -export type InsertSetting = typeof settings.$inferInsert; - -export const InsertSettingsSchema = createInsertSchema(settings); -export const SelectSettingsSchema = createSelectSchema(settings); diff --git a/apps/api/src/dictionary.json b/apps/api/src/dictionary.json deleted file mode 100644 index 9b3b841d..00000000 --- a/apps/api/src/dictionary.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "created_at_timestamp": { - "type": "integer", - "description": "A UNIX timestamp in seconds that represents the same date as `created_at`." - }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "updated_at_timestamp": { - "type": "integer", - "description": "A UNIX timestamp in seconds that represents the same date as `updated_at`." - }, - "word": { - "type": "string", - "description": "A word or expression in Arabic." - }, - "definition": { - "type": "string", - "description": "The definition of the word in Arabic." - }, - "translation": { - "type": "string", - "description": "The English translation of the word." - }, - "type": { - "type": "string", - "description": "The type of the word.", - "enum": ["ism", "fi'l", "harf", "expression"] - }, - "root": { - "type": "array", - "description": "An array of letters representing the root letters of the word.", - "items": { - "type": "string" - } - }, - "tags": { - "type": "array", - "description": "Any tags associated with the word. These can denote things like the context.", - "items": { - "type": "string" - } - }, - "antonyms": { - "type": "array", - "description": "Any antonyms of the word.", - "items": { - "type": "object", - "properties": { - "word": { - "type": "string" - } - } - } - }, - "examples": { - "type": "array", - "description": "Examples using the word.", - "items": { - "type": "object", - "properties": { - "sentence": { - "description": "A sentence using the word.", - "type": "string" - }, - "context": { - "description": "The context of the sentence. This can refer to casual or formal contexts, for example.", - "type": "string" - }, - "translation": { - "description": "The English translation of the sentence.", - "type": "string" - } - }, - "required": ["sentence"] - } - }, - "morphology": { - "type": "object", - "description": "The morphological properties of the word.", - "properties": { - "ism": { - "type": "object", - "description": "The morphological properties of an ism such as the gender and the different plural and singular forms.", - "properties": { - "singular": { - "type": "string" - }, - "dual": { - "type": "string" - }, - "plurals": { - "type": "array", - "description": "The (broken) plural forms of the word. Use harakat to indicate inflection.", - "items": { - "type": "object", - "properties": { - "word": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Any additional details about this form such as frequency of usage or context." - } - }, - "required": ["word"] - } - }, - "gender": { - "type": "string", - "enum": ["masculine", "feminine"] - }, - "inflection": { - "type": "string", - "description": "The inflection of the word. This refers to how many case endings the word can take. Three means it can take all three, two means it can only take raf' and nasb, and one means it is indeclinable.", - "enum": ["indeclinable", "diptote", "triptote"] - } - } - }, - "verb": { - "type": "object", - "description": "The morphological properties of a fi'l such as the verb conjugations and noun forms.", - "properties": { - "huroof": { - "type": "array", - "description": "A list of the prepositions that can be used with the fi'l. Typically, if the meaning with the preposition is completely different, then it's better to create a separate dictionary entry.", - "items": { - "type": "object", - "properties": { - "harf": { - "type": "string" - }, - "meaning": { - "type": "string", - "description": "The meaning of the fi'l when used with the preposition. If it is the same as the base meaning, then leave this blank." - } - }, - "required": ["harf"] - } - }, - "past_tense": { - "type": "string" - }, - "present_tense": { - "type": "string" - }, - "active_participle": { - "type": "string" - }, - "passive_participle": { - "type": "string" - }, - "imperative": { - "type": "string" - }, - "masadir": { - "type": "array", - "description": "A list of the possible verbal noun forms.", - "items": { - "type": "object", - "properties": { - "word": { - "type": "string" - }, - "details": { - "type": "string", - "description": "Any additional details about this form such as frequency of usage or context." - } - }, - "required": ["word"] - } - }, - "form": { - "type": "string", - "description": "The form or pattern of the verb from I - X." - }, - "form_arabic": { - "type": "string", - "description": "The form or pattern of the verb in Arabic using the root ف-ع-ل." - } - } - } - } - }, - "flashcard": { - "$ref": "flashcard.json" - }, - "flashcard_reverse": { - "$ref": "flashcard.json" - } - }, - "required": ["id", "word", "translation"] -} diff --git a/apps/api/src/flashcard.json b/apps/api/src/flashcard.json deleted file mode 100644 index 87bb0a3a..00000000 --- a/apps/api/src/flashcard.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "description": "Properties used for scheduling the flashcards associated with the word using FSRS. When adding new words, just set these to the default values. For updating existing words, leave them as-is to keep flashcard progress or set to default values to reset it.", - "properties": { - "difficulty": { - "type": "number", - "minimum": 0, - "default": 0 - }, - "due": { - "type": "string", - "format": "date-time", - "description": "The date when the card needs to be reviewed. Set to the current time for new cards." - }, - "due_timestamp": { - "type": "integer", - "minimum": 0, - "description": "A UNIX timestamp in seconds that represents the same date as `due`." - }, - "elapsed_days": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "lapses": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "last_review": { - "type": ["string", "null"], - "format": "date-time" - }, - "last_review_timestamp": { - "type": ["integer", "null"], - "minimum": 0, - "description": "A UNIX timestamp in seconds that represents the same date as `last_review`, if present." - }, - "reps": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "scheduled_days": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "stability": { - "type": "number", - "minimum": 0, - "default": 0 - }, - "state": { - "type": "integer", - "enum": [0, 1, 2, 3], - "description": "0 is New, 1 is Learning, 2 is Review, and 3 is Relearning.", - "default": 0 - } - }, - "required": [ - "difficulty", - "due", - "due_timestamp", - "elapsed_days", - "lapses", - "reps", - "scheduled_days", - "stability", - "state" - ] -} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 8b1f96c6..1c200647 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -6,12 +6,7 @@ import { router, createContext } from "./trpc"; import * as trpcExpress from "@trpc/server/adapters/express"; import cookieParser from "cookie-parser"; import cors from "cors"; -import { dictionaryRouter, trpcDictionaryRouter } from "./routers/dictionary"; -import { flashcardRouter } from "./routers/flashcard"; -import { getAllowedDomains, getFullSchema } from "./utils"; -import { tagsRouter } from "./routers/tags"; -import { settingsRouter } from "./routers/settings"; -import { decksRouter } from "./routers/decks"; +import { getAllowedDomains } from "./utils"; import { Session, User, auth } from "./auth"; import { toNodeHandler } from "better-auth/node"; import { config } from "./utils/config"; @@ -34,11 +29,6 @@ const port = config.PORT; const host = config.HOST; const appRouter = router({ - flashcard: flashcardRouter, - dictionary: trpcDictionaryRouter, - tags: tagsRouter, - settings: settingsRouter, - decks: decksRouter, migrations: migrationsRouter, databases: databasesRouter, }); @@ -75,20 +65,6 @@ app.all("/api/auth/*", toNodeHandler(auth)); app.use(express.json()); app.use(cookieParser()); -app.use(dictionaryRouter); - -app.get("/schema.json", async (_req, res) => { - try { - const schema = await getFullSchema(); - - return res.json(schema); - } catch (err) { - logger.error("Error bundling the schema: ", err); - - return res.status(500).send("There was an error fetching the schema."); - } -}); - app.use( "/trpc", trpcExpress.createExpressMiddleware({ diff --git a/apps/api/src/routers/decks.ts b/apps/api/src/routers/decks.ts deleted file mode 100644 index b4e2d1b8..00000000 --- a/apps/api/src/routers/decks.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { router, protectedProcedure } from "../trpc"; -import { db } from "../db"; -import { eq } from "drizzle-orm"; -import { nanoid } from "nanoid"; -import { - InsertDecksSchema, - SelectDecksSchema, - decks, -} from "../db/schema/decks"; -import { z } from "zod"; -import { FLASHCARD_LIMIT, queryFlashcards } from "./flashcard"; -import { getUserDbClient } from "../clients/turso"; - -export const decksRouter = router({ - list: protectedProcedure - .input( - z - .object({ show_reverse: z.boolean().default(false).optional() }) - .optional(), - ) - .output( - z.array( - SelectDecksSchema.extend({ - to_review: z.number(), - total_hits: z.number(), - }), - ), - // TODO: This optional will break types on the client. - // First appeared after upgrading to typescript v5.8.3 and trpc v11.4.3 - // related issue: https://github.com/trpc/trpc/issues/6521 - // .optional(), - ) - .query(async ({ ctx, input }) => { - const { user } = ctx; - const { show_reverse } = input ?? {}; - - const userDbClient = await getUserDbClient(user.id); - - if (!userDbClient) { - return []; - } - - const result = await userDbClient.execute( - "SELECT id, name, filters FROM decks LIMIT 20", - ); - - const results = result.rows.map((row) => ({ - id: row.id as string, - user_id: user.id, - name: row.name as string, - filters: row.filters ? JSON.parse(row.filters as string) : null, - })); - - const res = await Promise.all( - results.map(async (result) => { - const filters = result.filters; - - const { flashcards, totalHits } = await queryFlashcards({ - user_id: user.id, - fields: ["id"], - limit: FLASHCARD_LIMIT, - show_only_today: true, - filters, - show_reverse, - }); - - return { - ...result, - to_review: flashcards.length, - total_hits: totalHits, - }; - }), - ); - - return res; - }), - - create: protectedProcedure - .input(InsertDecksSchema.omit({ id: true, user_id: true })) - .output(SelectDecksSchema) - .mutation(async ({ ctx, input }) => { - const { user } = ctx; - - const results = await db - .insert(decks) - .values({ id: nanoid(), user_id: user.id, ...input }) - .returning(); - - const result = results[0]; - - return result; - }), - - update: protectedProcedure - .input(InsertDecksSchema.pick({ id: true, name: true, filters: true })) - .output(SelectDecksSchema) - .mutation(async ({ input }) => { - const results = await db - .update(decks) - .set({ name: input.name, filters: input.filters }) - .where(eq(decks.id, input.id)) - .returning(); - - const result = results[0]; - - return result; - }), - - delete: protectedProcedure - .input(z.object({ id: z.string() })) - .output(SelectDecksSchema) - .mutation(async ({ input }) => { - const { id } = input; - - const results = await db - .delete(decks) - .where(eq(decks.id, id)) - .returning(); - - const result = results[0]; - - return result; - }), -}); diff --git a/apps/api/src/routers/dictionary.ts b/apps/api/src/routers/dictionary.ts deleted file mode 100644 index 7d8e2857..00000000 --- a/apps/api/src/routers/dictionary.ts +++ /dev/null @@ -1,466 +0,0 @@ -import express, { - type Router, - type Request, - type Response, - type NextFunction, -} from "express"; -import { nanoid } from "nanoid"; -import { router, protectedProcedure } from "../trpc"; -import multer from "multer"; -import { meilisearchClient } from "../clients/meilisearch"; -import { auth } from "../middleware"; -import { - ErrorCode, - ImportErrorCode, - ImportResponseError, - MeilisearchError, -} from "../utils/error"; -import { z } from "zod"; -import { TRPCError } from "@trpc/server"; -import { DictionarySchema } from "../schemas/dictionary.schema"; -import { WordsSchema } from "../schemas/words.schema"; - -import { createEmptyCard } from "ts-fsrs"; -import { batchIterator } from "../utils"; - -// TODO: resolve this dynamically from the json schema -export const JSON_SCHEMA_FIELDS = [ - "id", - "word", - "definition", - "translation", - "type", - "root", - "tags", - "antonyms", - "examples", - "morphology", - "created_at", - "created_at_timestamp", - "updated_at", - "updated_at_timestamp", - "flashcard", - "flashcard_reverse", -]; - -export enum Inflection { - indeclinable = "indeclinable ", - diptote = "diptote ", - triptote = "triptote ", -} - -export const createFlashcardStatement = ({ - dictionaryEntryId, - direction, - flashcardData, -}: { - dictionaryEntryId: string; - direction: "forward" | "reverse"; - flashcardData?: z.infer["flashcard"]; -}) => { - const emptyCard = createEmptyCard(new Date()); - - const dueMs = flashcardData?.due_timestamp - ? flashcardData.due_timestamp * 1000 - : new Date(emptyCard.due).getTime(); - const lastReviewMs = flashcardData?.last_review_timestamp - ? flashcardData.last_review_timestamp * 1000 - : null; - - return { - sql: `INSERT INTO flashcards ( - id, dictionary_entry_id, difficulty, due, due_timestamp_ms, elapsed_days, - lapses, last_review, last_review_timestamp_ms, reps, scheduled_days, stability, state, direction, is_hidden - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(dictionary_entry_id, direction) DO UPDATE SET - difficulty = excluded.difficulty, - due = excluded.due, - due_timestamp_ms = excluded.due_timestamp_ms, - elapsed_days = excluded.elapsed_days, - lapses = excluded.lapses, - last_review = excluded.last_review, - last_review_timestamp_ms = excluded.last_review_timestamp_ms, - reps = excluded.reps, - scheduled_days = excluded.scheduled_days, - stability = excluded.stability, - state = excluded.state`, - args: [ - nanoid(), - dictionaryEntryId, - flashcardData?.difficulty ?? emptyCard.difficulty, - flashcardData?.due ?? emptyCard.due.toISOString(), - dueMs, - flashcardData?.elapsed_days ?? emptyCard.elapsed_days, - flashcardData?.lapses ?? emptyCard.lapses, - flashcardData?.last_review ?? null, - lastReviewMs, - flashcardData?.reps ?? emptyCard.reps, - flashcardData?.scheduled_days ?? emptyCard.scheduled_days, - flashcardData?.stability ?? emptyCard.stability, - flashcardData?.state ?? emptyCard.state, - direction, - 0, - ], - }; -}; - -const upload = multer({ - storage: multer.memoryStorage(), - limits: { - fileSize: 1024 * 1024 * 2, // 2 MB in bytes - }, - fileFilter: (_req, file, cb) => { - if (file.mimetype !== "application/json") { - cb(new Error("Invalid file type")); - - return; - } - - cb(null, true); - }, -}).single("dictionary"); - -const uploadWithErrorHandling = ( - req: Request, - res: Response, - next: NextFunction, -) => { - upload(req, res, (err) => { - if (err instanceof Error) { - if (err.message.includes("Invalid file type")) { - return res.status(400).json({ message: err.message }); - } - - return res.status(500).json({ message: err.message }); - } - - return next(); - }); -}; - -export const dictionaryRouter: Router = express.Router(); - -dictionaryRouter.post( - "/dictionary/import", - auth, - uploadWithErrorHandling, - async (req, res) => { - const fileData = req.file?.buffer.toString("utf-8"); - - if (!fileData) { - return res.status(200).end(); - } - - let dictionary; - - try { - dictionary = JSON.parse(fileData); - } catch (error) { - return res.status(400).json({ - message: "Invalid JSON format.", - code: ImportErrorCode.INVALID_JSON, - }); - } - - const validationResult = WordsSchema.safeParse(dictionary); - - if (!validationResult.success) { - console.error( - "Error importing dictionary", - JSON.stringify(validationResult.error.errors, null, 2), - ); - - return res.status(400).json({ - error: validationResult.error, - code: ImportErrorCode.VALIDATION_ERROR, - } as ImportResponseError); - } - - const validatedDictionary = validationResult.data; - - // Add timestamps to any record that doesn't have it. - const preProcessedDictionary = validatedDictionary.map(addTimestamps); - - // The user's index has the same id as their user id - const userIndexId = req.user.id; - const index = meilisearchClient.index(userIndexId); - - try { - // Adds or replaces documents - const { taskUid: addTaskUid } = await index.addDocuments( - preProcessedDictionary, - ); - - const addDocumentsTask = await meilisearchClient.waitForTask(addTaskUid); - - if (addDocumentsTask.error) { - const error = addDocumentsTask.error; - - throw new MeilisearchError({ - message: error.message, - code: error.code, - type: error.type, - }); - } - } catch (error) { - console.error(error); - if (error instanceof MeilisearchError) { - return res.status(500).json({ code: error.code, type: error.type }); - } else { - return res.status(500).json({ code: ErrorCode.UNKNOWN_ERROR }); - } - } - - return res.status(200).end(); - }, -); - -dictionaryRouter.post("/dictionary/export", auth, async (req, res) => { - const userIndexId = req.user.id; - const index = meilisearchClient.index(userIndexId); - - const shouldExportWithFlashcards = req.body?.includeFlashcards ?? false; - - const fieldsToExport = JSON_SCHEMA_FIELDS.filter((field) => - !shouldExportWithFlashcards - ? field !== "flashcard" && - field !== "flashcard_reverse" && - // When flashcards are included in an export, that implies the file is a backup. - // So we only want to include timestamps in backups, not shared dictionaries. - field !== "created_at" && - field !== "created_at_timestamp" && - field !== "updated_at" && - field !== "updated_at_timestamp" - : true, - ); - - const limit = 1000; - const allDocuments = []; - - let offset = 0; - - try { - let hasMoreDocuments = true; - - do { - const { results, total } = await index.getDocuments({ - offset, - limit, - fields: fieldsToExport, - }); - - if (results.length > 0 && results.length <= total) { - allDocuments.push(...results); - - offset += limit; - } else { - hasMoreDocuments = false; // No more documents to fetch - } - } while (hasMoreDocuments); - - // Set headers for downloading the file - res.setHeader("Content-Type", "application/json"); - res.setHeader("Content-Disposition", "attachment; filename=data.json"); - - return res.status(200).json(allDocuments); - } catch (error) { - console.error("Error exporting dictionary:", error); - - return res.status(500).json({ error: "Error exporting dictionary" }); - } -}); - -dictionaryRouter.delete("/dictionary", auth, async (req, res) => { - const userIndexId = req.user.id; - const index = meilisearchClient.index(userIndexId); - - const { taskUid: deleteTaskUid } = await index.deleteAllDocuments(); - - const deleteTask = await meilisearchClient.waitForTask(deleteTaskUid); - - if (deleteTask.error) { - const error = deleteTask.error; - - return res.status(500).json({ code: error.code, type: error.type }); - } - - return res.status(200).end(); -}); - -export const trpcDictionaryRouter = router({ - find: protectedProcedure - .input(z.object({ id: z.string() })) - .output(DictionarySchema) - .query(async ({ ctx, input }) => { - const { user } = ctx; - - const userIndex = meilisearchClient.index(user.id); - - const word = (await userIndex.getDocument(input.id)) as z.infer< - typeof DictionarySchema - >; - - return word; - }), - deleteWord: protectedProcedure - .input(z.object({ id: z.string() })) - .mutation(async ({ ctx, input }) => { - const { user } = ctx; - - const userIndex = meilisearchClient.index(user.id); - - try { - const { taskUid } = await userIndex.deleteDocument(input.id); - - const { status, error } = await userIndex.waitForTask(taskUid); - - if (status === "failed" && error) { - const { message, code, type } = error; - - throw new MeilisearchError({ - message, - code, - type, - }); - } - - // TODO: add better return type - return true; - } catch (err) { - if (err instanceof MeilisearchError) { - console.error(err.code, err.type, err.message); - } else if (err instanceof Error) { - console.error(err.message); - } - - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "unexpected_error", - }); - } - }), - editWord: protectedProcedure - .input(DictionarySchema.deepPartial()) - .mutation(async ({ ctx, input }) => { - const { user } = ctx; - - const userIndex = meilisearchClient.index(user.id); - - const now = new Date(); - const updatedAt = now.toISOString(); - const updatedAtTimestamp = Math.floor(now.getTime() / 1000); - - ctx.logger.debug(input, "Updating dictionary entry..."); - - try { - const { taskUid } = await userIndex.updateDocuments([ - { - ...input, - updated_at: updatedAt, - updated_at_timestamp: updatedAtTimestamp, - }, - ]); - - const { status, error } = await userIndex.waitForTask(taskUid); - - if (status === "failed" && error) { - const { message, code, type } = error; - - throw new MeilisearchError({ - message, - code, - type, - }); - } - - // TODO: add better return type - return true; - } catch (err) { - if (err instanceof MeilisearchError) { - console.error(err.code, err.type, err.message); - } else if (err instanceof Error) { - console.error(err.message); - } - - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "unexpected_error", - }); - } - }), - addWord: protectedProcedure - .input(DictionarySchema.omit({ id: true })) - .mutation(async ({ ctx, input }) => { - const { user } = ctx; - - const userIndex = meilisearchClient.index(user.id); - - const now = new Date(); - const createdAt = now.toISOString(); - const createdAtTimestamp = Math.floor(now.getTime() / 1000); - const id = nanoid(); - - try { - const { taskUid } = await userIndex.addDocuments([ - { - ...input, - id, - created_at: createdAt, - created_at_timestamp: createdAtTimestamp, - updated_at: createdAt, - updated_at_timestamp: createdAtTimestamp, - }, - ]); - - const { status, error } = await userIndex.waitForTask(taskUid); - - if (status === "failed" && error) { - const { message, code, type } = error; - - throw new MeilisearchError({ - message, - code, - type, - }); - } - - // TODO: add better return type - return true; - } catch (err) { - if (err instanceof MeilisearchError) { - console.error(err.code, err.type, err.message); - } else if (err instanceof Error) { - console.error(err.message); - } - - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "unexpected_error", - }); - } - }), -}); - -const addTimestamps = (word: z.infer) => { - const now = new Date(); - const timestamp = Math.floor(now.getTime() / 1000); // seconds - const isoString = now.toISOString(); - - if (!word.created_at_timestamp || !word.created_at) { - word.created_at = now.toISOString(); - word.created_at_timestamp = Math.floor(now.getTime() / 1000); - } - - if (!word.updated_at_timestamp || !word.updated_at) { - word.updated_at = now.toISOString(); - word.updated_at_timestamp = Math.floor(now.getTime() / 1000); - } - - return { - ...word, - created_at: word.created_at ?? isoString, - created_at_timestamp: word.created_at_timestamp ?? timestamp, - updated_at: word.updated_at ?? isoString, - updated_at_timestamp: word.updated_at_timestamp ?? timestamp, - }; -}; diff --git a/apps/api/src/routers/flashcard.ts b/apps/api/src/routers/flashcard.ts deleted file mode 100644 index cb9f43ed..00000000 --- a/apps/api/src/routers/flashcard.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { router, protectedProcedure } from "../trpc"; -import { meilisearchClient } from "../clients/meilisearch"; -import { Card, createEmptyCard } from "ts-fsrs"; -import { z } from "zod"; -import { DictionarySchema } from "../schemas/dictionary.schema"; -import { FlashcardSchema } from "../schemas/flashcard.schema"; -import { SelectDecksSchema } from "../db/schema/decks"; -import { MultiSearchQueryWithFederation } from "meilisearch"; -import { JSON_SCHEMA_FIELDS } from "./dictionary"; - -export const FLASHCARD_LIMIT = 100; - -export enum FlashcardState { - NEW = 0, - LEARNING = 1, - REVIEW = 2, - RE_LEARNING = 3, -} - -export type Flashcard = Card & { - id: string; - due: string; - last_review: string | null; - due_timestamp: number; - last_review_timestamp: number | null; -}; - -const FilterSchema = SelectDecksSchema.pick({ filters: true }); - -const TodaySchema = FilterSchema.extend({ - filters: FilterSchema.shape.filters.optional(), - show_reverse: z.boolean().default(false).optional(), -}).optional(); - -export const flashcardRouter = router({ - today: protectedProcedure - .input(TodaySchema) - .output( - z.object({ - total_hits: z.number(), - flashcards: z.array( - z.object({ - flashcard: FlashcardSchema, - reverse: z.boolean(), - card: DictionarySchema.pick({ - id: true, - word: true, - type: true, - translation: true, - tags: true, - morphology: true, - definition: true, - examples: true, - root: true, - antonyms: true, - }), - }), - ), - }), - ) - .query(async ({ ctx, input }) => { - const { user } = ctx; - const { show_reverse, filters } = input ?? {}; - - const { flashcards: dictionaryWords, totalHits } = await queryFlashcards({ - user_id: user.id, - limit: FLASHCARD_LIMIT, - filters, - show_reverse, - }); - - return { - total_hits: totalHits, - flashcards: dictionaryWords.map( - ({ - id, - word, - type, - translation, - flashcard, - reverse, - morphology, - definition, - examples, - root, - tags, - antonyms, - }) => { - return { - flashcard: flashcard ?? getEmptyFlashcard(id), - reverse, - card: { - id, - word, - type, - translation, - morphology, - definition, - examples, - root, - tags, - antonyms, - }, - }; - }, - ), - }; - }), - - reset: protectedProcedure - .input(z.object({ id: z.string() })) - .mutation(async ({ ctx, input }) => { - const { user } = ctx; - - const { id } = input; - - const userIndex = meilisearchClient.index(user.id); - - const document = await userIndex.getDocument(id); - - const { taskUid } = await userIndex.addDocuments([ - { - ...document, - flashcard: undefined, - flashcard_reverse: undefined, - }, - ]); - - await meilisearchClient.index(user.id).waitForTask(taskUid); - - return { - id, - }; - }), - - update: protectedProcedure - .input( - z.object({ - id: z.string(), - reverse: z.boolean(), - flashcard: FlashcardSchema, - }), - ) - .mutation(async ({ ctx, input }) => { - const { user } = ctx; - - const { id, reverse, flashcard } = input; - - const flashcardFieldToUpdate = reverse - ? "flashcard_reverse" - : "flashcard"; - - const { taskUid } = await meilisearchClient - .index(user.id) - .updateDocuments([ - { - id, - [flashcardFieldToUpdate]: { - ...flashcard, - id, - }, - }, - ]); - - await meilisearchClient.index(user.id).waitForTask(taskUid); - - return { - id, - ...flashcard, - }; - }), -}); - -type OutputFlashcards = z.infer & { - reverse?: boolean; -}; - -export const queryFlashcards = async ({ - user_id, - fields, - show_only_today = true, - limit = 1000, - filters, - show_reverse = false, -}: { - user_id: string; - filters?: z.infer["filters"]; - fields?: string[]; - show_only_today?: boolean; - limit?: number; - show_reverse?: boolean; -}) => { - const types = filters?.types?.length - ? filters?.types.map((type) => { - if (type === "fi'l") { - return '"fi\'l"'; - } - - return type; - }) - : ["ism", '"fi\'l"', "harf", "expression"]; - - const tags = filters?.tags?.map((tag) => `"${tag}"`) ?? []; - - /** - * Current timestamp in seconds - */ - const now = Math.floor(new Date().getTime() / 1000); - - const fieldsToRetrieve = fields ?? JSON_SCHEMA_FIELDS; - - const queries: MultiSearchQueryWithFederation[] = [ - { - indexUid: user_id, - limit, - sort: ["flashcard.due_timestamp:asc"], - attributesToRetrieve: fieldsToRetrieve.filter( - (field) => field !== "flashcard_reverse", - ), - filter: [ - show_only_today - ? `flashcard.due_timestamp NOT EXISTS OR flashcard.due_timestamp <= ${now}` - : "", - `type IN [${types.join(", ")}]`, - tags?.length ? `tags IN [${tags.join(", ")}]` : "", - ], - }, - ...(show_reverse - ? [ - { - indexUid: user_id, - limit, - sort: ["flashcard_reverse.due_timestamp:asc"], - attributesToRetrieve: fieldsToRetrieve.filter( - (field) => field !== "flashcard", - ), - filter: [ - show_only_today - ? `flashcard_reverse.due_timestamp NOT EXISTS OR flashcard_reverse.due_timestamp <= ${now}` - : "", - `type IN [${types.join(", ")}]`, - tags?.length ? `tags IN [${tags.join(", ")}]` : "", - ], - }, - ] - : []), - ]; - - const [forwardFlashcardResults, reverseFlashcardResults] = ( - await meilisearchClient.multiSearch>({ - queries, - }) - ).results; - - const totalHits = - (forwardFlashcardResults?.estimatedTotalHits ?? 0) + - (reverseFlashcardResults?.estimatedTotalHits ?? 0); - - const allFlashcards = [ - ...forwardFlashcardResults.hits, - ...(reverseFlashcardResults?.hits?.map((f) => ({ ...f, reverse: true })) ?? - []), - ] - .sort((a, b) => { - const aTimestamp = - a.flashcard?.due_timestamp ?? a.flashcard_reverse?.due_timestamp; - - const bTimestamp = - b.flashcard?.due_timestamp ?? b.flashcard_reverse?.due_timestamp; - - // For sorting, we want to review overdue cards first rather than prioritizing - // new ones. - if (aTimestamp == null && bTimestamp == null) return 0; // maintain relative order of new cards - if (aTimestamp == null) return 1; // a goes later (is new) - if (bTimestamp == null) return -1; // b goes later (is new) - - return aTimestamp - bTimestamp; - }) - .slice(0, limit); - - const flashcards = allFlashcards as OutputFlashcards[]; - - /** - * An array of flashcards where both flashcards and reversed flashcards use the - * `flashcard` field to store the flashcard data but with an additional `reverse` field - * to indicate the direction of each. - */ - const normalizedFlashcards = flashcards.map((card) => { - const { flashcard, flashcard_reverse, ...fields } = card; - const isReverse = !!fields.reverse; - - return { - ...fields, - flashcard: isReverse ? flashcard_reverse : flashcard, - reverse: isReverse, - }; - }); - - return { - flashcards: normalizedFlashcards, - totalHits, - }; -}; - -const getEmptyFlashcard = (id: string): Flashcard => { - return createEmptyCard(new Date(), (c) => { - const due = new Date(c.due); - const last_review = c.last_review ? new Date(c.last_review) : null; - - const due_timestamp = Math.floor(due.getTime() / 1000); - const last_review_timestamp = last_review - ? Math.floor(last_review.getTime() / 1000) - : null; - - return { - ...c, - id, - due: due.toISOString(), - last_review: last_review?.toISOString() ?? null, - due_timestamp, - last_review_timestamp, - } as Flashcard; - }); -}; diff --git a/apps/api/src/routers/settings.ts b/apps/api/src/routers/settings.ts deleted file mode 100644 index d725656e..00000000 --- a/apps/api/src/routers/settings.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { router, protectedProcedure } from "../trpc"; -import { - SelectSettingsSchema, - settings, - InsertSettingsSchema, -} from "../db/schema/settings"; -import { db } from "../db"; -import { nanoid } from "nanoid"; -import { getUserDbClient } from "../clients/turso"; - -export const settingsRouter = router({ - get: protectedProcedure - .output(SelectSettingsSchema.nullable()) - .query(async ({ ctx }) => { - const { user } = ctx; - - const userDbClient = await getUserDbClient(user.id); - - if (!userDbClient) { - return null; - } - - const result = await userDbClient.execute( - "SELECT id, show_reverse_flashcards, show_antonyms_in_flashcard FROM settings LIMIT 1", - ); - - if (result.rows.length === 0) { - return null; - } - - const row = result.rows[0]; - return { - id: row.id as string, - user_id: user.id, - show_reverse_flashcards: Boolean(row.show_reverse_flashcards), - show_antonyms_in_flashcard: row.show_antonyms_in_flashcard as - | "hidden" - | "answer" - | "hint", - }; - }), - - update: protectedProcedure - .input(InsertSettingsSchema.omit({ id: true, user_id: true })) - .output(SelectSettingsSchema) - .mutation(async ({ ctx, input }) => { - const { user } = ctx; - - const results = await db - .insert(settings) - .values({ id: nanoid(), user_id: user.id, ...input }) - .onConflictDoUpdate({ target: settings.user_id, set: { ...input } }) - .returning(); - - const result = results[0]; - - return result; - }), -}); diff --git a/apps/api/src/routers/tags.ts b/apps/api/src/routers/tags.ts deleted file mode 100644 index bd0d4953..00000000 --- a/apps/api/src/routers/tags.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { router, protectedProcedure } from "../trpc"; -import { meilisearchClient } from "../clients/meilisearch"; -import { z } from "zod"; - -export const tagsRouter = router({ - search: protectedProcedure - .input( - z - .object({ - query: z.string().optional(), - filter: z.array(z.string()).optional(), - }) - .optional() - .default({}), - ) - .query(async ({ ctx, input }) => { - const { user } = ctx; - - const client = meilisearchClient.index(user.id); - - const results = await client.searchForFacetValues({ - facetName: "tags", - facetQuery: input.query, - filter: constructTagsFilter(input.filter), - }); - - return results; - }), -}); - -const constructTagsFilter = (filterTags?: string[]) => { - return `tags NOT IN [${filterTags?.join(", ") ?? ""}]`; -}; diff --git a/apps/api/src/schema.json b/apps/api/src/schema.json deleted file mode 100644 index b3df2eb1..00000000 --- a/apps/api/src/schema.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": { - "$ref": "dictionary.json" - } -} diff --git a/apps/api/src/schemas/deck.schema.ts b/apps/api/src/schemas/deck.schema.ts deleted file mode 100644 index fa08551e..00000000 --- a/apps/api/src/schemas/deck.schema.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { SelectDecksSchema } from "../db/schema/decks"; - -export const DeckSchema = SelectDecksSchema; diff --git a/apps/api/src/schemas/dictionary.schema.ts b/apps/api/src/schemas/dictionary.schema.ts deleted file mode 100644 index 0290d463..00000000 --- a/apps/api/src/schemas/dictionary.schema.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { z } from "zod"; - -export const DictionarySchema = z.object({ - id: z.string(), - created_at: z.string().datetime({ offset: true }).optional(), - created_at_timestamp: z - .number() - .int() - .describe( - "A UNIX timestamp in seconds that represents the same date as `created_at`.", - ) - .optional(), - updated_at: z.string().datetime({ offset: true }).optional(), - updated_at_timestamp: z - .number() - .int() - .describe( - "A UNIX timestamp in seconds that represents the same date as `updated_at`.", - ) - .optional(), - word: z.string().describe("A word or expression in Arabic."), - definition: z - .string() - .describe("The definition of the word in Arabic.") - .optional(), - translation: z.string().describe("The English translation of the word."), - type: z - .enum(["ism", "fi'l", "harf", "expression"]) - .describe("The type of the word.") - .optional(), - root: z - .array(z.string()) - .describe("An array of letters representing the root letters of the word.") - .optional(), - tags: z - .array(z.string()) - .describe( - "Any tags associated with the word. These can denote things like the context.", - ) - .optional(), - antonyms: z - .array(z.object({ word: z.string().optional() })) - .describe("Any antonyms of the word.") - .optional(), - examples: z - .array( - z.object({ - sentence: z.string().describe("A sentence using the word."), - context: z - .string() - .describe( - "The context of the sentence. This can refer to casual or formal contexts, for example.", - ) - .optional(), - translation: z - .string() - .describe("The English translation of the sentence.") - .optional(), - }), - ) - .describe("Examples using the word.") - .optional(), - morphology: z - .object({ - ism: z - .object({ - singular: z.string().optional(), - dual: z.string().optional(), - plurals: z - .array( - z.object({ - word: z.string(), - details: z - .string() - .describe( - "Any additional details about this form such as frequency of usage or context.", - ) - .optional(), - }), - ) - .describe( - "The (broken) plural forms of the word. Use harakat to indicate inflection.", - ) - .optional(), - gender: z.enum(["masculine", "feminine"]).optional(), - inflection: z - .enum(["indeclinable", "diptote", "triptote"]) - .describe( - "The inflection of the word. This refers to how many case endings the word can take. Three means it can take all three, two means it can only take raf' and nasb, and one means it is indeclinable.", - ) - .optional(), - }) - .describe( - "The morphological properties of an ism such as the gender and the different plural and singular forms.", - ) - .optional(), - verb: z - .object({ - huroof: z - .array( - z.object({ - harf: z.string(), - meaning: z - .string() - .describe( - "The meaning of the fi'l when used with the preposition. If it is the same as the base meaning, then leave this blank.", - ) - .optional(), - }), - ) - .describe( - "A list of the prepositions that can be used with the fi'l. Typically, if the meaning with the preposition is completely different, then it's better to create a separate dictionary entry.", - ) - .optional(), - past_tense: z.string().optional(), - present_tense: z.string().optional(), - active_participle: z.string().optional(), - passive_participle: z.string().optional(), - imperative: z.string().optional(), - masadir: z - .array( - z.object({ - word: z.string(), - details: z - .string() - .describe( - "Any additional details about this form such as frequency of usage or context.", - ) - .optional(), - }), - ) - .describe("A list of the possible verbal noun forms.") - .optional(), - form: z - .string() - .describe("The form or pattern of the verb from I - X.") - .optional(), - form_arabic: z - .string() - .describe( - "The form or pattern of the verb in Arabic using the root ف-ع-ل.", - ) - .optional(), - }) - .describe( - "The morphological properties of a fi'l such as the verb conjugations and noun forms.", - ) - .optional(), - }) - .describe("The morphological properties of the word.") - .optional(), - flashcard: z - .object({ - difficulty: z.number().gte(0).default(0), - due: z - .string() - .datetime({ offset: true }) - .describe( - "The date when the card needs to be reviewed. Set to the current time for new cards.", - ), - due_timestamp: z - .number() - .int() - .gte(0) - .describe( - "A UNIX timestamp in seconds that represents the same date as `due`.", - ), - elapsed_days: z.number().int().gte(0).default(0), - lapses: z.number().int().gte(0).default(0), - last_review: z - .union([z.string().datetime({ offset: true }), z.null()]) - .optional(), - last_review_timestamp: z - .union([ - z - .number() - .int() - .gte(0) - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ), - z - .null() - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ), - ]) - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ) - .optional(), - reps: z.number().int().gte(0).default(0), - scheduled_days: z.number().int().gte(0).default(0), - stability: z.number().gte(0).default(0), - state: z - .union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]) - .describe("0 is New, 1 is Learning, 2 is Review, and 3 is Relearning.") - .default(0), - }) - .describe( - "Properties used for scheduling the flashcards associated with the word using FSRS. When adding new words, just set these to the default values. For updating existing words, leave them as-is to keep flashcard progress or set to default values to reset it.", - ) - .optional(), - flashcard_reverse: z - .object({ - difficulty: z.number().gte(0).default(0), - due: z - .string() - .datetime({ offset: true }) - .describe( - "The date when the card needs to be reviewed. Set to the current time for new cards.", - ), - due_timestamp: z - .number() - .int() - .gte(0) - .describe( - "A UNIX timestamp in seconds that represents the same date as `due`.", - ), - elapsed_days: z.number().int().gte(0).default(0), - lapses: z.number().int().gte(0).default(0), - last_review: z - .union([z.string().datetime({ offset: true }), z.null()]) - .optional(), - last_review_timestamp: z - .union([ - z - .number() - .int() - .gte(0) - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ), - z - .null() - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ), - ]) - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ) - .optional(), - reps: z.number().int().gte(0).default(0), - scheduled_days: z.number().int().gte(0).default(0), - stability: z.number().gte(0).default(0), - state: z - .union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]) - .describe("0 is New, 1 is Learning, 2 is Review, and 3 is Relearning.") - .default(0), - }) - .describe( - "Properties used for scheduling the flashcards associated with the word using FSRS. When adding new words, just set these to the default values. For updating existing words, leave them as-is to keep flashcard progress or set to default values to reset it.", - ) - .optional(), -}); diff --git a/apps/api/src/schemas/flashcard.schema.ts b/apps/api/src/schemas/flashcard.schema.ts deleted file mode 100644 index 6064eb8c..00000000 --- a/apps/api/src/schemas/flashcard.schema.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { z } from "zod"; - -export const FlashcardSchema = z - .object({ - difficulty: z.number().gte(0).default(0), - due: z - .string() - .datetime({ offset: true }) - .describe( - "The date when the card needs to be reviewed. Set to the current time for new cards.", - ), - due_timestamp: z - .number() - .int() - .gte(0) - .describe( - "A UNIX timestamp in seconds that represents the same date as `due`.", - ), - elapsed_days: z.number().int().gte(0).default(0), - lapses: z.number().int().gte(0).default(0), - last_review: z - .union([z.string().datetime({ offset: true }), z.null()]) - .optional(), - last_review_timestamp: z - .union([ - z - .number() - .int() - .gte(0) - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ), - z - .null() - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ), - ]) - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ) - .optional(), - reps: z.number().int().gte(0).default(0), - scheduled_days: z.number().int().gte(0).default(0), - stability: z.number().gte(0).default(0), - state: z - .union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]) - .describe("0 is New, 1 is Learning, 2 is Review, and 3 is Relearning.") - .default(0), - }) - .describe( - "Properties used for scheduling the flashcards associated with the word using FSRS. When adding new words, just set these to the default values. For updating existing words, leave them as-is to keep flashcard progress or set to default values to reset it.", - ); diff --git a/apps/api/src/schemas/index.ts b/apps/api/src/schemas/index.ts deleted file mode 100644 index 56fe2702..00000000 --- a/apps/api/src/schemas/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { DeckSchema } from "./deck.schema"; -export { FilterSchema } from "../db/schema/decks"; -export { DictionarySchema } from "./dictionary.schema"; -export { FlashcardSchema } from "./flashcard.schema"; -export { WordsSchema } from "./words.schema"; diff --git a/apps/api/src/schemas/words.schema.ts b/apps/api/src/schemas/words.schema.ts deleted file mode 100644 index 8a252d60..00000000 --- a/apps/api/src/schemas/words.schema.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { z } from "zod"; - -export const WordsSchema = z.array( - z.object({ - id: z.string(), - created_at: z.string().datetime({ offset: true }).optional(), - created_at_timestamp: z - .number() - .int() - .describe( - "A UNIX timestamp in seconds that represents the same date as `created_at`.", - ) - .optional(), - updated_at: z.string().datetime({ offset: true }).optional(), - updated_at_timestamp: z - .number() - .int() - .describe( - "A UNIX timestamp in seconds that represents the same date as `updated_at`.", - ) - .optional(), - word: z.string().describe("A word or expression in Arabic."), - definition: z - .string() - .describe("The definition of the word in Arabic.") - .optional(), - translation: z.string().describe("The English translation of the word."), - type: z - .enum(["ism", "fi'l", "harf", "expression"]) - .describe("The type of the word.") - .optional(), - root: z - .array(z.string()) - .describe( - "An array of letters representing the root letters of the word.", - ) - .optional(), - tags: z - .array(z.string()) - .describe( - "Any tags associated with the word. These can denote things like the context.", - ) - .optional(), - antonyms: z - .array(z.object({ word: z.string().optional() })) - .describe("Any antonyms of the word.") - .optional(), - examples: z - .array( - z.object({ - sentence: z.string().describe("A sentence using the word."), - context: z - .string() - .describe( - "The context of the sentence. This can refer to casual or formal contexts, for example.", - ) - .optional(), - translation: z - .string() - .describe("The English translation of the sentence.") - .optional(), - }), - ) - .describe("Examples using the word.") - .optional(), - morphology: z - .object({ - ism: z - .object({ - singular: z.string().optional(), - dual: z.string().optional(), - plurals: z - .array( - z.object({ - word: z.string(), - details: z - .string() - .describe( - "Any additional details about this form such as frequency of usage or context.", - ) - .optional(), - }), - ) - .describe( - "The (broken) plural forms of the word. Use harakat to indicate inflection.", - ) - .optional(), - gender: z.enum(["masculine", "feminine"]).optional(), - inflection: z - .enum(["indeclinable", "diptote", "triptote"]) - .describe( - "The inflection of the word. This refers to how many case endings the word can take. Three means it can take all three, two means it can only take raf' and nasb, and one means it is indeclinable.", - ) - .optional(), - }) - .describe( - "The morphological properties of an ism such as the gender and the different plural and singular forms.", - ) - .optional(), - verb: z - .object({ - huroof: z - .array( - z.object({ - harf: z.string(), - meaning: z - .string() - .describe( - "The meaning of the fi'l when used with the preposition. If it is the same as the base meaning, then leave this blank.", - ) - .optional(), - }), - ) - .describe( - "A list of the prepositions that can be used with the fi'l. Typically, if the meaning with the preposition is completely different, then it's better to create a separate dictionary entry.", - ) - .optional(), - past_tense: z.string().optional(), - present_tense: z.string().optional(), - active_participle: z.string().optional(), - passive_participle: z.string().optional(), - imperative: z.string().optional(), - masadir: z - .array( - z.object({ - word: z.string(), - details: z - .string() - .describe( - "Any additional details about this form such as frequency of usage or context.", - ) - .optional(), - }), - ) - .describe("A list of the possible verbal noun forms.") - .optional(), - form: z - .string() - .describe("The form or pattern of the verb from I - X.") - .optional(), - form_arabic: z - .string() - .describe( - "The form or pattern of the verb in Arabic using the root ف-ع-ل.", - ) - .optional(), - }) - .describe( - "The morphological properties of a fi'l such as the verb conjugations and noun forms.", - ) - .optional(), - }) - .describe("The morphological properties of the word.") - .optional(), - flashcard: z - .object({ - difficulty: z.number().gte(0).default(0), - due: z - .string() - .datetime({ offset: true }) - .describe( - "The date when the card needs to be reviewed. Set to the current time for new cards.", - ), - due_timestamp: z - .number() - .int() - .gte(0) - .describe( - "A UNIX timestamp in seconds that represents the same date as `due`.", - ), - elapsed_days: z.number().int().gte(0).default(0), - lapses: z.number().int().gte(0).default(0), - last_review: z - .union([z.string().datetime({ offset: true }), z.null()]) - .optional(), - last_review_timestamp: z - .union([ - z - .number() - .int() - .gte(0) - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ), - z - .null() - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ), - ]) - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ) - .optional(), - reps: z.number().int().gte(0).default(0), - scheduled_days: z.number().int().gte(0).default(0), - stability: z.number().gte(0).default(0), - state: z - .union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]) - .describe( - "0 is New, 1 is Learning, 2 is Review, and 3 is Relearning.", - ) - .default(0), - }) - .describe( - "Properties used for scheduling the flashcards associated with the word using FSRS. When adding new words, just set these to the default values. For updating existing words, leave them as-is to keep flashcard progress or set to default values to reset it.", - ) - .optional(), - flashcard_reverse: z - .object({ - difficulty: z.number().gte(0).default(0), - due: z - .string() - .datetime({ offset: true }) - .describe( - "The date when the card needs to be reviewed. Set to the current time for new cards.", - ), - due_timestamp: z - .number() - .int() - .gte(0) - .describe( - "A UNIX timestamp in seconds that represents the same date as `due`.", - ), - elapsed_days: z.number().int().gte(0).default(0), - lapses: z.number().int().gte(0).default(0), - last_review: z - .union([z.string().datetime({ offset: true }), z.null()]) - .optional(), - last_review_timestamp: z - .union([ - z - .number() - .int() - .gte(0) - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ), - z - .null() - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ), - ]) - .describe( - "A UNIX timestamp in seconds that represents the same date as `last_review`, if present.", - ) - .optional(), - reps: z.number().int().gte(0).default(0), - scheduled_days: z.number().int().gte(0).default(0), - stability: z.number().gte(0).default(0), - state: z - .union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]) - .describe( - "0 is New, 1 is Learning, 2 is Review, and 3 is Relearning.", - ) - .default(0), - }) - .describe( - "Properties used for scheduling the flashcards associated with the word using FSRS. When adding new words, just set these to the default values. For updating existing words, leave them as-is to keep flashcard progress or set to default values to reset it.", - ) - .optional(), - }), -); diff --git a/apps/api/src/utils/config.ts b/apps/api/src/utils/config.ts index f4360c3b..f711efe0 100644 --- a/apps/api/src/utils/config.ts +++ b/apps/api/src/utils/config.ts @@ -2,7 +2,7 @@ import { z } from "zod"; const EnvironmentVariablesSchema = z.object({ NODE_ENV: z.enum(["development", "production"]).default("development"), - PORT: z.string().transform(Number).default("3000"), + PORT: z.string().default("3000").transform(Number), HOST: z.string().default("localhost"), /** @@ -35,9 +35,6 @@ const EnvironmentVariablesSchema = z.object({ TURSO_ORG_SLUG: z.string(), TURSO_DB_GROUP: z.string(), - MEILISEARCH_HOST: z.string().url(), - MEILISEARCH_API_KEY: z.string().min(1), - BETTER_AUTH_SECRET: z.string().min(1), SENTRY_DSN: z.string().min(1), diff --git a/apps/api/src/utils/error.ts b/apps/api/src/utils/error.ts deleted file mode 100644 index b85c07d9..00000000 --- a/apps/api/src/utils/error.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ZodError, z } from "zod"; -import { DictionarySchema } from "../schemas"; - -export enum ErrorCode { - // Meilisearch Error Codes - // https://www.meilisearch.com/docs/reference/errors/error_codes - INDEX_ALREADY_EXISTS = "index_already_exists", - - // Custom Error Codes - UNKNOWN_ERROR = "unknown_error", -} - -/** - * Error codes used when importing a dictionary. - */ -export enum ImportErrorCode { - INVALID_JSON = "invalid_json", - VALIDATION_ERROR = "validation_error", -} - -export class MeilisearchError extends Error { - code: string; - type: string; - - constructor({ - message, - code, - type, - }: { - message?: string; - code: string; - type: string; - }) { - super(message); - - this.code = code; - this.type = type; - } -} - -export type ImportResponseError = { - code: ImportErrorCode; - error: ZodError>; -}; diff --git a/apps/api/src/utils/index.ts b/apps/api/src/utils/index.ts index 83edac24..f027e2ac 100644 --- a/apps/api/src/utils/index.ts +++ b/apps/api/src/utils/index.ts @@ -1,6 +1,3 @@ -import path, { dirname } from "path"; -import { fileURLToPath } from "url"; -import $RefParser from "@apidevtools/json-schema-ref-parser"; import { decodeJwt } from "jose"; /** @@ -16,17 +13,6 @@ export const getAllowedDomains = (domains: string[]) => { }); }; -/** - * Bundles the entire dictionary schema into a single JSON. - */ -export const getFullSchema = async () => { - const __dirname = dirname(fileURLToPath(import.meta.url)); - const schemaPath = path.join(__dirname, "../schema.json"); - const schema = await $RefParser.bundle(schemaPath); - - return schema; -}; - export const isJwtExpired = (token: string): boolean => { try { const payload = decodeJwt(token); diff --git a/apps/mobile/.example.env b/apps/mobile/.example.env index 0310a5cc..24530bf6 100644 --- a/apps/mobile/.example.env +++ b/apps/mobile/.example.env @@ -1,3 +1 @@ EXPO_PUBLIC_API_BASE_URL="" -EXPO_PUBLIC_VITE_MEILISEARCH_API_URL="" -EXPO_PUBLIC_VITE_MEILISEARCH_API_KEY="" diff --git a/apps/mobile/package.json b/apps/mobile/package.json index e2e06edd..fc63ca74 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -14,12 +14,12 @@ "preset": "jest-expo" }, "dependencies": { - "@better-auth/expo": "1.2.10", + "@better-auth/expo": "1.4.9", "@digitalartlab/expo-plugin-localization": "^3.0.0", "@expo/vector-icons": "^15.0.3", "@formatjs/intl-locale": "^4.2.11", "@formatjs/intl-pluralrules": "^5.4.4", - "@hookform/resolvers": "^5.0.1", + "@hookform/resolvers": "^5.2.2", "@lingui/core": "^5.3.2", "@lingui/react": "^5.3.2", "@orama/orama": "^3.1.16", @@ -34,7 +34,7 @@ "@trpc/react-query": "^11.4.3", "@trpc/server": "^11.4.3", "@trpc/tanstack-react-query": "^11.4.3", - "better-auth": "1.2.10", + "better-auth": "1.4.9", "class-variance-authority": "^0.7.0", "date-fns": "^3.6.0", "expo": "~54.0.25", @@ -68,9 +68,9 @@ "react-native-web": "~0.21.2", "react-native-webview": "13.15.0", "sonner-native": "^0.21.0", - "ts-fsrs": "^4.5.1", + "ts-fsrs": "^5.2.3", "uniwind": "^1.2.2", - "zod": "^3.24.1" + "zod": "4.0.17" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -80,7 +80,6 @@ "@bahar/fsrs": "workspace:*", "@bahar/i18n": "workspace:*", "@bahar/result": "workspace:*", - "@bahar/schemas": "workspace:*", "@bahar/search": "workspace:*", "@lingui/babel-plugin-lingui-macro": "^5.3.2", "@lingui/cli": "^5.3.2", diff --git a/apps/mobile/src/app/(search)/(home)/add-word.tsx b/apps/mobile/src/app/(search)/(home)/add-word.tsx index b6703256..6744152c 100644 --- a/apps/mobile/src/app/(search)/(home)/add-word.tsx +++ b/apps/mobile/src/app/(search)/(home)/add-word.tsx @@ -23,7 +23,7 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { useForm, Controller, useFieldArray } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { FormSchema } from "@bahar/schemas"; +import { FormSchema } from "@/lib/schemas/dictionary"; import { errorMap } from "@/utils/zod"; import * as z from "zod"; import { useMutation } from "@tanstack/react-query"; @@ -31,11 +31,11 @@ import { toast } from "sonner-native"; import { t } from "@lingui/core/macro"; import { dictionaryEntriesTable } from "@/lib/db/operations/dictionary-entries"; import { addToSearchIndex } from "@/lib/search"; -import { queryClient, trpcClient } from "@/utils/trpc"; +import { queryClient } from "@/utils/trpc"; import { flashcardsTable } from "@/lib/db/operations/flashcards"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -z.setErrorMap(errorMap); +z.config({ customError: errorMap }); type FormData = z.infer; @@ -69,10 +69,7 @@ const Breadcrumbs = () => { Home - + Add word @@ -90,15 +87,9 @@ const BackButton = () => { return ( ); @@ -176,19 +167,10 @@ const SelectDropdown = ({ onPress={() => setIsOpen(!isOpen)} className="flex-row items-center justify-between px-3 py-2.5 rounded-md border border-input bg-background" > - - {value - ? options.find((o) => o.value === value)?.label - : placeholder} + + {value ? options.find((o) => o.value === value)?.label : placeholder} - + {isOpen && ( @@ -246,13 +228,6 @@ export default function AddWordScreen() { console.warn("Failed to create flashcards:", error); } - trpcClient.dictionary.addWord - .mutate({ - word: newEntry.word, - translation: newEntry.translation, - }) - .catch((error) => console.warn("Failed to sync to remote:", error)); - await queryClient.invalidateQueries({ queryKey: flashcardsTable.today.cacheOptions.queryKey, }); @@ -534,12 +509,7 @@ export default function AddWordScreen() { onPress={() => removeExample(index)} className="p-1" > - + @@ -578,10 +548,7 @@ export default function AddWordScreen() { appendExample({ sentence: "", translation: "" }) } > - + Add Example @@ -620,12 +587,7 @@ export default function AddWordScreen() { onPress={() => removeAntonym(index)} className="p-2" > - + ))} @@ -634,10 +596,7 @@ export default function AddWordScreen() { size="sm" onPress={() => appendAntonym({ word: "" })} > - + Add Antonym @@ -726,12 +685,7 @@ export default function AddWordScreen() { onPress={() => removePlural(index)} className="p-2" > - + ))} @@ -740,10 +694,7 @@ export default function AddWordScreen() { size="sm" onPress={() => appendPlural({ word: "" })} > - + Add Plural @@ -941,12 +892,7 @@ export default function AddWordScreen() { onPress={() => removeMasdar(index)} className="p-2" > - + ))} @@ -955,10 +901,7 @@ export default function AddWordScreen() { size="sm" onPress={() => appendMasdar({ word: "" })} > - + Add Masdar @@ -983,12 +926,7 @@ export default function AddWordScreen() { onPress={() => removeHarf(index)} className="p-1" > - + @@ -1029,10 +967,7 @@ export default function AddWordScreen() { size="sm" onPress={() => appendHarf({ harf: "", meaning: "" })} > - + Add Harf diff --git a/apps/mobile/src/app/(search)/(home)/edit-word/[id].tsx b/apps/mobile/src/app/(search)/(home)/edit-word/[id].tsx index 28f253cd..4135215d 100644 --- a/apps/mobile/src/app/(search)/(home)/edit-word/[id].tsx +++ b/apps/mobile/src/app/(search)/(home)/edit-word/[id].tsx @@ -24,7 +24,7 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { useForm, Controller, useFieldArray } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { FormSchema } from "@bahar/schemas"; +import { FormSchema } from "@/lib/schemas/dictionary"; import { errorMap } from "@/utils/zod"; import * as z from "zod"; import { useMutation, useQuery } from "@tanstack/react-query"; @@ -35,7 +35,7 @@ import { updateSearchIndex, removeFromSearchIndex } from "@/lib/search"; import { queryClient } from "@/utils/trpc"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -z.setErrorMap(errorMap); +z.config({ customError: errorMap }); type FormData = z.infer; diff --git a/apps/mobile/src/components/flashcards/FlashcardReview.tsx b/apps/mobile/src/components/flashcards/FlashcardReview.tsx index d4d1224f..dbedf83e 100644 --- a/apps/mobile/src/components/flashcards/FlashcardReview.tsx +++ b/apps/mobile/src/components/flashcards/FlashcardReview.tsx @@ -36,7 +36,6 @@ import { decksTable } from "../../lib/db/operations/decks"; import { FlashcardCard } from "./FlashcardCard"; import { GradeButtons, ShowAnswerButton } from "./GradeButtons"; import { GradeFeedback } from "./GradeFeedback"; -import { trpcClient } from "../../utils/trpc"; import type { SelectDeck } from "@bahar/drizzle-user-db-schemas"; interface FlashcardReviewProps { @@ -94,15 +93,6 @@ export const FlashcardReview: React.FC = ({ }, }); - // Mutation for API sync (dual write) - const updateFlashcardRemote = async (flashcard: Parameters[0]) => { - try { - await trpcClient.flashcard.update.mutate(flashcard); - } catch (error) { - console.warn("Failed to sync flashcard to remote:", error); - } - }; - const currentCard = cards[0] ?? null; const totalHits = cards.length; // Use initialHasMore to prevent flickering when cards are graded @@ -189,28 +179,6 @@ export const FlashcardReview: React.FC = ({ id: currentCard.id, updates, }); - - // Sync to remote (fire and forget) - const selectedCard = schedulingCards[grade].card; - updateFlashcardRemote({ - flashcard: { - due: selectedCard.due.toISOString(), - last_review: selectedCard.last_review?.toISOString() ?? null, - due_timestamp: Math.floor(selectedCard.due.getTime() / 1000), - last_review_timestamp: selectedCard.last_review - ? Math.floor(selectedCard.last_review.getTime() / 1000) - : null, - difficulty: selectedCard.difficulty, - stability: selectedCard.stability, - reps: selectedCard.reps, - lapses: selectedCard.lapses, - elapsed_days: selectedCard.elapsed_days, - scheduled_days: selectedCard.scheduled_days, - state: selectedCard.state, - }, - id: currentCard.id, - reverse: currentCard.direction === "reverse", - }); }, [schedulingCards, currentCard, pendingGrade, scheduler, updateFlashcardLocal]); // Loading state diff --git a/packages/schemas/src/dictionary.ts b/apps/mobile/src/lib/schemas/dictionary.ts similarity index 99% rename from packages/schemas/src/dictionary.ts rename to apps/mobile/src/lib/schemas/dictionary.ts index b1d4cd38..5aad2a86 100644 --- a/packages/schemas/src/dictionary.ts +++ b/apps/mobile/src/lib/schemas/dictionary.ts @@ -75,4 +75,4 @@ export const FormSchema = z.object({ .optional(), }) .optional(), -}); \ No newline at end of file +}); diff --git a/apps/mobile/src/utils/zod.ts b/apps/mobile/src/utils/zod.ts index abe29bfe..539bf81d 100644 --- a/apps/mobile/src/utils/zod.ts +++ b/apps/mobile/src/utils/zod.ts @@ -1,55 +1,46 @@ import { plural, t } from "@lingui/core/macro"; -import z from "zod"; +import { z } from "zod/v4"; -// TODO: copied over from web client because upgrading to zod v4 -// is causing countless other type issues across the stack. -// refactor this to reuse configuration between web, mobile, and api. -export const errorMap: z.ZodErrorMap = (issue, ctx) => { - if (issue.code === z.ZodIssueCode.invalid_type) { +export const errorMap: z.core.$ZodErrorMap = (issue) => { + if (issue.code === "invalid_type") { if (issue.expected === "string") { - return { message: t`This field is required.` }; + return t`This field is required.`; } } - if (issue.code === z.ZodIssueCode.invalid_string) { - if (issue.validation === "email") { - return { message: t`That email is invalid.` }; + if (issue.code === "invalid_format") { + if (issue.format === "email") { + return t`That email is invalid.`; } } - if (issue.code === z.ZodIssueCode.too_small) { + if (issue.code === "too_small") { const val = issue.minimum as number; if (issue.type === "string") { if (val === 1) { - return { message: t`This field is required.` }; + return t`This field is required.`; } - return { - message: plural(val, { - one: "Must be at least # character", - other: "Must be at least # characters", - }), - }; + return plural(val, { + one: "Must be at least # character", + other: "Must be at least # characters", + }); } } - if (issue.code === z.ZodIssueCode.too_big) { + if (issue.code === "too_big") { const val = issue.maximum as number; if (issue.type === "string") { - return { - message: plural(val, { - one: "Cannot exceed # character", - other: "Cannot exceed # characters", - }), - }; + return plural(val, { + one: "Cannot exceed # character", + other: "Cannot exceed # characters", + }); } } - return { - message: ctx.defaultError, - }; + return undefined; }; -// Exporting zod here like in web just doesnt work. +z.config({ customError: errorMap }); diff --git a/apps/search/Dockerfile b/apps/search/Dockerfile deleted file mode 100644 index 5abc1f3f..00000000 --- a/apps/search/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM getmeili/meilisearch:v1.12.0 - -COPY ./apps/search/config.toml /etc/meilisearch/config.toml - -EXPOSE 7700 - -CMD ["meilisearch", "--config-file-path=/etc/meilisearch/config.toml"] - - diff --git a/apps/search/README.md b/apps/search/README.md deleted file mode 100644 index fe8fbd51..00000000 --- a/apps/search/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Bahar Search (Deprecated) - -**Note**: This directory is deprecated. The application has migrated to Orama for client-side full-text search. - -## Migration - -Previously, Bahar used Meilisearch as a centralized search service. The architecture has changed to use: - -- **Orama**: Client-side WASM-based full-text search engine -- **Location**: `/apps/web/src/lib/search/index.ts` -- **Data Source**: Local Turso database indexed on app initialization -- **Benefits**: No network latency for searches, works offline, reduced server load - -For search functionality, see the web app documentation. diff --git a/apps/search/config.toml b/apps/search/config.toml deleted file mode 100644 index 0db2bff0..00000000 --- a/apps/search/config.toml +++ /dev/null @@ -1,137 +0,0 @@ -# This file shows the default configuration of Meilisearch. -# All variables are defined here: https://www.meilisearch.com/docs/learn/configuration/instance_options#environment-variables - -# Designates the location where database files will be created and retrieved. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#database-path -db_path = "./data.ms" - -# Configures the instance's environment. Value must be either `production` or `development`. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#environment -env = "development" - -# The address on which the HTTP server will listen. -http_addr = "localhost:7700" - -# Sets the instance's master key, automatically protecting all routes except GET /health. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#master-key -# master_key = "YOUR_MASTER_KEY_VALUE" - -# Deactivates Meilisearch's built-in telemetry when provided. -# Meilisearch automatically collects data from all instances that do not opt out using this flag. -# All gathered data is used solely for the purpose of improving Meilisearch, and can be deleted at any time. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#disable-analytics -no_analytics = true - -# Sets the maximum size of accepted payloads. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#payload-limit-size -http_payload_size_limit = "100 MB" - -# Defines how much detail should be present in Meilisearch's logs. -# Meilisearch currently supports six log levels, listed in order of increasing verbosity: `OFF`, `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE` -# https://www.meilisearch.com/docs/learn/configuration/instance_options#log-level -log_level = "INFO" - -# Sets the maximum amount of RAM Meilisearch can use when indexing. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#max-indexing-memory -# max_indexing_memory = "2 GiB" - -# Sets the maximum number of threads Meilisearch can use during indexing. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#max-indexing-threads -# max_indexing_threads = 4 - -############# -### DUMPS ### -############# - -# Sets the directory where Meilisearch will create dump files. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#dump-directory -dump_dir = "dumps/" - -# Imports the dump file located at the specified path. Path must point to a .dump file. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#import-dump -# import_dump = "./path/to/my/file.dump" - -# Prevents Meilisearch from throwing an error when `import_dump` does not point to a valid dump file. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#ignore-missing-dump -ignore_missing_dump = false - -# Prevents a Meilisearch instance with an existing database from throwing an error when using `import_dump`. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#ignore-dump-if-db-exists -ignore_dump_if_db_exists = false - - -################# -### SNAPSHOTS ### -################# - -# Enables scheduled snapshots when true, disable when false (the default). -# If the value is given as an integer, then enables the scheduled snapshot with the passed value as the interval -# between each snapshot, in seconds. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#schedule-snapshot-creation -schedule_snapshot = false - -# Sets the directory where Meilisearch will store snapshots. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#snapshot-destination -snapshot_dir = "snapshots/" - -# Launches Meilisearch after importing a previously-generated snapshot at the given filepath. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#import-snapshot -# import_snapshot = "./path/to/my/snapshot" - -# Prevents a Meilisearch instance from throwing an error when `import_snapshot` does not point to a valid snapshot file. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#ignore-missing-snapshot -ignore_missing_snapshot = false - -# Prevents a Meilisearch instance with an existing database from throwing an error when using `import_snapshot`. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#ignore-snapshot-if-db-exists -ignore_snapshot_if_db_exists = false - - -########### -### SSL ### -########### - -# Enables client authentication in the specified path. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#ssl-authentication-path -# ssl_auth_path = "./path/to/root" - -# Sets the server's SSL certificates. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#ssl-certificates-path -# ssl_cert_path = "./path/to/certfile" - -# Sets the server's SSL key files. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#ssl-key-path -# ssl_key_path = "./path/to/private-key" - -# Sets the server's OCSP file. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#ssl-ocsp-path -# ssl_ocsp_path = "./path/to/ocsp-file" - -# Makes SSL authentication mandatory. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#ssl-require-auth -ssl_require_auth = false - -# Activates SSL session resumption. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#ssl-resumption -ssl_resumption = false - -# Activates SSL tickets. -# https://www.meilisearch.com/docs/learn/configuration/instance_options#ssl-tickets -ssl_tickets = false - -############################# -### Experimental features ### -############################# - -# Experimental metrics feature. For more information, see: -# Enables the Prometheus metrics on the `GET /metrics` endpoint. -experimental_enable_metrics = false - -# Experimental RAM reduction during indexing, do not use in production, see: -experimental_reduce_indexing_memory_usage = false - -# Defines whether logs should output human-readable text or JSON data. -# experimental_logs_mode="json" - -# Experimentally reduces the maximum number of tasks that will be processed at once, see: -# experimental_max_number_of_batched_tasks = 100 diff --git a/apps/search/example.env b/apps/search/example.env deleted file mode 100644 index 578ea324..00000000 --- a/apps/search/example.env +++ /dev/null @@ -1,6 +0,0 @@ -MEILI_LOG_LEVEL="INFO" -MEILI_LOGS_MODE="json" -MEILI_HTTP_ADDR="localhost:7700" -MEILI_ENV="development" -MEILI_MASTER_KEY="MASTER_KEY" -MEILI_DB_PATH="./data.ms" diff --git a/apps/search/fly.toml b/apps/search/fly.toml deleted file mode 100644 index 995a09e4..00000000 --- a/apps/search/fly.toml +++ /dev/null @@ -1,27 +0,0 @@ -# fly.toml app configuration file generated for bahar-search on 2025-01-04T20:37:04-05:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# - -app = 'bahar-search' -primary_region = 'yyz' - -[build] - -[http_service] - internal_port = 7700 - force_https = true - auto_stop_machines = 'off' - auto_start_machines = true - min_machines_running = 0 - processes = ['app'] - -[[vm]] - memory = '512mb' - cpu_kind = 'shared' - cpus = 1 - -[[mounts]] - source = "meili_data" - destination = "/meili_data" - initial_size = "1gb" diff --git a/apps/web/.example.env b/apps/web/.example.env index ebdc0ee8..b9145214 100644 --- a/apps/web/.example.env +++ b/apps/web/.example.env @@ -1,8 +1,5 @@ VITE_API_BASE_URL="http://localhost:3000" -VITE_MEILISEARCH_API_URL="http://localhost:7700" -VITE_MEILISEARCH_API_KEY="MASTER_KEY" - VITE_SENTRY_DSN="" VITE_SENTRY_ENV="local" diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile deleted file mode 100644 index a2d203c8..00000000 --- a/apps/web/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -FROM node:20-alpine AS base - -ARG PORT -ARG VITE_API_BASE_URL -ARG VITE_MEILISEARCH_API_KEY -ARG VITE_MEILISEARCH_API_URL - -ENV PORT $PORT -ENV VITE_API_BASE_URL $VITE_API_BASE_URL -ENV VITE_MEILISEARCH_API_KEY $VITE_MEILISEARCH_API_KEY -ENV VITE_MEILISEARCH_API_URL $VITE_MEILISEARCH_API_URL - -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" - -WORKDIR /app - -FROM base AS prune - -RUN npm install -g pnpm -RUN pnpm add -g turbo - -COPY . . - -RUN turbo prune web --docker - -FROM base AS development - -RUN npm install -g pnpm -RUN pnpm add -g turbo - -# First install the dependencies (as they change less often) -COPY .gitignore .gitignore -COPY --from=prune /app/out/json/ . -COPY --from=prune /app/out/pnpm-lock.yaml ./pnpm-lock.yaml - -RUN pnpm install --frozen-lockfile - -# Build the project -COPY --from=prune /app/out/full/ . -RUN pnpm turbo run build --filter=web - diff --git a/apps/web/env.ts b/apps/web/env.ts deleted file mode 100644 index 74e25c8f..00000000 --- a/apps/web/env.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig } from "@julr/vite-plugin-validate-env"; -import { z } from "zod"; - -export default defineConfig({ - validator: "zod", - schema: { - NODE_ENV: z.enum(["development", "production"]).default("development"), - PORT: z.string().transform(Number).default("4000"), - - VITE_API_BASE_URL: z.string().optional(), - - VITE_MEILISEARCH_API_URL: z.string().optional(), - VITE_MEILISEARCH_API_KEY: z.string().optional(), - - VITE_SENTRY_DSN: z.string().optional(), - VITE_SENTRY_ENV: z.enum(["local", "production"]).optional(), - }, -}); diff --git a/apps/web/package.json b/apps/web/package.json index 10709f6a..8a144bde 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,12 +11,11 @@ "wrangler:dev": "wrangler pages dev" }, "dependencies": { - "@hookform/resolvers": "^5.0.1", + "@hookform/resolvers": "^5.2.2", "@lingui/core": "^5.3.2", "@lingui/detect-locale": "^5.3.2", "@lingui/macro": "^5.3.2", "@lingui/react": "^5.3.2", - "@meilisearch/instant-meilisearch": "^0.16.0", "@orama/highlight": "^0.1.9", "@orama/orama": "^3.1.16", "@orama/plugin-qps": "^3.1.16", @@ -50,15 +49,15 @@ "@trpc/react-query": "^11.4.3", "@trpc/server": "^11.4.3", "@trpc/tanstack-react-query": "^11.4.3", - "@tursodatabase/sync-wasm": "^0.2.2", + "@tursodatabase/sync-wasm": "^0.3.2", "@uidotdev/usehooks": "^2.4.1", - "better-auth": "1.2.10", + "better-auth": "1.4.9", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "1.1.1", "date-fns": "^3.6.0", "dompurify": "^3.3.0", - "drizzle-orm": "^0.36.4", + "drizzle-orm": "^0.45.1", "input-otp": "1.4.2", "jotai": "^2.7.0", "lucide-react": "^0.507.0", @@ -66,20 +65,18 @@ "react": "19.0.0", "react-dom": "19.0.0", "react-hook-form": "^7.56.1", - "react-instantsearch": "^7.15.6", "tailwind-merge": "^2.2.1", - "ts-fsrs": "^4.5.1", + "ts-fsrs": "^5.2.3", "vaul": "^1.1.2", - "zod": "^3.24.1" + "zod": "4.0.17", + "@bahar/fsrs": "workspace:*" }, "devDependencies": { "@bahar/design-system": "workspace:*", "@bahar/drizzle-user-db-schemas": "workspace:*", "@bahar/eslint-config": "workspace:*", "@bahar/i18n": "workspace:*", - "@bahar/schemas": "workspace:*", "@bahar/typescript-config": "workspace:*", - "@julr/vite-plugin-validate-env": "^1.2.0", "@lingui/cli": "^5.3.2", "@lingui/conf": "^5.3.2", "@lingui/swc-plugin": "^5.5.2", @@ -100,6 +97,6 @@ "typescript": "^5.8.3", "vite": "^5.4.0", "vite-plugin-pwa": "^0.20.1", - "wrangler": "^3.91.0" + "wrangler": "^4.54.0" } } diff --git a/apps/web/src/components/features/decks/DeckDialogContent.tsx b/apps/web/src/components/features/decks/DeckDialogContent.tsx index d394aae8..9341e268 100644 --- a/apps/web/src/components/features/decks/DeckDialogContent.tsx +++ b/apps/web/src/components/features/decks/DeckDialogContent.tsx @@ -25,15 +25,12 @@ import { X } from "lucide-react"; import { Autocomplete } from "../../Autocomplete"; import { Label } from "../../ui/label"; import { Checkbox } from "../../ui/checkbox"; -import { trpc } from "@/lib/trpc"; import { useToast } from "@/hooks/useToast"; import { queryClient } from "@/lib/query"; -import { getQueryKey } from "@trpc/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation } from "@tanstack/react-query"; import { decksTable } from "@/lib/db/operations/decks"; -// TODO: reuse schema from the api const DeckSchema = z.object({ id: z.string(), name: z.string(), @@ -47,7 +44,6 @@ const DeckSchema = z.object({ .optional(), types: z.array(z.enum(["ism", "fi'l", "harf", "expression"])).optional(), }) - // API has this as object | null type even though we use optional .nullable(), total_hits: z.number(), to_review: z.number(), @@ -115,22 +111,7 @@ export const DeckDialogContent = ({ }) => { const allTypes: Types[] = ["ism", "fi'l", "harf", "expression"]; - const { mutateAsync: createDeck } = trpc.decks.create.useMutation({ - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: [...getQueryKey(trpc.decks.list), { type: "query" }], - }); - }, - }); - const { mutateAsync: updateDeck } = trpc.decks.update.useMutation({ - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: [...getQueryKey(trpc.decks.list), { type: "query" }], - }); - }, - }); - - const { mutateAsync: createDeckLocal } = useMutation({ + const { mutateAsync: createDeck } = useMutation({ mutationFn: decksTable.create.mutation, onSuccess: async () => { await queryClient.invalidateQueries({ @@ -139,7 +120,7 @@ export const DeckDialogContent = ({ }, }); - const { mutateAsync: updateDeckLocal } = useMutation({ + const { mutateAsync: updateDeck } = useMutation({ mutationFn: decksTable.update.mutation, onSuccess: async () => { await queryClient.invalidateQueries({ @@ -176,29 +157,16 @@ export const DeckDialogContent = ({ try { if (isEditing) { - await Promise.all([ - updateDeck({ - id: deck.id, - name, - filters, - }), - updateDeckLocal({ - id: deck.id, - updates: { name, filters }, - }), - ]); + await updateDeck({ + id: deck.id, + updates: { name, filters }, + }); toast({ title: t`Deck successfully updated!`, }); } else { - await Promise.all([ - createDeck({ - name, - filters, - }), - createDeckLocal({ deck: { name, filters } }), - ]); + await createDeck({ deck: { name, filters } }); toast({ title: t`Deck successfully created!`, diff --git a/apps/web/src/components/features/dictionary/add/AdditionalDetailsFormSection.tsx b/apps/web/src/components/features/dictionary/add/AdditionalDetailsFormSection.tsx index 8b99315b..adbaf0d0 100644 --- a/apps/web/src/components/features/dictionary/add/AdditionalDetailsFormSection.tsx +++ b/apps/web/src/components/features/dictionary/add/AdditionalDetailsFormSection.tsx @@ -24,7 +24,7 @@ import { import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { z } from "@/lib/zod"; -import { FormSchema } from "@bahar/schemas"; +import { FormSchema } from "@/lib/schemas/dictionary"; import { AnimatePresence, motion } from "motion/react"; import { Plus } from "lucide-react"; import { useFieldArray, useFormContext } from "react-hook-form"; diff --git a/apps/web/src/components/features/dictionary/add/BasicDetailsFormSection.tsx b/apps/web/src/components/features/dictionary/add/BasicDetailsFormSection.tsx index bcd5834a..12d7570e 100644 --- a/apps/web/src/components/features/dictionary/add/BasicDetailsFormSection.tsx +++ b/apps/web/src/components/features/dictionary/add/BasicDetailsFormSection.tsx @@ -10,7 +10,7 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { z } from "@/lib/zod"; -import { FormSchema } from "@bahar/schemas"; +import { FormSchema } from "@/lib/schemas/dictionary"; import { useFormContext } from "react-hook-form"; export const BasicDetailsFormSection = () => { diff --git a/apps/web/src/components/features/dictionary/add/CategoryFormSection.tsx b/apps/web/src/components/features/dictionary/add/CategoryFormSection.tsx index ec561945..f38038e5 100644 --- a/apps/web/src/components/features/dictionary/add/CategoryFormSection.tsx +++ b/apps/web/src/components/features/dictionary/add/CategoryFormSection.tsx @@ -27,7 +27,7 @@ import { } from "@/components/ui/select"; import { useDir } from "@/hooks/useDir"; import { z } from "@/lib/zod"; -import { FormSchema } from "@bahar/schemas"; +import { FormSchema } from "@/lib/schemas/dictionary"; import { useLingui } from "@lingui/react/macro"; import { InfoIcon } from "lucide-react"; import { useFormContext } from "react-hook-form"; diff --git a/apps/web/src/components/features/dictionary/add/IsmMorphologyCardSection.tsx b/apps/web/src/components/features/dictionary/add/IsmMorphologyCardSection.tsx index 532a6a09..d2c5791f 100644 --- a/apps/web/src/components/features/dictionary/add/IsmMorphologyCardSection.tsx +++ b/apps/web/src/components/features/dictionary/add/IsmMorphologyCardSection.tsx @@ -30,7 +30,7 @@ import { } from "@/components/ui/select"; import { useDir } from "@/hooks/useDir"; import { z } from "@/lib/zod"; -import { FormSchema, Inflection } from "@bahar/schemas"; +import { FormSchema, Inflection } from "@/lib/schemas/dictionary"; import { useLingui } from "@lingui/react/macro"; import { Plus, InfoIcon } from "lucide-react"; import { useFieldArray, useFormContext } from "react-hook-form"; diff --git a/apps/web/src/components/features/dictionary/add/MorphologyFormSection.tsx b/apps/web/src/components/features/dictionary/add/MorphologyFormSection.tsx index d77762f2..079b2bed 100644 --- a/apps/web/src/components/features/dictionary/add/MorphologyFormSection.tsx +++ b/apps/web/src/components/features/dictionary/add/MorphologyFormSection.tsx @@ -13,7 +13,7 @@ import { } from "@/components/ui/popover"; import { AnimatePresence, motion } from "motion/react"; import { useFormContext } from "react-hook-form"; -import { FormSchema } from "@bahar/schemas"; +import { FormSchema } from "@/lib/schemas/dictionary"; import { z } from "@/lib/zod"; import { useIsMobile } from "@/hooks/useIsMobile"; import { IsmMorphologyCardSection } from "./IsmMorphologyCardSection"; diff --git a/apps/web/src/components/features/dictionary/add/TagsFormSection.tsx b/apps/web/src/components/features/dictionary/add/TagsFormSection.tsx index 0ba39672..7f73491b 100644 --- a/apps/web/src/components/features/dictionary/add/TagsFormSection.tsx +++ b/apps/web/src/components/features/dictionary/add/TagsFormSection.tsx @@ -9,7 +9,7 @@ import { CardTitle, } from "@/components/ui/card"; import { z } from "@/lib/zod"; -import { FormSchema } from "@bahar/schemas"; +import { FormSchema } from "@/lib/schemas/dictionary"; import { X } from "lucide-react"; import { useFieldArray, useFormContext } from "react-hook-form"; diff --git a/apps/web/src/components/features/dictionary/add/VerbMorphologyCardSection.tsx b/apps/web/src/components/features/dictionary/add/VerbMorphologyCardSection.tsx index 4288535f..f05075ff 100644 --- a/apps/web/src/components/features/dictionary/add/VerbMorphologyCardSection.tsx +++ b/apps/web/src/components/features/dictionary/add/VerbMorphologyCardSection.tsx @@ -18,7 +18,7 @@ import { import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { z } from "@/lib/zod"; -import { FormSchema } from "@bahar/schemas"; +import { FormSchema } from "@/lib/schemas/dictionary"; import { Plus } from "lucide-react"; import { useFieldArray, useFormContext } from "react-hook-form"; diff --git a/apps/web/src/components/features/flashcards/FlashcardDrawer.tsx b/apps/web/src/components/features/flashcards/FlashcardDrawer/FlashcardDrawer.tsx similarity index 51% rename from apps/web/src/components/features/flashcards/FlashcardDrawer.tsx rename to apps/web/src/components/features/flashcards/FlashcardDrawer/FlashcardDrawer.tsx index ce98204b..423902ea 100644 --- a/apps/web/src/components/features/flashcards/FlashcardDrawer.tsx +++ b/apps/web/src/components/features/flashcards/FlashcardDrawer/FlashcardDrawer.tsx @@ -1,10 +1,9 @@ -import { t } from "@lingui/core/macro"; import { Plural, Trans } from "@lingui/react/macro"; import { useFormatNumber } from "@/hooks/useFormatNumber"; -import { Tooltip, TooltipTrigger, TooltipContent } from "../../ui/tooltip"; -import { Button } from "../../ui/button"; -import { intlFormatDistance } from "date-fns"; -import { Card, fsrs, Grade, Rating } from "ts-fsrs"; +import { Tooltip, TooltipTrigger, TooltipContent } from "../../../ui/tooltip"; +import { Button } from "../../../ui/button"; +import { toFsrsCard } from "@bahar/fsrs"; +import { fsrs, Grade, Rating } from "ts-fsrs"; import { Drawer, DrawerTrigger, @@ -13,7 +12,7 @@ import { DrawerTitle, DrawerDescription, DrawerFooter, -} from "../../ui/drawer"; +} from "../../../ui/drawer"; import { FC, PropsWithChildren, @@ -23,15 +22,12 @@ import { useRef, useState, } from "react"; -import { RouterOutput, trpc } from "@/lib/trpc"; import { queryClient } from "@/lib/query"; import { motion, AnimatePresence } from "motion/react"; -import { useDir } from "@/hooks/useDir"; -import { Badge } from "../../ui/badge"; -import { QuestionSide } from "./QuestionSide"; -import { AnswerSide } from "./AnswerSide"; -import { ReverseAnswerSide } from "./ReverseAnswerSide"; -import { ReverseQuestionSide } from "./ReverseQuestionSide"; +import { QuestionSide } from "../QuestionSide"; +import { AnswerSide } from "../AnswerSide"; +import { ReverseAnswerSide } from "../ReverseAnswerSide"; +import { ReverseQuestionSide } from "../ReverseQuestionSide"; import { useQuery, useMutation } from "@tanstack/react-query"; import { DEFAULT_BACKLOG_THRESHOLD_DAYS, @@ -40,53 +36,12 @@ import { FlashcardQueue, } from "@/lib/db/operations/flashcards"; import { decksTable } from "@/lib/db/operations/decks"; -import { - FlashcardState, - SelectDeck, - SelectFlashcard, -} from "@bahar/drizzle-user-db-schemas"; +import { SelectDeck } from "@bahar/drizzle-user-db-schemas"; import { cn } from "@bahar/design-system"; -import { - RotateCcw, - Brain, - ThumbsUp, - Zap, - Sparkles, - PartyPopper, - Archive, -} from "lucide-react"; - -const convertFlashcardToFsrsCard = ( - flashcard: SelectFlashcard, -): Card & { id: string } => { - return { - ...flashcard, - due: new Date(flashcard.due), - stability: flashcard.stability ?? 0, - difficulty: flashcard.difficulty ?? 0, - elapsed_days: flashcard.elapsed_days ?? 0, - scheduled_days: flashcard.scheduled_days ?? 0, - reps: flashcard.reps ?? 0, - lapses: flashcard.lapses ?? 0, - state: flashcard.state ?? FlashcardState.NEW, - last_review: flashcard.last_review - ? new Date(flashcard.last_review) - : undefined, - }; -}; - -const getTranslatedType = (str: "ism" | "fi'l" | "harf" | "expression") => { - switch (str) { - case "ism": - return t`Noun`; - case "fi'l": - return t`Verb`; - case "harf": - return t`Preposition`; - case "expression": - return t`Expression`; - } -}; +import { Brain, Sparkles, PartyPopper, Archive } from "lucide-react"; +import { GradeOption } from "./GradeOption"; +import { GradeFeedback } from "./GradeFeedback"; +import { TagBadgesList } from "./TagBadgesList"; interface FlashcardDrawerProps extends PropsWithChildren { filters?: SelectDeck["filters"]; @@ -96,175 +51,6 @@ interface FlashcardDrawerProps extends PropsWithChildren { queueCounts?: { regular: number; backlog: number }; } -export type Flashcard = RouterOutput["flashcard"]["today"]["flashcards"][0]; - -const GradeFeedback: FC<{ - grade: Grade | null; - onComplete: () => void; -}> = ({ grade, onComplete }) => { - useEffect(() => { - if (grade === null) return; - - const timer = setTimeout(onComplete, 600); - return () => clearTimeout(timer); - }, [grade, onComplete]); - - if (grade === null) return null; - - const feedbackConfig = { - [Rating.Again]: { - icon: RotateCcw, - color: "text-muted-foreground", - bgColor: "bg-muted/20", - animation: { - initial: { scale: 0, rotate: 0 }, - animate: { - scale: [0, 1.2, 1], - rotate: [0, -10, 10, -10, 0], - }, - transition: { duration: 0.5 }, - }, - }, - [Rating.Hard]: { - icon: Brain, - color: "text-orange-500", - bgColor: "bg-orange-500/10", - animation: { - initial: { scale: 0 }, - animate: { - scale: [0, 1.3, 1], - opacity: [0, 1, 1], - }, - transition: { duration: 0.5, ease: "easeOut" }, - }, - }, - [Rating.Good]: { - icon: ThumbsUp, - color: "text-primary", - bgColor: "bg-primary/10", - animation: { - initial: { scale: 0, y: 10, opacity: 0 }, - animate: { - scale: 1, - y: 0, - opacity: 1, - }, - transition: { - duration: 0.4, - type: "spring", - stiffness: 200, - damping: 15, - }, - }, - }, - [Rating.Easy]: { - icon: Zap, - color: "text-green-500", - bgColor: "bg-green-500/10", - animation: { - initial: { scale: 0, rotate: -20 }, - animate: { - scale: [0, 1.4, 1], - rotate: [-20, 10, 0], - }, - transition: { duration: 0.4, ease: "easeOut" }, - }, - }, - }; - - const config = feedbackConfig[grade]; - const Icon = config.icon; - - return ( - - {/* Background pulse */} - - - {/* Icon animation */} - - - - - {/* Sparkles for Easy */} - {grade === Rating.Easy && ( - <> - {[...Array(6)].map((_, i) => ( - - ))} - - )} - - ); -}; - -const TagBadgesList: FC<{ - currentCard: FlashcardWithDictionaryEntry; -}> = ({ currentCard }) => { - return ( - - {!!currentCard.dictionary_entry.type && ( - - {getTranslatedType(currentCard.dictionary_entry.type)} - - )} - {currentCard.dictionary_entry.tags?.map((tag, index) => ( - - - {tag} - - - ))} - - ); -}; - export const FlashcardDrawer: FC = ({ children, filters = {}, @@ -272,8 +58,6 @@ export const FlashcardDrawer: FC = ({ initialQueue = "regular", queueCounts, }) => { - const dir = useDir(); - const locale = dir === "rtl" ? "ar-u-nu-arab" : "en"; const { formatNumber } = useFormatNumber(); const [showAnswer, setShowAnswer] = useState(false); const [pendingGrade, setPendingGrade] = useState(null); @@ -319,54 +103,8 @@ export const FlashcardDrawer: FC = ({ selectedQueue, ], }); - const { mutateAsync: updateFlashcard } = trpc.flashcard.update.useMutation({ - // TODO: Remove this with dual write logic - // onMutate: async (updatedCard) => { - // const todayQueryKey = getQueryKey( - // trpc.flashcard.today, - // { filters, show_reverse }, - // "query", - // ); - // - // await queryClient.cancelQueries({ - // queryKey: todayQueryKey, - // exact: false, - // }); - // - // // TODO: when you have 101 cards, grading the next one will cause the - // // number to be off by one until the server response comes back. - // // This is due to the optimistic update. - // - // queryClient.setQueryData(todayQueryKey, (old: typeof data) => ({ - // total_hits: (old?.length ?? 0) - 1, - // flashcards: - // old?.filter( - // (card) => - // card.id !== updatedCard.id || - // (card.direction === "reverse") !== updatedCard.reverse, - // ) ?? [], - // })); - // }, - // onSettled: async () => { - // const todayQueryKey = getQueryKey( - // trpc.flashcard.today, - // undefined, - // "query", - // ); - // const deckListQueryKey = getQueryKey(trpc.decks.list, undefined, "query"); - // - // await queryClient.invalidateQueries({ - // queryKey: deckListQueryKey, - // }); - // - // await queryClient.invalidateQueries({ - // queryKey: todayQueryKey, - // exact: false, - // }); - // }, - }); - const { mutateAsync: updateFlashcardLocal } = useMutation({ + const { mutateAsync: updateFlashcard } = useMutation({ mutationFn: flashcardsTable.update.mutation, onSuccess: () => { queryClient.invalidateQueries({ @@ -396,39 +134,23 @@ export const FlashcardDrawer: FC = ({ const schedulingData = useMemo(() => { if (!currentCard) return null; const now = new Date(); - const scheduling_cards = f.repeat( - convertFlashcardToFsrsCard(currentCard), - now, - ); - return { scheduling_cards, now }; + const schedulingCards = f.repeat(toFsrsCard(currentCard), now); + return { schedulingCards, now }; }, [currentCard, f]); - const scheduling_cards = schedulingData?.scheduling_cards; + const schedulingCards = schedulingData?.schedulingCards; const now = schedulingData?.now ?? new Date(); const executeGrade = useCallback( async (grade: Grade) => { - if (!scheduling_cards || !currentCard) return; + if (!schedulingCards || !currentCard) return; - const selectedCard = scheduling_cards[grade].card; - const dueTimestamp = Math.floor(selectedCard.due.getTime() / 1000); + const selectedCard = schedulingCards[grade].card; const dueTimestampMs = selectedCard.due.getTime(); - const lastReviewTimestamp = selectedCard?.last_review - ? Math.floor(selectedCard.last_review.getTime() / 1000) - : null; const lastReviewTimestampMs = selectedCard?.last_review ? selectedCard.last_review.getTime() : null; - const newCard = { - ...selectedCard, - id: currentCard.id, - due: selectedCard.due.toISOString(), - last_review: selectedCard?.last_review?.toISOString() ?? null, - due_timestamp: dueTimestamp, - last_review_timestamp: lastReviewTimestamp, - }; - const localUpdates = { due: selectedCard.due.toISOString(), due_timestamp_ms: dueTimestampMs, @@ -446,23 +168,17 @@ export const FlashcardDrawer: FC = ({ setShowAnswer(false); setCards((prev) => prev.filter((c) => c.id !== currentCard.id)); - updateFlashcard({ - flashcard: newCard, - id: newCard.id, - reverse: currentCard.direction === "reverse", - }); - - await updateFlashcardLocal({ + await updateFlashcard({ id: currentCard.id, updates: localUpdates, }); }, - [currentCard, scheduling_cards, updateFlashcard, updateFlashcardLocal], + [currentCard, schedulingCards, updateFlashcard], ); const gradeCard = useCallback( (grade: Grade) => { - if (!scheduling_cards || !currentCard) return; + if (!schedulingCards || !currentCard) return; // Store the callback to execute after animation gradeCallbackRef.current = () => executeGrade(grade); @@ -470,7 +186,7 @@ export const FlashcardDrawer: FC = ({ // Show feedback animation setPendingGrade(grade); }, - [currentCard, scheduling_cards, executeGrade], + [currentCard, schedulingCards, executeGrade], ); const handleAnimationComplete = useCallback(() => { @@ -501,7 +217,6 @@ export const FlashcardDrawer: FC = ({ - {/* Grade feedback animation overlay */} {pendingGrade !== null && ( = ({ +

{selectedQueue === "regular" ? ( Cards due today or recently @@ -629,6 +345,7 @@ export const FlashcardDrawer: FC = ({ )} + Review your Arabic flashcards and grade your understanding with @@ -696,7 +413,7 @@ export const FlashcardDrawer: FC = ({ - {scheduling_cards && showAnswer ? ( + {schedulingCards && showAnswer ? ( = ({ transition={{ duration: 0.3 }} className="flex gap-2 sm:gap-4 items-stretch justify-center w-full max-w-lg mx-auto" > - {/* Again Button */} - - - - - {/* Hard Button */} - - - - - {/* Good Button */} - - - - - {/* Easy Button */} - - - + {( + [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy] as const + ).map((grade) => ( + gradeCard(grade)} + /> + ))} - ) : scheduling_cards ? ( + ) : schedulingCards ? ( void; +}> = ({ grade, onComplete }) => { + useEffect(() => { + if (grade === null) return; + + const timer = setTimeout(onComplete, 600); + return () => clearTimeout(timer); + }, [grade, onComplete]); + + if (grade === null) return null; + + const feedbackConfig = { + [Rating.Again]: { + icon: RotateCcw, + color: "text-muted-foreground", + bgColor: "bg-muted/20", + animation: { + initial: { scale: 0, rotate: 0 }, + animate: { + scale: [0, 1.2, 1], + rotate: [0, -10, 10, -10, 0], + }, + transition: { duration: 0.5 }, + }, + }, + [Rating.Hard]: { + icon: Brain, + color: "text-orange-500", + bgColor: "bg-orange-500/10", + animation: { + initial: { scale: 0 }, + animate: { + scale: [0, 1.3, 1], + opacity: [0, 1, 1], + }, + transition: { duration: 0.5, ease: "easeOut" }, + }, + }, + [Rating.Good]: { + icon: ThumbsUp, + color: "text-primary", + bgColor: "bg-primary/10", + animation: { + initial: { scale: 0, y: 10, opacity: 0 }, + animate: { + scale: 1, + y: 0, + opacity: 1, + }, + transition: { + duration: 0.4, + type: "spring", + stiffness: 200, + damping: 15, + }, + }, + }, + [Rating.Easy]: { + icon: Zap, + color: "text-green-500", + bgColor: "bg-green-500/10", + animation: { + initial: { scale: 0, rotate: -20 }, + animate: { + scale: [0, 1.4, 1], + rotate: [-20, 10, 0], + }, + transition: { duration: 0.4, ease: "easeOut" }, + }, + }, + }; + + const config = feedbackConfig[grade]; + const Icon = config.icon; + + return ( + + {/* Background pulse */} + + + {/* Icon animation */} + + + + + {/* Sparkles for Easy */} + {grade === Rating.Easy && ( + <> + {[...Array(6)].map((_, i) => ( + + ))} + + )} + + ); +}; diff --git a/apps/web/src/components/features/flashcards/FlashcardDrawer/GradeOption.tsx b/apps/web/src/components/features/flashcards/FlashcardDrawer/GradeOption.tsx new file mode 100644 index 00000000..51b55387 --- /dev/null +++ b/apps/web/src/components/features/flashcards/FlashcardDrawer/GradeOption.tsx @@ -0,0 +1,96 @@ +import { Button } from "@/components/ui/button"; +import { cn } from "@bahar/design-system"; +import { Trans } from "@lingui/react/macro"; +import { Brain, RotateCcw, ThumbsUp, Zap } from "lucide-react"; +import { motion } from "motion/react"; +import { FC, ReactNode, useMemo } from "react"; +import { Rating } from "ts-fsrs"; +import { formatInterval } from "./utils"; +import { useDir } from "@/hooks/useDir"; + +type ReviewRating = Rating.Again | Rating.Hard | Rating.Good | Rating.Easy; + +type GradeOptionConfig = { + label: ReactNode; + borderStyles: string; + icon: ReactNode; +}; + +type GradeOptionProps = { + grade: ReviewRating; + disabled?: boolean; + now: Date; + due: Date; + onClick: () => void; +}; + +export const GradeOption: FC = ({ + grade, + onClick, + now, + due, + disabled = false, +}) => { + const options = useMemo(() => { + const config: Record = { + [Rating.Again]: { + label: Again, + borderStyles: + "border-muted-foreground/20 hover:border-muted-foreground/40 hover:bg-muted/50", + icon: ( + + ), + }, + [Rating.Hard]: { + label: Hard, + borderStyles: + "border-orange-500/30 hover:border-orange-500/50 hover:bg-orange-500/10", + icon: , + }, + [Rating.Good]: { + label: Good, + borderStyles: + "border-primary/30 hover:border-primary/50 hover:bg-primary/10", + icon: , + }, + [Rating.Easy]: { + label: Easy, + borderStyles: + "border-green-500/30 hover:border-green-500/50 hover:bg-green-500/10", + icon: , + }, + }; + + return config; + }, []); + + const dir = useDir(); + const locale = dir === "rtl" ? "ar-u-nu-arab" : "en"; + + const { label, icon, borderStyles } = options[grade]; + + return ( + + + + ); +}; diff --git a/apps/web/src/components/features/flashcards/FlashcardDrawer/TagBadgesList.tsx b/apps/web/src/components/features/flashcards/FlashcardDrawer/TagBadgesList.tsx new file mode 100644 index 00000000..f36c022e --- /dev/null +++ b/apps/web/src/components/features/flashcards/FlashcardDrawer/TagBadgesList.tsx @@ -0,0 +1,56 @@ +import { FlashcardWithDictionaryEntry } from "@/lib/db/operations/flashcards"; +import { motion } from "motion/react"; +import { FC } from "react"; +import { Badge } from "../../../ui/badge"; +import { t } from "@lingui/core/macro"; +import { SelectDictionaryEntry } from "@bahar/drizzle-user-db-schemas"; + +const getTranslatedType = (entryType: SelectDictionaryEntry["type"]) => { + switch (entryType) { + case "ism": + return t`Noun`; + case "fi'l": + return t`Verb`; + case "harf": + return t`Preposition`; + case "expression": + return t`Expression`; + } +}; + +export const TagBadgesList: FC<{ + currentCard: FlashcardWithDictionaryEntry; +}> = ({ currentCard }) => { + return ( + + {!!currentCard.dictionary_entry.type && ( + + {getTranslatedType(currentCard.dictionary_entry.type)} + + )} + {currentCard.dictionary_entry.tags?.map((tag, index) => ( + + + {tag} + + + ))} + + ); +}; diff --git a/apps/web/src/components/features/flashcards/FlashcardDrawer/index.tsx b/apps/web/src/components/features/flashcards/FlashcardDrawer/index.tsx new file mode 100644 index 00000000..9722e845 --- /dev/null +++ b/apps/web/src/components/features/flashcards/FlashcardDrawer/index.tsx @@ -0,0 +1 @@ +export { FlashcardDrawer } from "./FlashcardDrawer"; diff --git a/apps/web/src/components/features/flashcards/FlashcardDrawer/utils.ts b/apps/web/src/components/features/flashcards/FlashcardDrawer/utils.ts new file mode 100644 index 00000000..50f47b41 --- /dev/null +++ b/apps/web/src/components/features/flashcards/FlashcardDrawer/utils.ts @@ -0,0 +1,18 @@ +import { intlFormatDistance } from "date-fns"; + +/** + * Formats the interval between two dates into a relative time string. + */ +export const formatInterval = (due: Date, now: Date, locale: string) => { + const DAYS_IN_MS = 1000 * 60 * 60 * 24; + + const diffMs = due.getTime() - now.getTime(); + const diffDays = Math.round(diffMs / DAYS_IN_MS); + const dueOnSameDay = diffDays < 1; + + if (dueOnSameDay) { + return intlFormatDistance(due, now, { style: "narrow", locale }); + } + + return intlFormatDistance(due, now, { style: "narrow", locale, unit: "day" }); +}; diff --git a/apps/web/src/components/features/settings/FlashcardSettingsCardSection.tsx b/apps/web/src/components/features/settings/FlashcardSettingsCardSection.tsx index 670dc6df..16e2f22a 100644 --- a/apps/web/src/components/features/settings/FlashcardSettingsCardSection.tsx +++ b/apps/web/src/components/features/settings/FlashcardSettingsCardSection.tsx @@ -21,11 +21,9 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Switch } from "@/components/ui/switch"; import { useToast } from "@/hooks/useToast"; import { queryClient } from "@/lib/query"; -import { trpc } from "@/lib/trpc"; import { z } from "@/lib/zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useLingui } from "@lingui/react/macro"; -import { getQueryKey } from "@trpc/react-query"; import { useCallback, useState } from "react"; import { useForm } from "react-hook-form"; import { useQuery, useMutation } from "@tanstack/react-query"; @@ -45,17 +43,8 @@ export const FlashcardSettingsCardSection = () => { queryFn: () => settingsTable.getSettings.query(), ...settingsTable.getSettings.cacheOptions, }); - const { mutateAsync: updateSettingsRemote } = - trpc.settings.update.useMutation({ - onSuccess: (serverData) => { - queryClient.setQueryData( - getQueryKey(trpc.settings.get, undefined, "query"), - serverData, - ); - }, - }); - const { mutateAsync: updateSettingsLocal } = useMutation({ + const { mutateAsync: updateSettings } = useMutation({ mutationFn: settingsTable.update.mutation, onSuccess: async () => { await queryClient.invalidateQueries({ @@ -130,10 +119,7 @@ export const FlashcardSettingsCardSection = () => { const onSubmit = useCallback( async (formData: z.infer) => { try { - await Promise.all([ - updateSettingsRemote(formData), - updateSettingsLocal({ updates: formData }), - ]); + await updateSettings({ updates: formData }); toast({ title: t`Flashcard settings updated!`, @@ -145,7 +131,7 @@ export const FlashcardSettingsCardSection = () => { }); } }, - [updateSettingsRemote, updateSettingsLocal, t, toast], + [updateSettings, t, toast], ); return ( diff --git a/apps/web/src/env.d.ts b/apps/web/src/env.d.ts deleted file mode 100644 index cafca302..00000000 --- a/apps/web/src/env.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// - -type ImportMetaEnvAugmented = - import("@julr/vite-plugin-validate-env").ImportMetaEnvAugmented< - typeof import("../env").default - >; - -interface ImportMetaEnv extends ImportMetaEnvAugmented { - // Now import.meta.env is totally type-safe and based on your `env.ts` schema definition - // You can also add custom variables that are not defined in your schema -} diff --git a/apps/web/src/lib/db/operations/flashcards.ts b/apps/web/src/lib/db/operations/flashcards.ts index 853ed527..6b741b71 100644 --- a/apps/web/src/lib/db/operations/flashcards.ts +++ b/apps/web/src/lib/db/operations/flashcards.ts @@ -269,6 +269,13 @@ export const flashcardsTable = { setClauses.push("lapses = ?"); params.push(updates.lapses); } + if ( + "learning_steps" in updates && + updates.learning_steps !== undefined + ) { + setClauses.push("learning_steps = ?"); + params.push(updates.learning_steps); + } if ("last_review" in updates && updates.last_review !== undefined) { setClauses.push("last_review = ?"); params.push(updates.last_review); @@ -593,6 +600,7 @@ export const flashcardsTable = { reps: rawCard.reps ?? 0, lapses: rawCard.lapses ?? 0, state: rawCard.state ?? FlashcardState.NEW, + learning_steps: rawCard.learning_steps ?? 0, last_review: rawCard.last_review ? new Date(rawCard.last_review) : undefined, diff --git a/apps/web/src/lib/error.ts b/apps/web/src/lib/error.ts index 70c9d7f6..63f3333f 100644 --- a/apps/web/src/lib/error.ts +++ b/apps/web/src/lib/error.ts @@ -1,26 +1,20 @@ import { t } from "@lingui/core/macro"; -import { FormSchema as DictionarySchema } from "@bahar/schemas"; -import { type ZodError, type ZodIssue, z } from "zod"; +import { FormSchema as DictionarySchema } from "@/lib/schemas/dictionary"; +import { z } from "zod/v4"; -// TODO: after migrating fully to turso user dbs, use the -// client-side type from the lib/db/import-export/v1 dir -// and also add version-specific error-handling -type ZodDictionaryError = ZodError>; +type ZodDictionaryError = z.ZodError>; /** * Error codes used when importing a dictionary. - * - * TODO: reuse the ones from the api */ export enum ImportErrorCode { INVALID_JSON = "invalid_json", VALIDATION_ERROR = "validation_error", } -// TODO: resue the ones from the api export type ImportResponseError = { code: ImportErrorCode; - error: ZodError>; + error: z.ZodError>; }; /** @@ -109,24 +103,24 @@ const formattedErrorMessage = ({ err, prefix, }: { - err: ZodIssue; + err: z.core.$ZodIssue; prefix: string; }) => { switch (err.code) { case "invalid_type": { // Handle required field errors - if (err.received === "undefined") { + if (err.input === undefined) { return `${prefix}${t`Required field`}`; } - return `${prefix}${t`Invalid type. Expected ${err.expected}, received ${err.received}`}`; + return `${prefix}${t`Invalid type. Expected ${err.expected}`}`; } - case "invalid_enum_value": - return `${prefix}${t`Invalid value. Expected one of: ${err.options.join( + case "invalid_value": + return `${prefix}${t`Invalid value. Expected one of: ${err.values.join( ", ", )}`}`; - case "invalid_string": - if (err.validation === "datetime") { + case "invalid_format": + if (err.format === "datetime") { return `${prefix}${t`Invalid datetime format`}`; } return `${prefix}${t`Invalid string format`}`; diff --git a/apps/web/src/lib/i18n.ts b/apps/web/src/lib/i18n.ts index 81c75911..fc61fab4 100644 --- a/apps/web/src/lib/i18n.ts +++ b/apps/web/src/lib/i18n.ts @@ -25,7 +25,6 @@ export const getLangDir = (lang?: TLocale) => { }; export async function dynamicActivate(locale: TLocale) { - // TODO: ideally don't have to hardcode this path const { messages } = await import( `../../../../packages/i18n/locales/${locale}.po` ); diff --git a/apps/web/src/lib/schemas/dictionary.ts b/apps/web/src/lib/schemas/dictionary.ts new file mode 100644 index 00000000..db35a2a1 --- /dev/null +++ b/apps/web/src/lib/schemas/dictionary.ts @@ -0,0 +1,78 @@ +import { z } from "@/lib/zod"; + +export enum Inflection { + indeclinable = "indeclinable", + diptote = "diptote", + triptote = "triptote", +} + +export const FormSchema = z.object({ + word: z.string().min(1), + translation: z.string().min(1), + definition: z.string().optional(), + root: z.string().optional(), + tags: z.array(z.object({ name: z.string() })).optional(), + antonyms: z + .array( + z.object({ + word: z.string(), + }), + ) + .optional(), + examples: z + .array( + z.object({ + sentence: z.string(), + context: z.string().optional(), + translation: z.string().optional(), + }), + ) + .optional(), + type: z.enum(["ism", "fi'l", "harf", "expression"]).optional(), + morphology: z + .object({ + ism: z + .object({ + singular: z.string().optional(), + dual: z.string().optional(), + plurals: z + .array( + z.object({ word: z.string(), details: z.string().optional() }), + ) + .optional(), + gender: z.enum(["masculine", "feminine"]).optional(), + inflection: z + .enum(["indeclinable", "diptote", "triptote"]) + .optional(), + }) + .optional(), + verb: z + .object({ + huroof: z + .array( + z.object({ + harf: z.string(), + meaning: z.string().optional(), + }), + ) + .optional(), + past_tense: z.string().optional(), + present_tense: z.string().optional(), + active_participle: z.string().optional(), + passive_participle: z.string().optional(), + imperative: z.string().optional(), + masadir: z + .array( + z.object({ + word: z.string(), + details: z.string().optional(), + }), + ) + .optional(), + form: z.string().optional(), + form_arabic: z.string().optional(), + }) + .optional(), + }) + .optional(), +}); diff --git a/apps/web/src/lib/trpc.ts b/apps/web/src/lib/trpc.ts index 4a10cd71..6ce07474 100644 --- a/apps/web/src/lib/trpc.ts +++ b/apps/web/src/lib/trpc.ts @@ -26,7 +26,7 @@ export const trpcReactClient = trpc.createClient({ ...opts?.headers, [TRACE_ID_HEADER]: generateTraceId(), }, - }); + } as RequestInit); }, }), ], @@ -44,7 +44,7 @@ export const trpcClient = createTRPCClient({ ...opts?.headers, [TRACE_ID_HEADER]: generateTraceId(), }, - }); + } as RequestInit); }, }), ], diff --git a/apps/web/src/lib/zod.ts b/apps/web/src/lib/zod.ts index 0d56b26e..3e8fd88e 100644 --- a/apps/web/src/lib/zod.ts +++ b/apps/web/src/lib/zod.ts @@ -1,54 +1,48 @@ import { plural, t } from "@lingui/core/macro"; -import z from "zod"; +import { z } from "zod/v4"; -const errorMap: z.ZodErrorMap = (issue, ctx) => { - if (issue.code === z.ZodIssueCode.invalid_type) { +const errorMap: z.core.$ZodErrorMap = (issue) => { + if (issue.code === "invalid_type") { if (issue.expected === "string") { - return { message: t`This field is required.` }; + return t`This field is required.`; } } - if (issue.code === z.ZodIssueCode.invalid_string) { - if (issue.validation === "email") { - return { message: t`That email is invalid.` }; + if (issue.code === "invalid_format") { + if (issue.format === "email") { + return t`That email is invalid.`; } } - if (issue.code === z.ZodIssueCode.too_small) { + if (issue.code === "too_small") { const val = issue.minimum as number; if (issue.type === "string") { if (val === 1) { - return { message: t`This field is required.` }; + return t`This field is required.`; } - return { - message: plural(val, { - one: "Must be at least # character", - other: "Must be at least # characters", - }), - }; + return plural(val, { + one: "Must be at least # character", + other: "Must be at least # characters", + }); } } - if (issue.code === z.ZodIssueCode.too_big) { + if (issue.code === "too_big") { const val = issue.maximum as number; if (issue.type === "string") { - return { - message: plural(val, { - one: "Cannot exceed # character", - other: "Cannot exceed # characters", - }), - }; + return plural(val, { + one: "Cannot exceed # character", + other: "Cannot exceed # characters", + }); } } - return { - message: ctx.defaultError, - }; + return undefined; }; -z.setErrorMap(errorMap); +z.config({ customError: errorMap }); export { z }; diff --git a/apps/web/src/router.ts b/apps/web/src/router.ts index 1b9ef758..7be0f9bc 100644 --- a/apps/web/src/router.ts +++ b/apps/web/src/router.ts @@ -31,7 +31,6 @@ Sentry.init({ tracePropagationTargets: [ new RegExp("^/"), // Match all local routes new RegExp(`^${import.meta.env.VITE_API_BASE_URL}`), - new RegExp(`^${import.meta.env.VITE_MEILISEARCH_API_URL}`), ], // Capture Replay for 10% of all sessions, diff --git a/apps/web/src/routes/_authorized-layout/_app-layout/decks/route.lazy.tsx b/apps/web/src/routes/_authorized-layout/_app-layout/decks/route.lazy.tsx index 63634b05..347f6961 100644 --- a/apps/web/src/routes/_authorized-layout/_app-layout/decks/route.lazy.tsx +++ b/apps/web/src/routes/_authorized-layout/_app-layout/decks/route.lazy.tsx @@ -1,6 +1,5 @@ import { Trans } from "@lingui/react/macro"; import { DeckDialogContent } from "@/components/features/decks/DeckDialogContent"; -import { trpc } from "@/lib/trpc"; import { Button } from "@/components/ui/button"; import { useLingui } from "@lingui/react/macro"; import { useFormatNumber } from "@/hooks/useFormatNumber"; @@ -29,9 +28,8 @@ import { } from "@/components/ui/table"; import { createLazyFileRoute } from "@tanstack/react-router"; import { MoreHorizontal, Plus } from "lucide-react"; -import { FlashcardDrawer } from "@/components/features/flashcards/FlashcardDrawer"; +import { FlashcardDrawer } from "@/components/features/flashcards/FlashcardDrawer/FlashcardDrawer"; import { queryClient } from "@/lib/query"; -import { getQueryKey } from "@trpc/react-query"; import { useToast } from "@/hooks/useToast"; import { Page } from "@/components/Page"; import { useQuery, useMutation } from "@tanstack/react-query"; @@ -56,15 +54,7 @@ const Decks = () => { ], }); - const { mutateAsync: deleteDeck } = trpc.decks.delete.useMutation({ - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: [...getQueryKey(trpc.decks.list), { type: "query" }], - }); - }, - }); - - const { mutateAsync: deleteDeckLocal } = useMutation({ + const { mutateAsync: deleteDeck } = useMutation({ mutationFn: decksTable.delete.mutation, onSuccess: async () => { await queryClient.invalidateQueries({ @@ -208,10 +198,7 @@ const Decks = () => { { - await Promise.all([ - deleteDeck({ id: deck.id }), - deleteDeckLocal({ id: deck.id }), - ]); + await deleteDeck({ id: deck.id }); toast({ title: t`Deck successfully deleted!`, diff --git a/apps/web/src/routes/_authorized-layout/_app-layout/dictionary/add/route.lazy.tsx b/apps/web/src/routes/_authorized-layout/_app-layout/dictionary/add/route.lazy.tsx index 26ac010d..8da84ecc 100644 --- a/apps/web/src/routes/_authorized-layout/_app-layout/dictionary/add/route.lazy.tsx +++ b/apps/web/src/routes/_authorized-layout/_app-layout/dictionary/add/route.lazy.tsx @@ -22,13 +22,12 @@ import { MorphologyFormSection, CategoryFormSection, } from "@/components/features/dictionary/add"; -import { trpc } from "@/lib/trpc"; import { useToast } from "@/hooks/useToast"; import { useLingui } from "@lingui/react/macro"; import { useEffect } from "react"; import { useDir } from "@/hooks/useDir"; import { TagsFormSection } from "@/components/features/dictionary/add/TagsFormSection"; -import { FormSchema } from "@bahar/schemas"; +import { FormSchema } from "@/lib/schemas/dictionary"; import { useAddDictionaryEntry } from "@/hooks/db"; const Breadcrumbs = ({ className }: { className?: string }) => { @@ -81,7 +80,6 @@ const BackButton = () => { }; const Add = () => { - const { mutateAsync: addWord } = trpc.dictionary.addWord.useMutation(); const { addDictionaryEntry } = useAddDictionaryEntry(); const { toast } = useToast(); @@ -154,11 +152,7 @@ const Add = () => { } })(); - await Promise.all([ - addWord(wordData), - // TODO: type shouldnt be optional - addDictionaryEntry({ word: { ...wordData, type: data.type ?? "ism" } }), - ]); + await addDictionaryEntry({ word: { ...wordData, type: data.type ?? "ism" } }); toast({ title: t`Successfully added word!`, diff --git a/apps/web/src/routes/_authorized-layout/_app-layout/dictionary/edit/$wordId.tsx b/apps/web/src/routes/_authorized-layout/_app-layout/dictionary/edit/$wordId.tsx index ea7a8fe6..7a1d4c39 100644 --- a/apps/web/src/routes/_authorized-layout/_app-layout/dictionary/edit/$wordId.tsx +++ b/apps/web/src/routes/_authorized-layout/_app-layout/dictionary/edit/$wordId.tsx @@ -2,8 +2,6 @@ import { Trans } from "@lingui/react/macro"; import { Page } from "@/components/Page"; import { Link, createFileRoute, useNavigate } from "@tanstack/react-router"; import { queryClient } from "@/lib/query"; -import { trpc } from "@/lib/trpc"; -import { getQueryKey } from "@trpc/react-query"; import { Breadcrumb, BreadcrumbItem, @@ -30,7 +28,7 @@ import { useLingui } from "@lingui/react/macro"; import { FC, useEffect } from "react"; import { useDir } from "@/hooks/useDir"; import { TagsFormSection } from "@/components/features/dictionary/add/TagsFormSection"; -import { FormSchema } from "@bahar/schemas"; +import { FormSchema } from "@/lib/schemas/dictionary"; import { Dialog, DialogClose, @@ -47,15 +45,7 @@ import { flashcardsTable } from "@/lib/db/operations/flashcards"; import { useDeleteDictionaryEntry, useEditDictionaryEntry } from "@/hooks/db"; const ResetFlashcardButton: FC<{ id: string }> = ({ id }) => { - const { mutateAsync: resetFlashcard } = trpc.flashcard.reset.useMutation({ - onSuccess: async () => { - await queryClient.invalidateQueries({ - queryKey: getQueryKey(trpc.flashcard.today, undefined, "query"), - }); - }, - }); - - const { mutateAsync: resetFlashcardLocal } = useMutation({ + const { mutateAsync: resetFlashcard } = useMutation({ mutationFn: async ({ dictionary_entry_id, }: { @@ -110,10 +100,7 @@ const ResetFlashcardButton: FC<{ id: string }> = ({ id }) => { type="button" className="w-max md:self-start self-center" onClick={async () => { - await Promise.all([ - resetFlashcard({ id }), - resetFlashcardLocal({ dictionary_entry_id: id }), - ]); + await resetFlashcard({ dictionary_entry_id: id }); toast({ title: t`Successfully reset the flashcard.`, @@ -130,7 +117,6 @@ const ResetFlashcardButton: FC<{ id: string }> = ({ id }) => { }; const DeleteWordButton: FC<{ id: string }> = ({ id }) => { - const { mutateAsync: deleteWord } = trpc.dictionary.deleteWord.useMutation(); const { deleteDictionaryEntry } = useDeleteDictionaryEntry(); const navigate = useNavigate(); @@ -166,10 +152,7 @@ const DeleteWordButton: FC<{ id: string }> = ({ id }) => { type="button" className="w-max md:self-start self-center" onClick={async () => { - await Promise.all([ - deleteWord({ id }), - deleteDictionaryEntry({ id }), - ]); + await deleteDictionaryEntry({ id }); navigate({ to: "/" }); }} @@ -281,19 +264,6 @@ const Edit = () => { queryKey: [...dictionaryEntriesTable.entry.cacheOptions.queryKey, wordId], }); - const { mutateAsync: editWord } = trpc.dictionary.editWord.useMutation({ - onSuccess: async () => { - await queryClient.invalidateQueries({ - queryKey: getQueryKey( - trpc.dictionary.find, - { - id: wordId, - }, - "query", - ), - }); - }, - }); const { toast } = useToast(); const { t } = useLingui(); const form = useForm>({ @@ -346,10 +316,7 @@ const Edit = () => { } })(); - await Promise.all([ - editWord(input), - editDictionaryEntry({ id: input.id, updates: input }), - ]); + await editDictionaryEntry({ id: input.id, updates: input }); toast({ title: t`Successfully updated the word!`, diff --git a/apps/web/src/routes/_authorized-layout/_app-layout/settings/route.lazy.tsx b/apps/web/src/routes/_authorized-layout/_app-layout/settings/route.lazy.tsx index b5c6864c..877d6b9d 100644 --- a/apps/web/src/routes/_authorized-layout/_app-layout/settings/route.lazy.tsx +++ b/apps/web/src/routes/_authorized-layout/_app-layout/settings/route.lazy.tsx @@ -160,13 +160,6 @@ const Settings = () => { await db.checkpoint(); }); - tracedFetch(`${import.meta.env.VITE_API_BASE_URL}/dictionary`, { - method: "DELETE", - credentials: "include", - }).catch((err: unknown) => { - console.error("Failed to delete from Meilisearch:", err); - }); - reset(); resetOramaDb(); const hydrateResult = await hydrateOramaDb(); @@ -315,20 +308,6 @@ const Settings = () => { await db.checkpoint(); }); - const meilisearchFormData = new FormData(); - meilisearchFormData.append("dictionary", file!); - - tracedFetch( - `${import.meta.env.VITE_API_BASE_URL}/dictionary/import`, - { - method: "POST", - body: meilisearchFormData, - credentials: "include", - }, - ).catch((err: unknown) => { - console.error("Failed to sync to Meilisearch:", err); - }); - reset(); resetOramaDb(); const hydrateResult = await hydrateOramaDb(); diff --git a/apps/web/src/routes/_authorized-layout/_search-layout/index.lazy.tsx b/apps/web/src/routes/_authorized-layout/_search-layout/index.lazy.tsx index 7502a773..c91d3588 100644 --- a/apps/web/src/routes/_authorized-layout/_search-layout/index.lazy.tsx +++ b/apps/web/src/routes/_authorized-layout/_search-layout/index.lazy.tsx @@ -7,7 +7,7 @@ import { ArrowUp, PlusIcon, GraduationCap, BookOpen } from "lucide-react"; import { useWindowScroll, useWindowSize } from "@uidotdev/usehooks"; import { cn } from "@bahar/design-system"; import { Page, itemVariants } from "@/components/Page"; -import { FlashcardDrawer } from "@/components/features/flashcards/FlashcardDrawer"; +import { FlashcardDrawer } from "@/components/features/flashcards/FlashcardDrawer/FlashcardDrawer"; import { DEFAULT_BACKLOG_THRESHOLD_DAYS, flashcardsTable, diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml deleted file mode 100644 index 55866ed6..00000000 --- a/docker-compose.prod.yaml +++ /dev/null @@ -1,33 +0,0 @@ -services: - api_prod: - build: - context: . - dockerfile: ./apps/api/Dockerfile - target: production - ports: - - "3000:3000" - env_file: - - ./apps/api/.env - depends_on: - - libsql - meilisearch: - build: - context: . - dockerfile: ./apps/search/Dockerfile - ports: - - "7700:7700" - env_file: - - ./apps/search/.env - volumes: - - "./meili_data:/meili_data" - libsql: - image: ghcr.io/tursodatabase/libsql-server:latest - platform: linux/amd64 - # For some reason, the default db user - # in the container doesn't have write permissions - user: root - ports: - - "8080:8080" - - "5001:5001" - volumes: - - ./libsql:/var/lib/sqld diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index eaa5b739..00000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,67 +0,0 @@ -services: - api: - build: - context: . - dockerfile: ./apps/api/Dockerfile - target: development - command: > - sh -c "pnpm run --filter api drizzle:migrate && exec pnpm turbo run dev --filter=api" - ports: - - "3000:3000" - env_file: - - ./apps/api/.env - volumes: - - .:/app - depends_on: - - libsql - drizzle_studio: - network_mode: host - build: - context: . - dockerfile: ./apps/api/Dockerfile - target: development - command: > - sh -c "pnpm run --filter api drizzle:studio --host=0.0.0.0 --port 4983" - environment: - - DATABASE_URL=http://localhost:8080 - volumes: - - .:/app - depends_on: - - libsql - web: - build: - context: . - dockerfile: ./apps/web/Dockerfile - target: development - command: pnpm turbo run dev --filter=web -- --port 4000 --host - depends_on: - - api - ports: - - "4000:4000" - env_file: - - ./apps/web/.env - volumes: - - .:/app - meilisearch: - build: - context: . - dockerfile: ./apps/search/Dockerfile - ports: - - "7700:7700" - env_file: - - ./apps/search/.env - volumes: - - "./meili_data:/meili_data" - libsql: - image: ghcr.io/tursodatabase/libsql-server:latest - platform: linux/amd64 - # For some reason, the default db user - # in the container doesn't have write permissions - user: root - ports: - - "8080:8080" - - "5001:5001" - # environment: - # - SQLD_DB_PATH=/tmp/libsql - volumes: - - ./libsql:/var/lib/sqld diff --git a/package.json b/package.json index fd49893b..f41d08b1 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,5 @@ "packageManager": "pnpm@8.15.3", "engines": { "node": ">=18" - }, - "pnpm": { - "overrides": { - "zod": "3.24.1", - "zod-to-json-schema": "3.24.5" - } } } diff --git a/packages/db-operations/package.json b/packages/db-operations/package.json index 88db8bc7..733967af 100644 --- a/packages/db-operations/package.json +++ b/packages/db-operations/package.json @@ -8,13 +8,13 @@ "peerDependencies": { "@bahar/drizzle-user-db-schemas": "workspace:*", "@bahar/result": "workspace:*", - "zod": "^3.24.1" + "zod": "4.0.17" }, "devDependencies": { "@bahar/drizzle-user-db-schemas": "workspace:*", "@bahar/result": "workspace:*", "@bahar/typescript-config": "workspace:*", - "zod": "^3.24.1" + "zod": "4.0.17" }, "dependencies": { "nanoid": "^5.0.7", diff --git a/packages/drizzle-user-db-schemas/drizzle/0001_even_kitty_pryde.sql b/packages/drizzle-user-db-schemas/drizzle/0001_even_kitty_pryde.sql new file mode 100644 index 00000000..66c276fa --- /dev/null +++ b/packages/drizzle-user-db-schemas/drizzle/0001_even_kitty_pryde.sql @@ -0,0 +1 @@ +ALTER TABLE `flashcards` ADD `learning_steps` integer DEFAULT 0; \ No newline at end of file diff --git a/packages/drizzle-user-db-schemas/drizzle/meta/0001_snapshot.json b/packages/drizzle-user-db-schemas/drizzle/meta/0001_snapshot.json new file mode 100644 index 00000000..62f5129d --- /dev/null +++ b/packages/drizzle-user-db-schemas/drizzle/meta/0001_snapshot.json @@ -0,0 +1,391 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "9fc5f9c4-64c5-4516-8fdc-6e99c9b9779d", + "prevId": "d47631a0-386a-4c46-a2aa-3a1be896b140", + "tables": { + "decks": { + "name": "decks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "filters": { + "name": "filters", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dictionary_entries": { + "name": "dictionary_entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at_timestamp_ms": { + "name": "created_at_timestamp_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at_timestamp_ms": { + "name": "updated_at_timestamp_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "word": { + "name": "word", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "translation": { + "name": "translation", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "root": { + "name": "root", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "antonyms": { + "name": "antonyms", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "examples": { + "name": "examples", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "morphology": { + "name": "morphology", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "flashcards": { + "name": "flashcards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "dictionary_entry_id": { + "name": "dictionary_entry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "difficulty": { + "name": "difficulty", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "due": { + "name": "due", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "due_timestamp_ms": { + "name": "due_timestamp_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "elapsed_days": { + "name": "elapsed_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "lapses": { + "name": "lapses", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "last_review": { + "name": "last_review", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_review_timestamp_ms": { + "name": "last_review_timestamp_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "learning_steps": { + "name": "learning_steps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "reps": { + "name": "reps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "scheduled_days": { + "name": "scheduled_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "stability": { + "name": "stability", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "state": { + "name": "state", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'forward'" + }, + "is_hidden": { + "name": "is_hidden", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "flashcards_entry_direction_unique": { + "name": "flashcards_entry_direction_unique", + "columns": [ + "dictionary_entry_id", + "direction" + ], + "isUnique": true + }, + "flashcards_due_timestamp_ms_index": { + "name": "flashcards_due_timestamp_ms_index", + "columns": [ + "due_timestamp_ms" + ], + "isUnique": false + } + }, + "foreignKeys": { + "flashcards_dictionary_entry_id_dictionary_entries_id_fk": { + "name": "flashcards_dictionary_entry_id_dictionary_entries_id_fk", + "tableFrom": "flashcards", + "tableTo": "dictionary_entries", + "columnsFrom": [ + "dictionary_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "migrations": { + "name": "migrations", + "columns": { + "version": { + "name": "version", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at_ms": { + "name": "applied_at_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'applied'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "show_antonyms_in_flashcard": { + "name": "show_antonyms_in_flashcard", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'hidden'" + }, + "show_reverse_flashcards": { + "name": "show_reverse_flashcards", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/drizzle-user-db-schemas/drizzle/meta/_journal.json b/packages/drizzle-user-db-schemas/drizzle/meta/_journal.json index be46f49c..b0603394 100644 --- a/packages/drizzle-user-db-schemas/drizzle/meta/_journal.json +++ b/packages/drizzle-user-db-schemas/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1761496780590, "tag": "0000_complete_changeling", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1766694906322, + "tag": "0001_even_kitty_pryde", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/drizzle-user-db-schemas/package.json b/packages/drizzle-user-db-schemas/package.json index 85b24b66..a914cf41 100644 --- a/packages/drizzle-user-db-schemas/package.json +++ b/packages/drizzle-user-db-schemas/package.json @@ -9,12 +9,12 @@ "drizzle:gen": "drizzle-kit generate" }, "peerDependencies": { - "zod": "^3.24.1", - "drizzle-orm": "^0.36.4", - "drizzle-zod": "^0.5.1" + "zod": "4.0.17", + "drizzle-orm": "^0.45.1", + "drizzle-zod": "^0.8.3" }, "devDependencies": { "@bahar/typescript-config": "workspace:*", - "drizzle-kit": "^0.28.1" + "drizzle-kit": "^0.31.8" } } diff --git a/packages/drizzle-user-db-schemas/src/flashcards.ts b/packages/drizzle-user-db-schemas/src/flashcards.ts index 1cf4f711..dc6885af 100644 --- a/packages/drizzle-user-db-schemas/src/flashcards.ts +++ b/packages/drizzle-user-db-schemas/src/flashcards.ts @@ -30,6 +30,7 @@ export const flashcards = sqliteTable( lapses: integer("lapses").default(0), last_review: text("last_review"), last_review_timestamp_ms: integer("last_review_timestamp_ms"), + learning_steps: integer("learning_steps").default(0), reps: integer("reps").default(0), scheduled_days: integer("scheduled_days").default(0), stability: real("stability").default(0), @@ -43,17 +44,14 @@ export const flashcards = sqliteTable( .notNull() .default(false), }, - (table) => ({ + (table) => [ // Unique constraint: each dictionary entry can have at most one flashcard per direction - entryDirectionIdx: uniqueIndex("flashcards_entry_direction_unique").on( + uniqueIndex("flashcards_entry_direction_unique").on( table.dictionary_entry_id, table.direction, ), - - dueTimestampMsIdx: index("flashcards_due_timestamp_ms_index").on( - table.due_timestamp_ms, - ), - }), + index("flashcards_due_timestamp_ms_index").on(table.due_timestamp_ms), + ], ); export type SelectFlashcard = typeof flashcards.$inferSelect; diff --git a/packages/fsrs/package.json b/packages/fsrs/package.json index 0cf5d908..5bf91b49 100644 --- a/packages/fsrs/package.json +++ b/packages/fsrs/package.json @@ -9,10 +9,10 @@ "@bahar/drizzle-user-db-schemas": "workspace:*" }, "peerDependencies": { - "ts-fsrs": "^4.5.1" + "ts-fsrs": "^5.2.3" }, "devDependencies": { "@bahar/typescript-config": "workspace:*", - "ts-fsrs": "^4.5.1" + "ts-fsrs": "^5.2.3" } } diff --git a/packages/fsrs/src/index.ts b/packages/fsrs/src/index.ts index 5f7ef445..22ae9b3a 100644 --- a/packages/fsrs/src/index.ts +++ b/packages/fsrs/src/index.ts @@ -43,6 +43,7 @@ export const toFsrsCard = ( reps: flashcard.reps ?? 0, lapses: flashcard.lapses ?? 0, state: (flashcard.state ?? FlashcardState.NEW) as State, + learning_steps: flashcard.learning_steps ?? 0, last_review: flashcard.last_review ? new Date(flashcard.last_review) : undefined, @@ -69,6 +70,7 @@ export const fromFsrsCard = ( lapses: card.lapses, last_review: card.last_review?.toISOString() ?? null, last_review_timestamp_ms: lastReviewTimestampMs, + learning_steps: card.learning_steps, reps: card.reps, scheduled_days: card.scheduled_days, stability: card.stability, @@ -131,6 +133,7 @@ export const gradeFlashcard = ( | "lapses" | "elapsed_days" | "scheduled_days" + | "learning_steps" > => { const fsrsCard = toFsrsCard(flashcard); const scheduling = scheduler.repeat(fsrsCard, now); @@ -151,5 +154,6 @@ export const gradeFlashcard = ( lapses: selectedCard.lapses, elapsed_days: selectedCard.elapsed_days, scheduled_days: selectedCard.scheduled_days, + learning_steps: selectedCard.learning_steps, }; }; diff --git a/packages/schemas/package.json b/packages/schemas/package.json deleted file mode 100644 index 6dbebd0a..00000000 --- a/packages/schemas/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "@bahar/schemas", - "version": "0.0.0", - "private": true, - "license": "MIT", - "main": "src/index.ts", - "types": "src/index.ts", - "peerDependencies": { - "zod": "^3.24.1" - }, - "devDependencies": { - "@bahar/typescript-config": "workspace:*" - } -} \ No newline at end of file diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts deleted file mode 100644 index 4789280a..00000000 --- a/packages/schemas/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./dictionary"; \ No newline at end of file diff --git a/packages/schemas/tsconfig.json b/packages/schemas/tsconfig.json deleted file mode 100644 index ae4b48c3..00000000 --- a/packages/schemas/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@bahar/typescript-config/base.json", - "compilerOptions": { - "moduleResolution": "bundler", - "target": "ES2022" - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 436d370a..76604b9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,10 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - zod: 3.24.1 - zod-to-json-schema: 3.24.5 - importers: .: @@ -27,12 +23,9 @@ importers: apps/api: dependencies: - '@apidevtools/json-schema-ref-parser': - specifier: ^11.7.2 - version: 11.7.2 '@better-auth/expo': - specifier: 1.2.10 - version: 1.2.10(better-auth@1.2.10) + specifier: 1.4.9 + version: 1.4.9(@better-auth/core@1.4.9)(better-auth@1.4.9)(expo-constants@18.0.10)(expo-linking@8.0.9)(expo-web-browser@15.0.9) '@libsql/client': specifier: ^0.10.0 version: 0.10.0 @@ -54,15 +47,12 @@ importers: '@tursodatabase/api': specifier: ^1.9.0 version: 1.9.0 - '@types/cookie-parser': - specifier: ^1.4.6 - version: 1.4.6 '@upstash/redis': - specifier: ^1.28.4 - version: 1.28.4 + specifier: ^1.36.0 + version: 1.36.0 better-auth: - specifier: 1.2.10 - version: 1.2.10 + specifier: 1.4.9 + version: 1.4.9(drizzle-kit@0.31.8)(drizzle-orm@0.45.1)(react-dom@19.1.0)(react@19.1.0) cookie-parser: specifier: ^1.4.6 version: 1.4.6 @@ -73,20 +63,17 @@ importers: specifier: ^16.4.5 version: 16.4.5 drizzle-orm: - specifier: ^0.36.4 - version: 0.36.4(@libsql/client@0.10.0)(@opentelemetry/api@1.9.0)(react@19.1.0) + specifier: ^0.45.1 + version: 0.45.1(@libsql/client@0.10.0)(@opentelemetry/api@1.9.0)(@upstash/redis@1.36.0)(kysely@0.28.5) drizzle-zod: - specifier: ^0.5.1 - version: 0.5.1(drizzle-orm@0.36.4)(zod@3.24.1) + specifier: ^0.8.3 + version: 0.8.3(drizzle-orm@0.45.1)(zod@4.0.17) express: specifier: ^4.18.2 version: 4.18.2 jose: - specifier: ^6.0.13 - version: 6.0.13 - meilisearch: - specifier: ^0.45.0 - version: 0.45.0 + specifier: ^6.1.3 + version: 6.1.3 multer: specifier: 1.4.5-lts.1 version: 1.4.5-lts.1 @@ -102,15 +89,12 @@ importers: resend: specifier: ^4.6.0 version: 4.6.0(react-dom@19.1.0)(react@19.1.0) - ts-fsrs: - specifier: ^4.5.1 - version: 4.5.1 tsx: specifier: ^4.7.1 version: 4.7.1 zod: - specifier: 3.24.1 - version: 3.24.1 + specifier: 4.0.17 + version: 4.0.17 devDependencies: '@bahar/eslint-config': specifier: workspace:* @@ -118,6 +102,9 @@ importers: '@bahar/typescript-config': specifier: workspace:* version: link:../../packages/config-typescript + '@types/cookie-parser': + specifier: ^1.4.6 + version: 1.4.6 '@types/cors': specifier: ^2.8.17 version: 2.8.17 @@ -140,8 +127,8 @@ importers: specifier: ^7.0.2 version: 7.0.2(eslint@8.56.0)(typescript@5.8.3) drizzle-kit: - specifier: ^0.28.1 - version: 0.28.1 + specifier: ^0.31.8 + version: 0.31.8 esbuild: specifier: ^0.21.5 version: 0.21.5 @@ -151,12 +138,6 @@ importers: eslint: specifier: ^8.56.0 version: 8.56.0 - json-refs: - specifier: ^3.0.15 - version: 3.0.15 - json-schema-to-zod: - specifier: ^2.4.1 - version: 2.4.1 pino-pretty: specifier: ^13.0.0 version: 13.0.0 @@ -228,8 +209,8 @@ importers: apps/mobile: dependencies: '@better-auth/expo': - specifier: 1.2.10 - version: 1.2.10(better-auth@1.2.10) + specifier: 1.4.9 + version: 1.4.9(@better-auth/core@1.4.9)(better-auth@1.4.9)(expo-constants@18.0.10)(expo-linking@8.0.9)(expo-web-browser@15.0.9) '@digitalartlab/expo-plugin-localization': specifier: ^3.0.0 version: 3.0.0(expo@54.0.25) @@ -243,8 +224,8 @@ importers: specifier: ^5.4.4 version: 5.4.4 '@hookform/resolvers': - specifier: ^5.0.1 - version: 5.0.1(react-hook-form@7.56.4) + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.56.4) '@lingui/core': specifier: ^5.3.2 version: 5.3.2(@lingui/babel-plugin-lingui-macro@5.3.2) @@ -288,8 +269,8 @@ importers: specifier: ^11.4.3 version: 11.4.3(@tanstack/react-query@5.83.0)(@trpc/client@11.4.3)(@trpc/server@11.4.3)(react-dom@19.1.0)(react@19.1.0)(typescript@5.9.2) better-auth: - specifier: 1.2.10 - version: 1.2.10 + specifier: 1.4.9 + version: 1.4.9(drizzle-kit@0.31.8)(drizzle-orm@0.45.1)(react-dom@19.1.0)(react@19.1.0) class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -390,14 +371,14 @@ importers: specifier: ^0.21.0 version: 0.21.0(react-native-gesture-handler@2.28.0)(react-native-reanimated@4.1.5)(react-native-safe-area-context@5.6.2)(react-native-screens@4.16.0)(react-native-svg@15.12.1)(react-native@0.81.5)(react@19.1.0) ts-fsrs: - specifier: ^4.5.1 - version: 4.5.1 + specifier: ^5.2.3 + version: 5.2.3 uniwind: specifier: ^1.2.2 version: 1.2.2(react-native@0.81.5)(react@19.1.0)(tailwindcss@4.1.17) zod: - specifier: 3.24.1 - version: 3.24.1 + specifier: 4.0.17 + version: 4.0.17 devDependencies: '@babel/core': specifier: ^7.25.2 @@ -420,9 +401,6 @@ importers: '@bahar/result': specifier: workspace:* version: link:../../packages/result - '@bahar/schemas': - specifier: workspace:* - version: link:../../packages/schemas '@bahar/search': specifier: workspace:* version: link:../../packages/search @@ -474,9 +452,12 @@ importers: apps/web: dependencies: + '@bahar/fsrs': + specifier: workspace:* + version: link:../../packages/fsrs '@hookform/resolvers': - specifier: ^5.0.1 - version: 5.0.1(react-hook-form@7.56.4) + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.56.4) '@lingui/core': specifier: ^5.3.2 version: 5.3.2(@lingui/babel-plugin-lingui-macro@5.3.2)(babel-plugin-macros@3.1.0) @@ -489,9 +470,6 @@ importers: '@lingui/react': specifier: ^5.3.2 version: 5.3.2(react@19.0.0) - '@meilisearch/instant-meilisearch': - specifier: ^0.16.0 - version: 0.16.0 '@orama/highlight': specifier: ^0.1.9 version: 0.1.9 @@ -592,14 +570,14 @@ importers: specifier: ^11.4.3 version: 11.4.3(@tanstack/react-query@5.83.0)(@trpc/client@11.4.3)(@trpc/server@11.4.3)(react-dom@19.0.0)(react@19.0.0)(typescript@5.8.3) '@tursodatabase/sync-wasm': - specifier: ^0.2.2 - version: 0.2.2 + specifier: ^0.3.2 + version: 0.3.2 '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1(react-dom@19.0.0)(react@19.0.0) better-auth: - specifier: 1.2.10 - version: 1.2.10 + specifier: 1.4.9 + version: 1.4.9(drizzle-orm@0.45.1)(react-dom@19.0.0)(react@19.0.0) class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -616,8 +594,8 @@ importers: specifier: ^3.3.0 version: 3.3.0 drizzle-orm: - specifier: ^0.36.4 - version: 0.36.4(@types/react@19.0.0)(react@19.0.0) + specifier: ^0.45.1 + version: 0.45.1(@libsql/client@0.10.0)(@opentelemetry/api@1.9.0)(@upstash/redis@1.36.0)(kysely@0.28.5) input-otp: specifier: 1.4.2 version: 1.4.2(react-dom@19.0.0)(react@19.0.0) @@ -639,21 +617,18 @@ importers: react-hook-form: specifier: ^7.56.1 version: 7.56.4(react@19.0.0) - react-instantsearch: - specifier: ^7.15.6 - version: 7.15.8(algoliasearch@5.35.0)(react-dom@19.0.0)(react@19.0.0) tailwind-merge: specifier: ^2.2.1 version: 2.2.1 ts-fsrs: - specifier: ^4.5.1 - version: 4.5.1 + specifier: ^5.2.3 + version: 5.2.3 vaul: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.0.0)(react@19.0.0) zod: - specifier: 3.24.1 - version: 3.24.1 + specifier: 4.0.17 + version: 4.0.17 devDependencies: '@bahar/design-system': specifier: workspace:* @@ -667,15 +642,9 @@ importers: '@bahar/i18n': specifier: workspace:* version: link:../../packages/i18n - '@bahar/schemas': - specifier: workspace:* - version: link:../../packages/schemas '@bahar/typescript-config': specifier: workspace:* version: link:../../packages/config-typescript - '@julr/vite-plugin-validate-env': - specifier: ^1.2.0 - version: 1.2.0(vite@5.4.21)(zod@3.24.1) '@lingui/cli': specifier: ^5.3.2 version: 5.3.2(typescript@5.8.3) @@ -737,8 +706,8 @@ importers: specifier: ^0.20.1 version: 0.20.1(vite@5.4.21)(workbox-build@7.3.0)(workbox-window@7.3.0) wrangler: - specifier: ^3.91.0 - version: 3.91.0 + specifier: ^4.54.0 + version: 4.54.0 packages/config-eslint: devDependencies: @@ -785,8 +754,8 @@ importers: specifier: workspace:* version: link:../config-typescript zod: - specifier: 3.24.1 - version: 3.24.1 + specifier: 4.0.17 + version: 4.0.17 packages/design-system: dependencies: @@ -810,21 +779,21 @@ importers: packages/drizzle-user-db-schemas: dependencies: drizzle-orm: - specifier: ^0.36.4 - version: 0.36.4(@libsql/client@0.10.0)(@opentelemetry/api@1.9.0)(react@19.1.0) + specifier: ^0.45.1 + version: 0.45.1(@libsql/client@0.10.0)(@opentelemetry/api@1.9.0)(@upstash/redis@1.36.0)(kysely@0.28.5) drizzle-zod: - specifier: ^0.5.1 - version: 0.5.1(drizzle-orm@0.36.4)(zod@3.24.1) + specifier: ^0.8.3 + version: 0.8.3(drizzle-orm@0.45.1)(zod@4.0.17) zod: - specifier: 3.24.1 - version: 3.24.1 + specifier: 4.0.17 + version: 4.0.17 devDependencies: '@bahar/typescript-config': specifier: workspace:* version: link:../config-typescript drizzle-kit: - specifier: ^0.28.1 - version: 0.28.1 + specifier: ^0.31.8 + version: 0.31.8 packages/fsrs: dependencies: @@ -836,8 +805,8 @@ importers: specifier: workspace:* version: link:../config-typescript ts-fsrs: - specifier: ^4.5.1 - version: 4.5.1 + specifier: ^5.2.3 + version: 5.2.3 packages/i18n: dependencies: @@ -846,7 +815,7 @@ importers: version: 5.3.2 '@lingui/conf': specifier: ^5.3.2 - version: 5.3.2(typescript@5.9.2) + version: 5.3.2(typescript@5.8.3) '@lingui/core': specifier: ^5.3.2 version: 5.3.2(@lingui/babel-plugin-lingui-macro@5.3.2)(babel-plugin-macros@3.1.0) @@ -857,16 +826,6 @@ importers: specifier: workspace:* version: link:../config-typescript - packages/schemas: - dependencies: - zod: - specifier: 3.24.1 - version: 3.24.1 - devDependencies: - '@bahar/typescript-config': - specifier: workspace:* - version: link:../config-typescript - packages/search: dependencies: '@bahar/drizzle-user-db-schemas': @@ -901,136 +860,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /@algolia/abtesting@1.1.0: - resolution: {integrity: sha512-sEyWjw28a/9iluA37KLGu8vjxEIlb60uxznfTUmXImy7H5NvbpSO6yYgmgH5KiD7j+zTUUihiST0jEP12IoXow==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - '@algolia/requester-browser-xhr': 5.35.0 - '@algolia/requester-fetch': 5.35.0 - '@algolia/requester-node-http': 5.35.0 - dev: false - - /@algolia/client-abtesting@5.35.0: - resolution: {integrity: sha512-uUdHxbfHdoppDVflCHMxRlj49/IllPwwQ2cQ8DLC4LXr3kY96AHBpW0dMyi6ygkn2MtFCc6BxXCzr668ZRhLBQ==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - '@algolia/requester-browser-xhr': 5.35.0 - '@algolia/requester-fetch': 5.35.0 - '@algolia/requester-node-http': 5.35.0 - dev: false - - /@algolia/client-analytics@5.35.0: - resolution: {integrity: sha512-SunAgwa9CamLcRCPnPHx1V2uxdQwJGqb1crYrRWktWUdld0+B2KyakNEeVn5lln4VyeNtW17Ia7V7qBWyM/Skw==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - '@algolia/requester-browser-xhr': 5.35.0 - '@algolia/requester-fetch': 5.35.0 - '@algolia/requester-node-http': 5.35.0 - dev: false - - /@algolia/client-common@5.35.0: - resolution: {integrity: sha512-ipE0IuvHu/bg7TjT2s+187kz/E3h5ssfTtjpg1LbWMgxlgiaZIgTTbyynM7NfpSJSKsgQvCQxWjGUO51WSCu7w==} - engines: {node: '>= 14.0.0'} - dev: false - - /@algolia/client-insights@5.35.0: - resolution: {integrity: sha512-UNbCXcBpqtzUucxExwTSfAe8gknAJ485NfPN6o1ziHm6nnxx97piIbcBQ3edw823Tej2Wxu1C0xBY06KgeZ7gA==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - '@algolia/requester-browser-xhr': 5.35.0 - '@algolia/requester-fetch': 5.35.0 - '@algolia/requester-node-http': 5.35.0 - dev: false - - /@algolia/client-personalization@5.35.0: - resolution: {integrity: sha512-/KWjttZ6UCStt4QnWoDAJ12cKlQ+fkpMtyPmBgSS2WThJQdSV/4UWcqCUqGH7YLbwlj3JjNirCu3Y7uRTClxvA==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - '@algolia/requester-browser-xhr': 5.35.0 - '@algolia/requester-fetch': 5.35.0 - '@algolia/requester-node-http': 5.35.0 - dev: false - - /@algolia/client-query-suggestions@5.35.0: - resolution: {integrity: sha512-8oCuJCFf/71IYyvQQC+iu4kgViTODbXDk3m7yMctEncRSRV+u2RtDVlpGGfPlJQOrAY7OONwJlSHkmbbm2Kp/w==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - '@algolia/requester-browser-xhr': 5.35.0 - '@algolia/requester-fetch': 5.35.0 - '@algolia/requester-node-http': 5.35.0 - dev: false - - /@algolia/client-search@5.35.0: - resolution: {integrity: sha512-FfmdHTrXhIduWyyuko1YTcGLuicVbhUyRjO3HbXE4aP655yKZgdTIfMhZ/V5VY9bHuxv/fGEh3Od1Lvv2ODNTg==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - '@algolia/requester-browser-xhr': 5.35.0 - '@algolia/requester-fetch': 5.35.0 - '@algolia/requester-node-http': 5.35.0 - dev: false - - /@algolia/events@4.0.1: - resolution: {integrity: sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==} - dev: false - - /@algolia/ingestion@1.35.0: - resolution: {integrity: sha512-gPzACem9IL1Co8mM1LKMhzn1aSJmp+Vp434An4C0OBY4uEJRcqsLN3uLBlY+bYvFg8C8ImwM9YRiKczJXRk0XA==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - '@algolia/requester-browser-xhr': 5.35.0 - '@algolia/requester-fetch': 5.35.0 - '@algolia/requester-node-http': 5.35.0 - dev: false - - /@algolia/monitoring@1.35.0: - resolution: {integrity: sha512-w9MGFLB6ashI8BGcQoVt7iLgDIJNCn4OIu0Q0giE3M2ItNrssvb8C0xuwJQyTy1OFZnemG0EB1OvXhIHOvQwWw==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - '@algolia/requester-browser-xhr': 5.35.0 - '@algolia/requester-fetch': 5.35.0 - '@algolia/requester-node-http': 5.35.0 - dev: false - - /@algolia/recommend@5.35.0: - resolution: {integrity: sha512-AhrVgaaXAb8Ue0u2nuRWwugt0dL5UmRgS9LXe0Hhz493a8KFeZVUE56RGIV3hAa6tHzmAV7eIoqcWTQvxzlJeQ==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - '@algolia/requester-browser-xhr': 5.35.0 - '@algolia/requester-fetch': 5.35.0 - '@algolia/requester-node-http': 5.35.0 - dev: false - - /@algolia/requester-browser-xhr@5.35.0: - resolution: {integrity: sha512-diY415KLJZ6x1Kbwl9u96Jsz0OstE3asjXtJ9pmk1d+5gPuQ5jQyEsgC+WmEXzlec3iuVszm8AzNYYaqw6B+Zw==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - dev: false - - /@algolia/requester-fetch@5.35.0: - resolution: {integrity: sha512-uydqnSmpAjrgo8bqhE9N1wgcB98psTRRQXcjc4izwMB7yRl9C8uuAQ/5YqRj04U0mMQ+fdu2fcNF6m9+Z1BzDQ==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - dev: false - - /@algolia/requester-node-http@5.35.0: - resolution: {integrity: sha512-RgLX78ojYOrThJHrIiPzT4HW3yfQa0D7K+MQ81rhxqaNyNBu4F1r+72LNHYH/Z+y9I1Mrjrd/c/Ue5zfDgAEjQ==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.35.0 - dev: false - /@alloc/quick-lru@5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -1049,10 +878,6 @@ packages: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.30 - /@antfu/utils@0.7.10: - resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} - dev: true - /@apideck/better-ajv-errors@0.3.6(ajv@8.17.1): resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} engines: {node: '>=10'} @@ -1065,15 +890,6 @@ packages: leven: 3.1.0 dev: true - /@apidevtools/json-schema-ref-parser@11.7.2: - resolution: {integrity: sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==} - engines: {node: '>= 16'} - dependencies: - '@jsdevtools/ono': 7.1.3 - '@types/json-schema': 7.0.15 - js-yaml: 4.1.0 - dev: false - /@astrojs/check@0.9.6(prettier@3.1.1)(typescript@5.9.2): resolution: {integrity: sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA==} hasBin: true @@ -1227,7 +1043,7 @@ packages: dependencies: sitemap: 8.0.2 stream-replace-string: 2.0.0 - zod: 3.24.1 + zod: 3.25.76 dev: false /@astrojs/telemetry@3.3.0: @@ -3443,26 +3259,71 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true - /@better-auth/expo@1.2.10(better-auth@1.2.10): - resolution: {integrity: sha512-wMnYtDGF5W37Kuq7OY3/mudlWbbvxLMX0zoOMgB/icx3mReD6dv3hfpuVPoNQxT/dl/R+Rj7dvJ3b5Rk4CI3NA==} + /@better-auth/core@1.4.9(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7)(jose@6.1.3)(kysely@0.28.5)(nanostores@1.1.0): + resolution: {integrity: sha512-JT2q4NDkQzN22KclUEoZ7qU6tl9HUTfK1ctg2oWlT87SEagkwJcnrUwS9VznL+u9ziOIfY27P0f7/jSnmvLcoQ==} peerDependencies: - better-auth: 1.2.10 + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + better-call: 1.1.7 + jose: ^6.1.0 + kysely: ^0.28.5 + nanostores: ^1.0.1 dependencies: - '@better-fetch/fetch': 1.1.18 - better-auth: 1.2.10 - better-call: 1.0.16 - zod: 3.24.1 + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@standard-schema/spec': 1.1.0 + better-call: 1.1.7(zod@4.0.17) + jose: 6.1.3 + kysely: 0.28.5 + nanostores: 1.1.0 + zod: 4.2.1 dev: false - /@better-auth/utils@0.2.5: - resolution: {integrity: sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ==} + /@better-auth/expo@1.4.9(@better-auth/core@1.4.9)(better-auth@1.4.9)(expo-constants@18.0.10)(expo-linking@8.0.9)(expo-web-browser@15.0.9): + resolution: {integrity: sha512-BgiL4KbDGGyy3nPNRxcE2pClp6myAJ+W991l7xwkcZAmH9l6lLkeAKO+SR5R8HGlTY8vKichdgVVy9FRu9bpUQ==} + peerDependencies: + '@better-auth/core': 1.4.9 + better-auth: 1.4.9 + expo-constants: '>=17.0.0' + expo-linking: '>=7.0.0' + expo-network: ^8.0.7 + expo-web-browser: '>=14.0.0' + peerDependenciesMeta: + expo-constants: + optional: true + expo-linking: + optional: true + expo-network: + optional: true + expo-web-browser: + optional: true dependencies: - typescript: 5.9.2 - uncrypto: 0.1.3 + '@better-auth/core': 1.4.9(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7)(jose@6.1.3)(kysely@0.28.5)(nanostores@1.1.0) + '@better-fetch/fetch': 1.1.21 + better-auth: 1.4.9(drizzle-kit@0.31.8)(drizzle-orm@0.45.1)(react-dom@19.1.0)(react@19.1.0) + better-call: 1.1.7(zod@4.2.1) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5)(react@19.1.0) + expo-web-browser: 15.0.9(expo@54.0.25)(react-native@0.81.5) + zod: 4.2.1 + dev: false + + /@better-auth/telemetry@1.4.9(@better-auth/core@1.4.9): + resolution: {integrity: sha512-Tthy1/Gmx+pYlbvRQPBTKfVei8+pJwvH1NZp+5SbhwA6K2EXIaoonx/K6N/AXYs2aKUpyR4/gzqDesDjL7zd6A==} + peerDependencies: + '@better-auth/core': 1.4.9 + dependencies: + '@better-auth/core': 1.4.9(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7)(jose@6.1.3)(kysely@0.28.5)(nanostores@1.1.0) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + dev: false + + /@better-auth/utils@0.3.0: + resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} dev: false - /@better-fetch/fetch@1.1.18: - resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} + /@better-fetch/fetch@1.1.21: + resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} dev: false /@capsizecss/unpack@3.0.1: @@ -3479,6 +3340,13 @@ packages: mime: 3.0.0 dev: true + /@cloudflare/kv-asset-handler@0.4.1: + resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==} + engines: {node: '>=18.0.0'} + dependencies: + mime: 3.0.0 + dev: true + /@cloudflare/unenv-preset@2.0.2(unenv@2.0.0-rc.14)(workerd@1.20250718.0): resolution: {integrity: sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==} peerDependencies: @@ -3492,14 +3360,18 @@ packages: workerd: 1.20250718.0 dev: true - /@cloudflare/workerd-darwin-64@1.20241106.1: - resolution: {integrity: sha512-zxvaToi1m0qzAScrxFt7UvFVqU8DxrCO2CinM1yQkv5no7pA1HolpIrwZ0xOhR3ny64Is2s/J6BrRjpO5dM9Zw==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - requiresBuild: true + /@cloudflare/unenv-preset@2.7.13(unenv@2.0.0-rc.24)(workerd@1.20251210.0): + resolution: {integrity: sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: ^1.20251202.0 + peerDependenciesMeta: + workerd: + optional: true + dependencies: + unenv: 2.0.0-rc.24 + workerd: 1.20251210.0 dev: true - optional: true /@cloudflare/workerd-darwin-64@1.20250718.0: resolution: {integrity: sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==} @@ -3510,10 +3382,10 @@ packages: dev: true optional: true - /@cloudflare/workerd-darwin-arm64@1.20241106.1: - resolution: {integrity: sha512-j3dg/42D/bPgfNP3cRUBxF+4waCKO/5YKwXNj+lnVOwHxDu+ne5pFw9TIkKYcWTcwn0ZUkbNZNM5rhJqRn4xbg==} + /@cloudflare/workerd-darwin-64@1.20251210.0: + resolution: {integrity: sha512-Nn9X1moUDERA9xtFdCQ2XpQXgAS9pOjiCxvOT8sVx9UJLAiBLkfSCGbpsYdarODGybXCpjRlc77Yppuolvt7oQ==} engines: {node: '>=16'} - cpu: [arm64] + cpu: [x64] os: [darwin] requiresBuild: true dev: true @@ -3528,11 +3400,11 @@ packages: dev: true optional: true - /@cloudflare/workerd-linux-64@1.20241106.1: - resolution: {integrity: sha512-Ih+Ye8E1DMBXcKrJktGfGztFqHKaX1CeByqshmTbODnWKHt6O65ax3oTecUwyC0+abuyraOpAtdhHNpFMhUkmw==} + /@cloudflare/workerd-darwin-arm64@1.20251210.0: + resolution: {integrity: sha512-Mg8iYIZQFnbevq/ls9eW/eneWTk/EE13Pej1MwfkY5et0jVpdHnvOLywy/o+QtMJFef1AjsqXGULwAneYyBfHw==} engines: {node: '>=16'} - cpu: [x64] - os: [linux] + cpu: [arm64] + os: [darwin] requiresBuild: true dev: true optional: true @@ -3546,10 +3418,10 @@ packages: dev: true optional: true - /@cloudflare/workerd-linux-arm64@1.20241106.1: - resolution: {integrity: sha512-mdQFPk4+14Yywn7n1xIzI+6olWM8Ybz10R7H3h+rk0XulMumCWUCy1CzIDauOx6GyIcSgKIibYMssVHZR30ObA==} + /@cloudflare/workerd-linux-64@1.20251210.0: + resolution: {integrity: sha512-kjC2fCZhZ2Gkm1biwk2qByAYpGguK5Gf5ic8owzSCUw0FOUfQxTZUT9Lp3gApxsfTLbbnLBrX/xzWjywH9QR4g==} engines: {node: '>=16'} - cpu: [arm64] + cpu: [x64] os: [linux] requiresBuild: true dev: true @@ -3564,11 +3436,11 @@ packages: dev: true optional: true - /@cloudflare/workerd-windows-64@1.20241106.1: - resolution: {integrity: sha512-4rtcss31E/Rb/PeFocZfr+B9i1MdrkhsTBWizh8siNR4KMmkslU2xs2wPaH1z8+ErxkOsHrKRa5EPLh5rIiFeg==} + /@cloudflare/workerd-linux-arm64@1.20251210.0: + resolution: {integrity: sha512-2IB37nXi7PZVQLa1OCuO7/6pNxqisRSO8DmCQ5x/3sezI5op1vwOxAcb1osAnuVsVN9bbvpw70HJvhKruFJTuA==} engines: {node: '>=16'} - cpu: [x64] - os: [win32] + cpu: [arm64] + os: [linux] requiresBuild: true dev: true optional: true @@ -3582,17 +3454,11 @@ packages: dev: true optional: true - /@cloudflare/workers-shared@0.9.0: - resolution: {integrity: sha512-eP6Ir45uPbKnpADVzUCtkRUYxYxjB1Ew6n/whTJvHu8H4m93USHAceCMm736VBZdlxuhXXUjEP3fCUxKPn+cfw==} - engines: {node: '>=16.7.0'} - dependencies: - mime: 3.0.0 - zod: 3.24.1 - dev: true - - /@colors/colors@1.5.0: - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} + /@cloudflare/workerd-windows-64@1.20251210.0: + resolution: {integrity: sha512-Uaz6/9XE+D6E7pCY4OvkCuJHu7HcSDzeGcCGY1HLhojXhHd7yL52c3yfiyJdS8hPatiAa0nn5qSI/42+aTdDSw==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] requiresBuild: true dev: true optional: true @@ -3617,7 +3483,6 @@ packages: /@drizzle-team/brocli@0.10.2: resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} - dev: true /@egjs/hammerjs@2.0.17: resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} @@ -3662,18 +3527,11 @@ packages: tslib: 2.8.1 dev: false - /@emnapi/runtime@1.5.0: - resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} - dependencies: - tslib: 2.8.1 - dev: false - /@emnapi/runtime@1.7.1: resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} requiresBuild: true dependencies: tslib: 2.8.1 - optional: true /@emnapi/wasi-threads@1.1.0: resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -3687,15 +3545,13 @@ packages: dependencies: esbuild: 0.18.20 source-map-support: 0.5.21 - dev: true /@esbuild-kit/esm-loader@2.6.5: resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} deprecated: 'Merged into tsx: https://tsx.is' dependencies: '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.7.2 - dev: true + get-tsconfig: 4.8.1 /@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19): resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} @@ -3721,6 +3577,7 @@ packages: cpu: [ppc64] os: [aix] requiresBuild: true + dev: false optional: true /@esbuild/aix-ppc64@0.21.5: @@ -3740,6 +3597,14 @@ packages: dev: true optional: true + /@esbuild/aix-ppc64@0.25.12: + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + optional: true + /@esbuild/aix-ppc64@0.25.2: resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} engines: {node: '>=18'} @@ -3748,6 +3613,15 @@ packages: requiresBuild: true optional: true + /@esbuild/aix-ppc64@0.27.0: + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.17.19: resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} engines: {node: '>=12'} @@ -3763,7 +3637,6 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: true optional: true /@esbuild/android-arm64@0.19.12: @@ -3772,6 +3645,7 @@ packages: cpu: [arm64] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/android-arm64@0.21.5: @@ -3791,6 +3665,14 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.25.12: + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + /@esbuild/android-arm64@0.25.2: resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} engines: {node: '>=18'} @@ -3799,6 +3681,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-arm64@0.27.0: + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.17.19: resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} engines: {node: '>=12'} @@ -3814,7 +3705,6 @@ packages: cpu: [arm] os: [android] requiresBuild: true - dev: true optional: true /@esbuild/android-arm@0.19.12: @@ -3823,6 +3713,7 @@ packages: cpu: [arm] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/android-arm@0.21.5: @@ -3842,6 +3733,14 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.25.12: + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true + /@esbuild/android-arm@0.25.2: resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} engines: {node: '>=18'} @@ -3850,6 +3749,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-arm@0.27.0: + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.17.19: resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} engines: {node: '>=12'} @@ -3865,7 +3773,6 @@ packages: cpu: [x64] os: [android] requiresBuild: true - dev: true optional: true /@esbuild/android-x64@0.19.12: @@ -3874,6 +3781,7 @@ packages: cpu: [x64] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/android-x64@0.21.5: @@ -3893,6 +3801,14 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.25.12: + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + requiresBuild: true + optional: true + /@esbuild/android-x64@0.25.2: resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} engines: {node: '>=18'} @@ -3901,6 +3817,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-x64@0.27.0: + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.17.19: resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} engines: {node: '>=12'} @@ -3916,7 +3841,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: true optional: true /@esbuild/darwin-arm64@0.19.12: @@ -3925,6 +3849,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: false optional: true /@esbuild/darwin-arm64@0.21.5: @@ -3944,6 +3869,14 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.25.12: + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + /@esbuild/darwin-arm64@0.25.2: resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} engines: {node: '>=18'} @@ -3952,6 +3885,15 @@ packages: requiresBuild: true optional: true + /@esbuild/darwin-arm64@0.27.0: + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.17.19: resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} engines: {node: '>=12'} @@ -3967,7 +3909,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: true optional: true /@esbuild/darwin-x64@0.19.12: @@ -3976,6 +3917,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: false optional: true /@esbuild/darwin-x64@0.21.5: @@ -3995,6 +3937,14 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.25.12: + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + /@esbuild/darwin-x64@0.25.2: resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} engines: {node: '>=18'} @@ -4003,6 +3953,15 @@ packages: requiresBuild: true optional: true + /@esbuild/darwin-x64@0.27.0: + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.17.19: resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} engines: {node: '>=12'} @@ -4018,7 +3977,6 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true - dev: true optional: true /@esbuild/freebsd-arm64@0.19.12: @@ -4027,6 +3985,7 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true + dev: false optional: true /@esbuild/freebsd-arm64@0.21.5: @@ -4046,6 +4005,14 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.25.12: + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + optional: true + /@esbuild/freebsd-arm64@0.25.2: resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} engines: {node: '>=18'} @@ -4054,6 +4021,15 @@ packages: requiresBuild: true optional: true + /@esbuild/freebsd-arm64@0.27.0: + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.17.19: resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} engines: {node: '>=12'} @@ -4069,7 +4045,6 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: true optional: true /@esbuild/freebsd-x64@0.19.12: @@ -4078,6 +4053,7 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true + dev: false optional: true /@esbuild/freebsd-x64@0.21.5: @@ -4097,6 +4073,14 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.25.12: + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + optional: true + /@esbuild/freebsd-x64@0.25.2: resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} engines: {node: '>=18'} @@ -4105,6 +4089,15 @@ packages: requiresBuild: true optional: true + /@esbuild/freebsd-x64@0.27.0: + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.17.19: resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} engines: {node: '>=12'} @@ -4120,7 +4113,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-arm64@0.19.12: @@ -4129,6 +4121,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-arm64@0.21.5: @@ -4148,6 +4141,14 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.25.12: + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + /@esbuild/linux-arm64@0.25.2: resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} engines: {node: '>=18'} @@ -4156,6 +4157,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-arm64@0.27.0: + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.17.19: resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} engines: {node: '>=12'} @@ -4171,7 +4181,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-arm@0.19.12: @@ -4180,6 +4189,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-arm@0.21.5: @@ -4199,6 +4209,14 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.25.12: + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + /@esbuild/linux-arm@0.25.2: resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} engines: {node: '>=18'} @@ -4207,6 +4225,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-arm@0.27.0: + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.17.19: resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} engines: {node: '>=12'} @@ -4222,7 +4249,6 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-ia32@0.19.12: @@ -4231,6 +4257,7 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-ia32@0.21.5: @@ -4250,6 +4277,14 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.25.12: + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + requiresBuild: true + optional: true + /@esbuild/linux-ia32@0.25.2: resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} engines: {node: '>=18'} @@ -4258,6 +4293,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-ia32@0.27.0: + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.17.19: resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} engines: {node: '>=12'} @@ -4273,7 +4317,6 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-loong64@0.19.12: @@ -4282,6 +4325,7 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-loong64@0.21.5: @@ -4301,6 +4345,14 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.25.12: + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + requiresBuild: true + optional: true + /@esbuild/linux-loong64@0.25.2: resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} engines: {node: '>=18'} @@ -4309,6 +4361,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-loong64@0.27.0: + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.17.19: resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} engines: {node: '>=12'} @@ -4324,7 +4385,6 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-mips64el@0.19.12: @@ -4333,6 +4393,7 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-mips64el@0.21.5: @@ -4352,6 +4413,14 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.25.12: + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + optional: true + /@esbuild/linux-mips64el@0.25.2: resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} engines: {node: '>=18'} @@ -4360,6 +4429,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-mips64el@0.27.0: + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.17.19: resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} engines: {node: '>=12'} @@ -4375,7 +4453,6 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-ppc64@0.19.12: @@ -4384,6 +4461,7 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-ppc64@0.21.5: @@ -4403,6 +4481,14 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.25.12: + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + optional: true + /@esbuild/linux-ppc64@0.25.2: resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} engines: {node: '>=18'} @@ -4411,6 +4497,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-ppc64@0.27.0: + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.17.19: resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} engines: {node: '>=12'} @@ -4426,7 +4521,6 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-riscv64@0.19.12: @@ -4435,6 +4529,7 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-riscv64@0.21.5: @@ -4454,6 +4549,14 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.25.12: + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optional: true + /@esbuild/linux-riscv64@0.25.2: resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} engines: {node: '>=18'} @@ -4462,6 +4565,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-riscv64@0.27.0: + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.17.19: resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} engines: {node: '>=12'} @@ -4477,7 +4589,6 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-s390x@0.19.12: @@ -4486,6 +4597,7 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-s390x@0.21.5: @@ -4505,6 +4617,14 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.25.12: + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + requiresBuild: true + optional: true + /@esbuild/linux-s390x@0.25.2: resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} engines: {node: '>=18'} @@ -4513,6 +4633,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-s390x@0.27.0: + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.17.19: resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} engines: {node: '>=12'} @@ -4528,7 +4657,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true optional: true /@esbuild/linux-x64@0.19.12: @@ -4537,6 +4665,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-x64@0.21.5: @@ -4556,6 +4685,14 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.25.12: + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + /@esbuild/linux-x64@0.25.2: resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} engines: {node: '>=18'} @@ -4564,6 +4701,23 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-x64@0.27.0: + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-arm64@0.25.12: + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + requiresBuild: true + optional: true + /@esbuild/netbsd-arm64@0.25.2: resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} engines: {node: '>=18'} @@ -4572,6 +4726,15 @@ packages: requiresBuild: true optional: true + /@esbuild/netbsd-arm64@0.27.0: + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.17.19: resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} engines: {node: '>=12'} @@ -4587,7 +4750,6 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true - dev: true optional: true /@esbuild/netbsd-x64@0.19.12: @@ -4596,6 +4758,7 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true + dev: false optional: true /@esbuild/netbsd-x64@0.21.5: @@ -4615,6 +4778,14 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.25.12: + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + optional: true + /@esbuild/netbsd-x64@0.25.2: resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} engines: {node: '>=18'} @@ -4623,6 +4794,15 @@ packages: requiresBuild: true optional: true + /@esbuild/netbsd-x64@0.27.0: + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-arm64@0.23.1: resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} engines: {node: '>=18'} @@ -4632,6 +4812,14 @@ packages: dev: true optional: true + /@esbuild/openbsd-arm64@0.25.12: + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + requiresBuild: true + optional: true + /@esbuild/openbsd-arm64@0.25.2: resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} engines: {node: '>=18'} @@ -4640,6 +4828,15 @@ packages: requiresBuild: true optional: true + /@esbuild/openbsd-arm64@0.27.0: + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.17.19: resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} engines: {node: '>=12'} @@ -4655,7 +4852,6 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true - dev: true optional: true /@esbuild/openbsd-x64@0.19.12: @@ -4664,6 +4860,7 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true + dev: false optional: true /@esbuild/openbsd-x64@0.21.5: @@ -4680,15 +4877,49 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true - dev: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.25.12: + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + optional: true + + /@esbuild/openbsd-x64@0.25.2: + resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + optional: true + + /@esbuild/openbsd-x64@0.27.0: + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openharmony-arm64@0.25.12: + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + requiresBuild: true optional: true - /@esbuild/openbsd-x64@0.25.2: - resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} + /@esbuild/openharmony-arm64@0.27.0: + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] + cpu: [arm64] + os: [openharmony] requiresBuild: true + dev: true optional: true /@esbuild/sunos-x64@0.17.19: @@ -4706,7 +4937,6 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true - dev: true optional: true /@esbuild/sunos-x64@0.19.12: @@ -4715,6 +4945,7 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true + dev: false optional: true /@esbuild/sunos-x64@0.21.5: @@ -4734,6 +4965,14 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.25.12: + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + requiresBuild: true + optional: true + /@esbuild/sunos-x64@0.25.2: resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} engines: {node: '>=18'} @@ -4742,6 +4981,15 @@ packages: requiresBuild: true optional: true + /@esbuild/sunos-x64@0.27.0: + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.17.19: resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} engines: {node: '>=12'} @@ -4757,7 +5005,6 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: true optional: true /@esbuild/win32-arm64@0.19.12: @@ -4766,6 +5013,7 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true + dev: false optional: true /@esbuild/win32-arm64@0.21.5: @@ -4785,6 +5033,14 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.25.12: + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + /@esbuild/win32-arm64@0.25.2: resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} engines: {node: '>=18'} @@ -4793,6 +5049,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-arm64@0.27.0: + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.17.19: resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} engines: {node: '>=12'} @@ -4808,7 +5073,6 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: true optional: true /@esbuild/win32-ia32@0.19.12: @@ -4817,6 +5081,7 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true + dev: false optional: true /@esbuild/win32-ia32@0.21.5: @@ -4836,6 +5101,14 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.25.12: + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + /@esbuild/win32-ia32@0.25.2: resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} engines: {node: '>=18'} @@ -4844,6 +5117,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-ia32@0.27.0: + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.17.19: resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} engines: {node: '>=12'} @@ -4859,7 +5141,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: true optional: true /@esbuild/win32-x64@0.19.12: @@ -4868,6 +5149,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: false optional: true /@esbuild/win32-x64@0.21.5: @@ -4887,6 +5169,14 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.25.12: + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + /@esbuild/win32-x64@0.25.2: resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} engines: {node: '>=18'} @@ -4895,6 +5185,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-x64@0.27.0: + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.56.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5188,8 +5487,8 @@ packages: optional: true dependencies: ws: 8.18.3 - zod: 3.24.1 - zod-to-json-schema: 3.24.5(zod@3.24.1) + zod: 3.25.76 + zod-to-json-schema: 3.25.0(zod@3.25.76) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -5432,17 +5731,13 @@ packages: tslib: 2.8.1 dev: false - /@hexagon/base64@1.1.28: - resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} - dev: false - - /@hookform/resolvers@5.0.1(react-hook-form@7.56.4): - resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==} + /@hookform/resolvers@5.2.2(react-hook-form@7.56.4): + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} peerDependencies: react-hook-form: ^7.55.0 dependencies: '@standard-schema/utils': 0.3.0 - react-hook-form: 7.56.4(react@19.0.0) + react-hook-form: 7.56.4(react@19.1.0) dev: false /@humanwhocodes/config-array@0.11.13: @@ -6206,33 +6501,6 @@ packages: '@jridgewell/sourcemap-codec': 1.5.5 dev: true - /@jsdevtools/ono@7.1.3: - resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} - dev: false - - /@julr/vite-plugin-validate-env@1.2.0(vite@5.4.21)(zod@3.24.1): - resolution: {integrity: sha512-taAYS+7zOREHjiLiMp6S1ZhYRxWs4QfC7Usvqvbi4ni78vOuHREbk446A5u0im06eTxUOfqishLbZ879N0hHMg==} - engines: {node: '>=16'} - peerDependencies: - vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0 || ^6.0.0 - zod: 3.24.1 - peerDependenciesMeta: - zod: - optional: true - dependencies: - '@poppinss/cliui': 6.4.1 - '@poppinss/validator-lite': 1.0.3 - unconfig: 0.6.0 - vite: 5.4.21(@types/node@20.10.6) - zod: 3.24.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@levischuck/tiny-cbor@0.2.11: - resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} - dev: false - /@libsql/client@0.10.0: resolution: {integrity: sha512-2ERn08T4XOVx34yBtUPq0RDjAdd9TJ5qNH/izugr208ml2F94mk92qC64kXyDVQINodWJvp3kAdq6P4zTtCZ7g==} dependencies: @@ -6407,7 +6675,7 @@ packages: '@babel/core': 7.26.0 '@babel/runtime': 7.26.7 '@babel/types': 7.26.3 - '@lingui/conf': 5.3.2 + '@lingui/conf': 5.3.2(typescript@5.8.3) '@lingui/core': 5.3.2(@lingui/babel-plugin-lingui-macro@5.3.2)(babel-plugin-macros@3.1.0) '@lingui/message-utils': 5.3.2 babel-plugin-macros: 3.1.0 @@ -6435,6 +6703,7 @@ packages: transitivePeerDependencies: - supports-color - typescript + dev: true /@lingui/babel-plugin-lingui-macro@5.3.2(babel-plugin-macros@3.1.0)(typescript@5.9.2): resolution: {integrity: sha512-NdXrq8aZlPjN4jeA/LkSLNyx5vPGmrW+r2ywMNQDPQPVP28Hq8c3hF9SQc1t7hwBorGQ3qzIQ7i2Vm6Y8PnjQw==} @@ -6455,7 +6724,6 @@ packages: transitivePeerDependencies: - supports-color - typescript - dev: true /@lingui/babel-plugin-lingui-macro@5.3.2(typescript@5.9.2): resolution: {integrity: sha512-NdXrq8aZlPjN4jeA/LkSLNyx5vPGmrW+r2ywMNQDPQPVP28Hq8c3hF9SQc1t7hwBorGQ3qzIQ7i2Vm6Y8PnjQw==} @@ -6488,7 +6756,7 @@ packages: '@babel/types': 7.26.3 '@lingui/babel-plugin-extract-messages': 5.3.2 '@lingui/babel-plugin-lingui-macro': 5.3.2(babel-plugin-macros@3.1.0) - '@lingui/conf': 5.3.2 + '@lingui/conf': 5.3.2(typescript@5.8.3) '@lingui/core': 5.3.2(@lingui/babel-plugin-lingui-macro@5.3.2)(babel-plugin-macros@3.1.0) '@lingui/format-po': 5.3.2 '@lingui/message-utils': 5.3.2 @@ -6593,19 +6861,6 @@ packages: - typescript dev: true - /@lingui/conf@5.3.2: - resolution: {integrity: sha512-c0Dfovr9BLuwAnY5GADxKcwBUQdVl0Jo/JUa3cumIXFhHzZGb78kfhCHjWWQdX8+WQD8qzSl/YkVDbxhcQJGmg==} - engines: {node: '>=20.0.0'} - dependencies: - '@babel/runtime': 7.26.7 - chalk: 4.1.2 - cosmiconfig: 8.3.6(typescript@5.9.2) - jest-validate: 29.7.0 - jiti: 1.21.7 - transitivePeerDependencies: - - typescript - dev: false - /@lingui/conf@5.3.2(typescript@5.8.3): resolution: {integrity: sha512-c0Dfovr9BLuwAnY5GADxKcwBUQdVl0Jo/JUa3cumIXFhHzZGb78kfhCHjWWQdX8+WQD8qzSl/YkVDbxhcQJGmg==} engines: {node: '>=20.0.0'} @@ -6660,7 +6915,7 @@ packages: optional: true dependencies: '@babel/runtime': 7.26.7 - '@lingui/babel-plugin-lingui-macro': 5.3.2(babel-plugin-macros@3.1.0)(typescript@5.8.3) + '@lingui/babel-plugin-lingui-macro': 5.3.2(babel-plugin-macros@3.1.0)(typescript@5.9.2) '@lingui/message-utils': 5.3.2 babel-plugin-macros: 3.1.0 unraw: 3.0.0 @@ -6674,7 +6929,7 @@ packages: resolution: {integrity: sha512-YZxk0CqdTNV6Mwo/PRsx4SHoNxfGEMEK6OHDk42/MGJiUQ6/Od6+2qlAW5z9TvapHnzWbDbU3EdBcqLvgD7Rqw==} engines: {node: '>=20.0.0'} dependencies: - '@lingui/conf': 5.3.2 + '@lingui/conf': 5.3.2(typescript@5.8.3) '@lingui/message-utils': 5.3.2 date-fns: 3.6.0 pofile: 1.1.4 @@ -6879,14 +7134,6 @@ packages: - supports-color dev: false - /@meilisearch/instant-meilisearch@0.16.0: - resolution: {integrity: sha512-JdqG/Wq+8cbzwxz4DKLuUuTetpiAcpaGQaOvi2wJzzXyYfyoiGh3/F12XMY8CA/pfDRLZtrZpDYvYLcsH+QUqw==} - dependencies: - meilisearch: 0.37.0 - transitivePeerDependencies: - - encoding - dev: false - /@messageformat/parser@5.1.0: resolution: {integrity: sha512-jKlkls3Gewgw6qMjKZ9SFfHUpdzEVdovKFtW1qRhJ3WI4FW5R/NnGDqr8SDGz+krWDO3ki94boMmQvGke1HwUQ==} dependencies: @@ -6909,7 +7156,7 @@ packages: resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} dependencies: '@emnapi/core': 1.5.0 - '@emnapi/runtime': 1.5.0 + '@emnapi/runtime': 1.7.1 '@tybys/wasm-util': 0.10.1 dev: false @@ -6923,13 +7170,14 @@ packages: eslint-scope: 5.1.1 dev: true - /@noble/ciphers@0.6.0: - resolution: {integrity: sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==} + /@noble/ciphers@2.1.1: + resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} + engines: {node: '>= 20.19.0'} dev: false - /@noble/hashes@1.8.0: - resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} - engines: {node: ^14.21.3 || >=16} + /@noble/hashes@2.0.1: + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} dev: false /@nodelib/fs.scandir@2.1.5: @@ -7437,49 +7685,6 @@ packages: resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} dev: false - /@peculiar/asn1-android@2.4.0: - resolution: {integrity: sha512-YFueREq97CLslZZBI8dKzis7jMfEHSLxM+nr0Zdx1POiXFLjqqwoY5s0F1UimdBiEw/iKlHey2m56MRDv7Jtyg==} - dependencies: - '@peculiar/asn1-schema': 2.4.0 - asn1js: 3.0.6 - tslib: 2.8.1 - dev: false - - /@peculiar/asn1-ecc@2.4.0: - resolution: {integrity: sha512-fJiYUBCJBDkjh347zZe5H81BdJ0+OGIg0X9z06v8xXUoql3MFeENUX0JsjCaVaU9A0L85PefLPGYkIoGpTnXLQ==} - dependencies: - '@peculiar/asn1-schema': 2.4.0 - '@peculiar/asn1-x509': 2.4.0 - asn1js: 3.0.6 - tslib: 2.8.1 - dev: false - - /@peculiar/asn1-rsa@2.4.0: - resolution: {integrity: sha512-6PP75voaEnOSlWR9sD25iCQyLgFZHXbmxvUfnnDcfL6Zh5h2iHW38+bve4LfH7a60x7fkhZZNmiYqAlAff9Img==} - dependencies: - '@peculiar/asn1-schema': 2.4.0 - '@peculiar/asn1-x509': 2.4.0 - asn1js: 3.0.6 - tslib: 2.8.1 - dev: false - - /@peculiar/asn1-schema@2.4.0: - resolution: {integrity: sha512-umbembjIWOrPSOzEGG5vxFLkeM8kzIhLkgigtsOrfLKnuzxWxejAcUX+q/SoZCdemlODOcr5WiYa7+dIEzBXZQ==} - dependencies: - asn1js: 3.0.6 - pvtsutils: 1.3.6 - tslib: 2.8.1 - dev: false - - /@peculiar/asn1-x509@2.4.0: - resolution: {integrity: sha512-F7mIZY2Eao2TaoVqigGMLv+NDdpwuBKU1fucHPONfzaBS4JXXCNCmfO0Z3dsy7JzKGqtDcYC1mr9JjaZQZNiuw==} - dependencies: - '@peculiar/asn1-schema': 2.4.0 - asn1js: 3.0.6 - pvtsutils: 1.3.6 - tslib: 2.8.1 - dev: false - /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -7498,33 +7703,22 @@ packages: tslib: 2.8.1 dev: true - /@poppinss/cliui@6.4.1: - resolution: {integrity: sha512-tdV3QpAfrPFRLPOh98F8QxWBvwYF3ziWGGtpVqfZtFNTFkC7nQnVQlUW55UtQ7rkeMmFohxfDI+2JNWScGJ1jQ==} - engines: {node: '>=18.16.0'} + /@poppinss/colors@4.1.6: + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} dependencies: - '@poppinss/colors': 4.1.3 - cli-boxes: 3.0.0 - cli-table3: 0.6.5 - cli-truncate: 4.0.0 - log-update: 6.1.0 - pretty-hrtime: 1.0.3 - string-width: 7.2.0 - supports-color: 9.4.0 - terminal-size: 4.0.0 - wordwrap: 1.0.0 + kleur: 4.1.5 dev: true - /@poppinss/colors@4.1.3: - resolution: {integrity: sha512-A0FjJ6x14donWDN3bHAFFjJaPWTwM2PgWT834+bPKVK6Xukf25CscoRqCPYI939a8yuJFX9PYWWnVbUVI0E2Cg==} - engines: {node: '>=18.16.0'} + /@poppinss/dumper@0.6.5: + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} dependencies: - kleur: 4.1.5 + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.1.1 + supports-color: 10.2.2 dev: true - /@poppinss/validator-lite@1.0.3: - resolution: {integrity: sha512-u4dmT7PDHwNtxY3q1jHVp/u+hMEEcBlkzd37QwwM4tVt/0mLlEDttSfPQ+TT7sqPG4VEtWKwVSlMInwPUYyJpA==} - dependencies: - validator: 13.12.0 + /@poppinss/exception@1.2.3: + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} dev: true /@prisma/instrumentation@5.22.0: @@ -9990,26 +10184,14 @@ packages: tslib: 2.8.1 dev: false - /@simplewebauthn/browser@13.1.2: - resolution: {integrity: sha512-aZnW0KawAM83fSBUgglP5WofbrLbLyr7CoPqYr66Eppm7zO86YX6rrCjRB3hQKPrL7ATvY4FVXlykZ6w6FwYYw==} - dev: false - - /@simplewebauthn/server@13.1.2: - resolution: {integrity: sha512-VwoDfvLXSCaRiD+xCIuyslU0HLxVggeE5BL06+GbsP2l1fGf5op8e0c3ZtKoi+vSg1q4ikjtAghC23ze2Q3H9g==} - engines: {node: '>=20.0.0'} - dependencies: - '@hexagon/base64': 1.1.28 - '@levischuck/tiny-cbor': 0.2.11 - '@peculiar/asn1-android': 2.4.0 - '@peculiar/asn1-ecc': 2.4.0 - '@peculiar/asn1-rsa': 2.4.0 - '@peculiar/asn1-schema': 2.4.0 - '@peculiar/asn1-x509': 2.4.0 - dev: false - /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + /@sindresorhus/is@7.1.1: + resolution: {integrity: sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==} + engines: {node: '>=18'} + dev: true + /@sinonjs/commons@3.0.1: resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} dependencies: @@ -10020,6 +10202,14 @@ packages: dependencies: '@sinonjs/commons': 3.0.1 + /@speed-highlight/core@1.2.12: + resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==} + dev: true + + /@standard-schema/spec@1.1.0: + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + dev: false + /@standard-schema/utils@0.3.0: resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} dev: false @@ -10767,28 +10957,28 @@ packages: whatwg-fetch: 3.6.20 dev: false - /@tursodatabase/database-common@0.2.2: - resolution: {integrity: sha512-cSNpms6MIaRj29B37XzIu9yGbea0HSGDupZs8QrMxR3rKqgHJIfY04ysqDD/4lS8TNIImPlPZrZgqQz+b/FoKQ==} + /@tursodatabase/database-common@0.3.2: + resolution: {integrity: sha512-CPkPuvDMUS6mKIdAwpCbfU7g6U81/mSfCcMDXZ6Bt1dA8DJyOgTZRf80MDaWTKLQ+0mjkmQ0cZPb9zD/R41Vkg==} dev: false - /@tursodatabase/database-wasm-common@0.2.2: - resolution: {integrity: sha512-fBQk7+omGw0fjn4ZC1D1aCnj4kXRoqBwGoC63vpB6d6aR5i+es2ng1uII3vktUBsC4JmPLBj51/B59AIqM1e7w==} + /@tursodatabase/database-wasm-common@0.3.2: + resolution: {integrity: sha512-ljpo471synyHzse8unmD+d2azo32A9nbiONS43URWlvEhSOpnkhuE5MjNgP9m468f8VyEU2qjvbgqxH8Ko9Q6A==} dependencies: '@napi-rs/wasm-runtime': 1.0.7 dev: false - /@tursodatabase/sync-common@0.2.2: - resolution: {integrity: sha512-JkSSkMc2hz04bBOziqm3DWnGO+Bn9+7cHWiqLTi6IoyQevCWaP3j4wX4989iTYJUHD7RwW522v8duh1lN4VT5w==} + /@tursodatabase/sync-common@0.3.2: + resolution: {integrity: sha512-cUQEHUu79sbPNVi2bba2jTDz3GIN/HOf5bLkdMNqX5lXy78/H/HrnkflZ1QbjgutVLyIkz5jkd0PgvzojFwh+A==} dependencies: - '@tursodatabase/database-common': 0.2.2 + '@tursodatabase/database-common': 0.3.2 dev: false - /@tursodatabase/sync-wasm@0.2.2: - resolution: {integrity: sha512-BIrOsRDyDijm8ooNipDx++SEMkPJSpPQa9z2PVDpxd97jHsPzOGyq6sg7W6g2VJaXYtHVo8+c0xUXJtujAOBNg==} + /@tursodatabase/sync-wasm@0.3.2: + resolution: {integrity: sha512-R1JkmyjcJGtTz59sTWvyFWHvYa0Xiqkcs66EoVo99E4S7lXYGbwVSuulI7GY23T2sI3E895hOiKjSLoWPOkdnw==} dependencies: - '@tursodatabase/database-common': 0.2.2 - '@tursodatabase/database-wasm-common': 0.2.2 - '@tursodatabase/sync-common': 0.2.2 + '@tursodatabase/database-common': 0.3.2 + '@tursodatabase/database-wasm-common': 0.3.2 + '@tursodatabase/sync-common': 0.3.2 dev: false /@tybys/wasm-util@0.10.1: @@ -10827,6 +11017,7 @@ packages: dependencies: '@types/connect': 3.4.38 '@types/node': 20.10.6 + dev: true /@types/connect@3.4.36: resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} @@ -10838,12 +11029,13 @@ packages: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: '@types/node': 20.10.6 + dev: true /@types/cookie-parser@1.4.6: resolution: {integrity: sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==} dependencies: '@types/express': 4.17.21 - dev: false + dev: true /@types/cors@2.8.17: resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} @@ -10857,10 +11049,6 @@ packages: '@types/ms': 2.1.0 dev: false - /@types/dom-speech-recognition@0.0.1: - resolution: {integrity: sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw==} - dev: false - /@types/eslint-scope@3.7.7: resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: @@ -10895,6 +11083,7 @@ packages: '@types/qs': 6.9.11 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 + dev: true /@types/express@4.17.21: resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} @@ -10903,6 +11092,7 @@ packages: '@types/express-serve-static-core': 4.17.43 '@types/qs': 6.9.11 '@types/serve-static': 1.15.5 + dev: true /@types/fontkit@2.0.8: resolution: {integrity: sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew==} @@ -10910,10 +11100,6 @@ packages: '@types/node': 20.10.6 dev: false - /@types/google.maps@3.58.1: - resolution: {integrity: sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==} - dev: false - /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: @@ -10928,12 +11114,9 @@ packages: '@types/unist': 3.0.3 dev: false - /@types/hogan.js@3.0.5: - resolution: {integrity: sha512-/uRaY3HGPWyLqOyhgvW9Aa43BNnLZrNeQxl2p8wqId4UHMfPKolSB+U7BlZyO1ng7MkLnyEAItsBzCG0SDhqrA==} - dev: false - /@types/http-errors@2.0.4: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + dev: true /@types/istanbul-lib-coverage@2.0.6: resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -10965,6 +11148,7 @@ packages: /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true /@types/json5@0.0.29: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -10982,9 +11166,11 @@ packages: /@types/mime@1.3.5: resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + dev: true /@types/mime@3.0.4: resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} + dev: true /@types/ms@2.1.0: resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -11008,12 +11194,6 @@ packages: '@types/unist': 3.0.3 dev: false - /@types/node-forge@1.3.11: - resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} - dependencies: - '@types/node': 20.10.6 - dev: true - /@types/node@17.0.45: resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} dev: false @@ -11079,9 +11259,11 @@ packages: /@types/qs@6.9.11: resolution: {integrity: sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==} + dev: true /@types/range-parser@1.2.7: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + dev: true /@types/react-dom@19.0.0: resolution: {integrity: sha512-1KfiQKsH1o00p9m5ag12axHQSb3FOU9H20UTrujVSkNhuCrRHiQWFqgEnTNK5ZNfnzZv8UWrnXVqCmCF9fgY3w==} @@ -11135,6 +11317,7 @@ packages: dependencies: '@types/mime': 1.3.5 '@types/node': 20.10.6 + dev: true /@types/serve-static@1.15.5: resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} @@ -11142,6 +11325,7 @@ packages: '@types/http-errors': 2.0.4 '@types/mime': 3.0.4 '@types/node': 20.10.6 + dev: true /@types/shimmer@1.2.0: resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} @@ -11640,10 +11824,10 @@ packages: /@ungap/structured-clone@1.3.0: resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - /@upstash/redis@1.28.4: - resolution: {integrity: sha512-UalkSAny/dz1m8giEhD3Y5ru1o+CPHI32wFyS3MyzDzj2TRvEN+lTw+mPwi20ojk0H2gs8TBW3qsrvwuLLy+pA==} + /@upstash/redis@1.36.0: + resolution: {integrity: sha512-9zN2UV9QJGPnXfWU3yZBLVQaqqENDh7g+Y4J2vJuSxBCi9FQ0aUOtaXlzuFhnsiZvCqM+eS27ic+tgmkWUsfOg==} dependencies: - crypto-js: 4.2.0 + uncrypto: 0.1.3 dev: false /@urql/core@5.1.0: @@ -11928,10 +12112,6 @@ packages: deprecated: Use your platform's native atob() and btoa() methods instead dev: true - /abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - dev: false - /abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -12081,35 +12261,6 @@ packages: require-from-string: 2.0.2 dev: true - /algoliasearch-helper@3.25.0(algoliasearch@5.35.0): - resolution: {integrity: sha512-vQoK43U6HXA9/euCqLjvyNdM4G2Fiu/VFp4ae0Gau9sZeIKBPvUPnXfLYAe65Bg7PFuw03coeu5K6lTPSXRObw==} - peerDependencies: - algoliasearch: '>= 3.1 < 6' - dependencies: - '@algolia/events': 4.0.1 - algoliasearch: 5.35.0 - dev: false - - /algoliasearch@5.35.0: - resolution: {integrity: sha512-Y+moNhsqgLmvJdgTsO4GZNgsaDWv8AOGAaPeIeHKlDn/XunoAqYbA+XNpBd1dW8GOXAUDyxC9Rxc7AV4kpFcIg==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/abtesting': 1.1.0 - '@algolia/client-abtesting': 5.35.0 - '@algolia/client-analytics': 5.35.0 - '@algolia/client-common': 5.35.0 - '@algolia/client-insights': 5.35.0 - '@algolia/client-personalization': 5.35.0 - '@algolia/client-query-suggestions': 5.35.0 - '@algolia/client-search': 5.35.0 - '@algolia/ingestion': 1.35.0 - '@algolia/monitoring': 1.35.0 - '@algolia/recommend': 5.35.0 - '@algolia/requester-browser-xhr': 5.35.0 - '@algolia/requester-fetch': 5.35.0 - '@algolia/requester-node-http': 5.35.0 - dev: false - /anser@1.4.10: resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} @@ -12130,13 +12281,6 @@ packages: engines: {node: '>=14.16'} dev: true - /ansi-escapes@7.0.0: - resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} - engines: {node: '>=18'} - dependencies: - environment: 1.1.0 - dev: true - /ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} @@ -12329,15 +12473,6 @@ packages: /asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - /asn1js@3.0.6: - resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} - engines: {node: '>=12.0.0'} - dependencies: - pvtsutils: 1.3.6 - pvutils: 1.1.3 - tslib: 2.8.1 - dev: false - /ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} dev: true @@ -12412,9 +12547,9 @@ packages: xxhash-wasm: 1.1.0 yargs-parser: 21.1.1 yocto-spinner: 0.2.3 - zod: 3.24.1 - zod-to-json-schema: 3.24.5(zod@3.24.1) - zod-to-ts: 1.2.0(typescript@5.9.2)(zod@3.24.1) + zod: 3.25.76 + zod-to-json-schema: 3.25.0(zod@3.25.76) + zod-to-ts: 1.2.0(typescript@5.9.2)(zod@3.25.76) optionalDependencies: sharp: 0.34.5 transitivePeerDependencies: @@ -12780,48 +12915,205 @@ packages: peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.28.3 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.3) - - /bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - dev: false - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - /base-64@1.0.0: - resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + '@babel/core': 7.28.3 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.3) + + /bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + dev: false + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + /better-auth@1.4.9(drizzle-kit@0.31.8)(drizzle-orm@0.45.1)(react-dom@19.1.0)(react@19.1.0): + resolution: {integrity: sha512-usSdjuyTzZwIvM8fjF8YGhPncxV3MAg3dHUO9uPUnf0yklXUSYISiH1+imk6/Z+UBqsscyyPRnbIyjyK97p7YA==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + better-sqlite3: ^12.0.0 + drizzle-kit: '>=0.31.4' + drizzle-orm: '>=0.41.0' + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + better-sqlite3: + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + dependencies: + '@better-auth/core': 1.4.9(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7)(jose@6.1.3)(kysely@0.28.5)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.9(@better-auth/core@1.4.9) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 + '@noble/hashes': 2.0.1 + better-call: 1.1.7(zod@4.0.17) + defu: 6.1.4 + drizzle-kit: 0.31.8 + drizzle-orm: 0.45.1(@libsql/client@0.10.0)(@opentelemetry/api@1.9.0)(@upstash/redis@1.36.0)(kysely@0.28.5) + jose: 6.1.3 + kysely: 0.28.5 + nanostores: 1.1.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + zod: 4.2.1 + dev: false + + /better-auth@1.4.9(drizzle-orm@0.45.1)(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-usSdjuyTzZwIvM8fjF8YGhPncxV3MAg3dHUO9uPUnf0yklXUSYISiH1+imk6/Z+UBqsscyyPRnbIyjyK97p7YA==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + better-sqlite3: ^12.0.0 + drizzle-kit: '>=0.31.4' + drizzle-orm: '>=0.41.0' + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + better-sqlite3: + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + dependencies: + '@better-auth/core': 1.4.9(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.7)(jose@6.1.3)(kysely@0.28.5)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.9(@better-auth/core@1.4.9) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 + '@noble/hashes': 2.0.1 + better-call: 1.1.7(zod@4.0.17) + defu: 6.1.4 + drizzle-orm: 0.45.1(@libsql/client@0.10.0)(@opentelemetry/api@1.9.0)(@upstash/redis@1.36.0)(kysely@0.28.5) + jose: 6.1.3 + kysely: 0.28.5 + nanostores: 1.1.0 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + zod: 4.2.1 dev: false - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - /better-auth@1.2.10: - resolution: {integrity: sha512-nEj1RG4DdLUuJiV5CR93ORyPCptGRBwksaPPCkUtGo9ka+UIlTpaiKoTaTqVLLYlqwX4bOj9tJ32oBNdf2G3Kg==} + /better-call@1.1.7(zod@4.0.17): + resolution: {integrity: sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true dependencies: - '@better-auth/utils': 0.2.5 - '@better-fetch/fetch': 1.1.18 - '@noble/ciphers': 0.6.0 - '@noble/hashes': 1.8.0 - '@simplewebauthn/browser': 13.1.2 - '@simplewebauthn/server': 13.1.2 - better-call: 1.0.16 - defu: 6.1.4 - jose: 5.10.0 - kysely: 0.28.5 - nanostores: 0.11.4 - zod: 3.24.1 + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 2.7.1 + zod: 4.0.17 dev: false - /better-call@1.0.16: - resolution: {integrity: sha512-42dgJ1rOtc0anOoxjXPOWuel/Z/4aeO7EJ2SiXNwvlkySSgjXhNjAjTMWa8DL1nt6EXS3jl3VKC3mPsU/lUgVA==} + /better-call@1.1.7(zod@4.2.1): + resolution: {integrity: sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true dependencies: - '@better-fetch/fetch': 1.1.18 - rou3: 0.5.1 + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 set-cookie-parser: 2.7.1 - uncrypto: 0.1.3 + zod: 4.2.1 dev: false /better-opn@3.0.2: @@ -12991,16 +13283,6 @@ packages: run-applescript: 5.0.0 dev: true - /bundle-require@5.0.0(esbuild@0.21.5): - resolution: {integrity: sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.18' - dependencies: - esbuild: 0.21.5 - load-tsconfig: 0.2.5 - dev: true - /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -13068,15 +13350,6 @@ packages: /caniuse-lite@1.0.30001737: resolution: {integrity: sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==} - /capnp-ts@0.7.0: - resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} - dependencies: - debug: 4.4.3 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - dev: true - /ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} dev: false @@ -13244,6 +13517,7 @@ packages: /cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} + dev: false /cli-cursor@2.1.0: resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} @@ -13257,40 +13531,16 @@ packages: dependencies: restore-cursor: 3.1.0 - /cli-cursor@5.0.0: - resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} - engines: {node: '>=18'} - dependencies: - restore-cursor: 5.1.0 - dev: true - /cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} - /cli-table3@0.6.5: - resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} - engines: {node: 10.* || >= 12.*} - dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 - dev: true - /cli-table@0.3.11: resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} engines: {node: '>= 0.2.0'} dependencies: colors: 1.0.3 - /cli-truncate@4.0.0: - resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} - engines: {node: '>=18'} - dependencies: - slice-ansi: 5.0.0 - string-width: 7.2.0 - dev: true - /cli-width@3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} @@ -13444,10 +13694,6 @@ packages: engines: {node: '>=4.0.0'} dev: true - /component-emitter@1.3.1: - resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} - dev: true - /compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -13541,11 +13787,6 @@ packages: /cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - dev: false - - /cookiejar@2.1.4: - resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - dev: true /core-js-compat@3.45.1: resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==} @@ -13652,10 +13893,6 @@ packages: uncrypto: 0.1.3 dev: false - /crypto-js@4.2.0: - resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} - dev: false - /crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -13791,10 +14028,6 @@ packages: /date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} - /date-fns@4.1.0: - resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} - dev: true - /dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} dev: true @@ -13842,18 +14075,6 @@ packages: dependencies: ms: 2.1.3 - /debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - dev: true - /debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -14025,13 +14246,6 @@ packages: dequal: 2.0.3 dev: false - /dezalgo@1.0.4: - resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} - dependencies: - asap: 2.0.6 - wrappy: 1.0.2 - dev: true - /dfa@1.2.0: resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} dev: false @@ -14127,48 +14341,47 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} - /drizzle-kit@0.28.1: - resolution: {integrity: sha512-JimOV+ystXTWMgZkLHYHf2w3oS28hxiH1FR0dkmJLc7GHzdGJoJAQtQS5DRppnabsRZwE2U1F6CuezVBgmsBBQ==} + /drizzle-kit@0.31.8: + resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==} hasBin: true dependencies: '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 - esbuild: 0.19.12 - esbuild-register: 3.5.0(esbuild@0.19.12) + esbuild: 0.25.12 + esbuild-register: 3.5.0(esbuild@0.25.12) transitivePeerDependencies: - supports-color - dev: true - /drizzle-orm@0.36.4(@libsql/client@0.10.0)(@opentelemetry/api@1.9.0)(react@19.1.0): - resolution: {integrity: sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA==} + /drizzle-orm@0.45.1(@libsql/client@0.10.0)(@opentelemetry/api@1.9.0)(@upstash/redis@1.36.0)(kysely@0.28.5): + resolution: {integrity: sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' - '@cloudflare/workers-types': '>=3' + '@cloudflare/workers-types': '>=4' '@electric-sql/pglite': '>=0.2.0' '@libsql/client': '>=0.10.0' '@libsql/client-wasm': '>=0.10.0' '@neondatabase/serverless': '>=0.10.0' '@op-engineering/op-sqlite': '>=2' '@opentelemetry/api': ^1.4.1 - '@planetscale/database': '>=1' + '@planetscale/database': '>=1.13' '@prisma/client': '*' '@tidbcloud/serverless': '*' '@types/better-sqlite3': '*' '@types/pg': '*' - '@types/react': '>=18' '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' '@vercel/postgres': '>=0.8.0' '@xata.io/client': '*' better-sqlite3: '>=7' bun-types: '*' expo-sqlite: '>=14.0.0' + gel: '>=2' knex: '*' kysely: '*' mysql2: '>=2' pg: '>=8' postgres: '>=3' prisma: '*' - react: '>=18' sql.js: '>=1' sqlite3: '>=5' peerDependenciesMeta: @@ -14198,10 +14411,10 @@ packages: optional: true '@types/pg': optional: true - '@types/react': - optional: true '@types/sql.js': optional: true + '@upstash/redis': + optional: true '@vercel/postgres': optional: true '@xata.io/client': @@ -14212,6 +14425,8 @@ packages: optional: true expo-sqlite: optional: true + gel: + optional: true knex: optional: true kysely: @@ -14224,8 +14439,6 @@ packages: optional: true prisma: optional: true - react: - optional: true sql.js: optional: true sqlite3: @@ -14233,113 +14446,18 @@ packages: dependencies: '@libsql/client': 0.10.0 '@opentelemetry/api': 1.9.0 - react: 19.1.0 - dev: false - - /drizzle-orm@0.36.4(@types/react@19.0.0)(react@19.0.0): - resolution: {integrity: sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA==} - peerDependencies: - '@aws-sdk/client-rds-data': '>=3' - '@cloudflare/workers-types': '>=3' - '@electric-sql/pglite': '>=0.2.0' - '@libsql/client': '>=0.10.0' - '@libsql/client-wasm': '>=0.10.0' - '@neondatabase/serverless': '>=0.10.0' - '@op-engineering/op-sqlite': '>=2' - '@opentelemetry/api': ^1.4.1 - '@planetscale/database': '>=1' - '@prisma/client': '*' - '@tidbcloud/serverless': '*' - '@types/better-sqlite3': '*' - '@types/pg': '*' - '@types/react': '>=18' - '@types/sql.js': '*' - '@vercel/postgres': '>=0.8.0' - '@xata.io/client': '*' - better-sqlite3: '>=7' - bun-types: '*' - expo-sqlite: '>=14.0.0' - knex: '*' - kysely: '*' - mysql2: '>=2' - pg: '>=8' - postgres: '>=3' - prisma: '*' - react: '>=18' - sql.js: '>=1' - sqlite3: '>=5' - peerDependenciesMeta: - '@aws-sdk/client-rds-data': - optional: true - '@cloudflare/workers-types': - optional: true - '@electric-sql/pglite': - optional: true - '@libsql/client': - optional: true - '@libsql/client-wasm': - optional: true - '@neondatabase/serverless': - optional: true - '@op-engineering/op-sqlite': - optional: true - '@opentelemetry/api': - optional: true - '@planetscale/database': - optional: true - '@prisma/client': - optional: true - '@tidbcloud/serverless': - optional: true - '@types/better-sqlite3': - optional: true - '@types/pg': - optional: true - '@types/react': - optional: true - '@types/sql.js': - optional: true - '@vercel/postgres': - optional: true - '@xata.io/client': - optional: true - better-sqlite3: - optional: true - bun-types: - optional: true - expo-sqlite: - optional: true - knex: - optional: true - kysely: - optional: true - mysql2: - optional: true - pg: - optional: true - postgres: - optional: true - prisma: - optional: true - react: - optional: true - sql.js: - optional: true - sqlite3: - optional: true - dependencies: - '@types/react': 19.0.0 - react: 19.0.0 + '@upstash/redis': 1.36.0 + kysely: 0.28.5 dev: false - /drizzle-zod@0.5.1(drizzle-orm@0.36.4)(zod@3.24.1): - resolution: {integrity: sha512-C/8bvzUH/zSnVfwdSibOgFjLhtDtbKYmkbPbUCq46QZyZCH6kODIMSOgZ8R7rVjoI+tCj3k06MRJMDqsIeoS4A==} + /drizzle-zod@0.8.3(drizzle-orm@0.45.1)(zod@4.0.17): + resolution: {integrity: sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww==} peerDependencies: - drizzle-orm: '>=0.23.13' - zod: 3.24.1 + drizzle-orm: '>=0.36.0' + zod: ^3.25.0 || ^4.0.0 dependencies: - drizzle-orm: 0.36.4(@libsql/client@0.10.0)(@opentelemetry/api@1.9.0)(react@19.1.0) - zod: 3.24.1 + drizzle-orm: 0.45.1(@libsql/client@0.10.0)(@opentelemetry/api@1.9.0)(@upstash/redis@1.36.0)(kysely@0.28.5) + zod: 4.0.17 dev: false /dset@3.1.4: @@ -14390,6 +14508,7 @@ packages: /emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + dev: false /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -14426,11 +14545,6 @@ packages: resolution: {integrity: sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==} engines: {node: '>=8'} - /environment@1.1.0: - resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} - engines: {node: '>=18'} - dev: true - /eol@0.9.1: resolution: {integrity: sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==} @@ -14439,6 +14553,10 @@ packages: dependencies: is-arrayish: 0.2.1 + /error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + dev: true + /error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} dependencies: @@ -14654,16 +14772,15 @@ packages: engines: {node: '>=v14.0.0', npm: '>=7.0.0'} dev: true - /esbuild-register@3.5.0(esbuild@0.19.12): + /esbuild-register@3.5.0(esbuild@0.25.12): resolution: {integrity: sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==} peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.4.1 - esbuild: 0.19.12 + debug: 4.4.3 + esbuild: 0.25.12 transitivePeerDependencies: - supports-color - dev: true /esbuild@0.17.19: resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} @@ -14723,7 +14840,6 @@ packages: '@esbuild/win32-arm64': 0.18.20 '@esbuild/win32-ia32': 0.18.20 '@esbuild/win32-x64': 0.18.20 - dev: true /esbuild@0.19.12: resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} @@ -14754,6 +14870,7 @@ packages: '@esbuild/win32-arm64': 0.19.12 '@esbuild/win32-ia32': 0.19.12 '@esbuild/win32-x64': 0.19.12 + dev: false /esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} @@ -14817,6 +14934,39 @@ packages: '@esbuild/win32-x64': 0.23.1 dev: true + /esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + /esbuild@0.25.2: resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} engines: {node: '>=18'} @@ -14849,6 +14999,40 @@ packages: '@esbuild/win32-ia32': 0.25.2 '@esbuild/win32-x64': 0.25.2 + /esbuild@0.27.0: + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.0 + '@esbuild/android-arm': 0.27.0 + '@esbuild/android-arm64': 0.27.0 + '@esbuild/android-x64': 0.27.0 + '@esbuild/darwin-arm64': 0.27.0 + '@esbuild/darwin-x64': 0.27.0 + '@esbuild/freebsd-arm64': 0.27.0 + '@esbuild/freebsd-x64': 0.27.0 + '@esbuild/linux-arm': 0.27.0 + '@esbuild/linux-arm64': 0.27.0 + '@esbuild/linux-ia32': 0.27.0 + '@esbuild/linux-loong64': 0.27.0 + '@esbuild/linux-mips64el': 0.27.0 + '@esbuild/linux-ppc64': 0.27.0 + '@esbuild/linux-riscv64': 0.27.0 + '@esbuild/linux-s390x': 0.27.0 + '@esbuild/linux-x64': 0.27.0 + '@esbuild/netbsd-arm64': 0.27.0 + '@esbuild/netbsd-x64': 0.27.0 + '@esbuild/openbsd-arm64': 0.27.0 + '@esbuild/openbsd-x64': 0.27.0 + '@esbuild/openharmony-arm64': 0.27.0 + '@esbuild/sunos-x64': 0.27.0 + '@esbuild/win32-arm64': 0.27.0 + '@esbuild/win32-ia32': 0.27.0 + '@esbuild/win32-x64': 0.27.0 + dev: true + /escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -16223,15 +16407,6 @@ packages: fetch-blob: 3.2.0 dev: false - /formidable@2.1.2: - resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} - dependencies: - dezalgo: 1.0.4 - hexoid: 1.0.0 - once: 1.4.0 - qs: 6.11.0 - dev: true - /forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} dev: false @@ -16330,6 +16505,7 @@ packages: /get-east-asian-width@1.3.0: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} + dev: false /get-intrinsic@1.2.2: resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} @@ -16422,7 +16598,6 @@ packages: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} dependencies: resolve-pkg-maps: 1.0.0 - dev: true /getenv@2.0.0: resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==} @@ -16577,12 +16752,6 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true - /graphlib@2.1.8: - resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} - dependencies: - lodash: 4.17.21 - dev: true - /h3@1.15.4: resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} dependencies: @@ -16843,19 +17012,6 @@ packages: dependencies: hermes-estree: 0.32.0 - /hexoid@1.0.0: - resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} - engines: {node: '>=8'} - dev: true - - /hogan.js@3.0.2: - resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==} - hasBin: true - dependencies: - mkdirp: 0.3.0 - nopt: 1.0.10 - dev: false - /hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} dependencies: @@ -16871,10 +17027,6 @@ packages: dependencies: lru-cache: 10.4.3 - /htm@3.1.1: - resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} - dev: false - /html-encoding-sniffer@3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -17035,19 +17187,6 @@ packages: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} dev: false - /importx@0.5.1: - resolution: {integrity: sha512-YrRaigAec1sC2CdIJjf/hCH1Wp9Ii8Cq5ROw4k5nJ19FVl2FcJUHZ5gGIb1vs8+JNYIyOJpc2fcufS2330bxDw==} - dependencies: - bundle-require: 5.0.0(esbuild@0.21.5) - debug: 4.4.3 - esbuild: 0.21.5 - jiti: 2.6.1 - pathe: 1.1.2 - tsx: 4.19.2 - transitivePeerDependencies: - - supports-color - dev: true - /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -17106,32 +17245,6 @@ packages: strip-ansi: 6.0.1 through: 2.3.8 - /instantsearch-ui-components@0.11.1: - resolution: {integrity: sha512-ZqUbJYYgObQ47J08ftXV1KNC1vdEoiD4/49qrkCdW46kRzLxLgYXJGuEuk48DQwK4aBtIoccgTyfbMGfcqNjxg==} - dependencies: - '@babel/runtime': 7.28.3 - dev: false - - /instantsearch.js@4.78.3(algoliasearch@5.35.0): - resolution: {integrity: sha512-0i7vyX9jHEIKSfhu+CZHL/ySnbMAe7e98YUJiZX5D7AiXo2WvAPbnV/3CXIPR0whNWOXKGvlv7Ji7Pt4Yrn+Aw==} - peerDependencies: - algoliasearch: '>= 3.1 < 6' - dependencies: - '@algolia/events': 4.0.1 - '@types/dom-speech-recognition': 0.0.1 - '@types/google.maps': 3.58.1 - '@types/hogan.js': 3.0.5 - '@types/qs': 6.9.11 - algoliasearch: 5.35.0 - algoliasearch-helper: 3.25.0(algoliasearch@5.35.0) - hogan.js: 3.0.2 - htm: 3.1.1 - instantsearch-ui-components: 0.11.1 - preact: 10.19.6 - qs: 6.9.7 - search-insights: 2.17.3 - dev: false - /internal-slot@1.0.6: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} engines: {node: '>= 0.4'} @@ -17331,18 +17444,6 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - /is-fullwidth-code-point@4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} - dev: true - - /is-fullwidth-code-point@5.0.0: - resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} - engines: {node: '>=18'} - dependencies: - get-east-asian-width: 1.3.0 - dev: true - /is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} @@ -17677,10 +17778,6 @@ packages: set-function-name: 2.0.1 dev: true - /itty-time@1.0.6: - resolution: {integrity: sha512-+P8IZaLLBtFv8hCkIjcymZOp4UJ+xW6bSlQsXGqrkmJh7vSiMFSlNne0mCYagEE0N7HDNR5jJBRxwN0oYv61Rw==} - dev: true - /jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} dependencies: @@ -18212,12 +18309,8 @@ packages: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} dev: true - /jose@5.10.0: - resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} - dev: false - - /jose@6.0.13: - resolution: {integrity: sha512-Yms4GpbmdANamS51kKK6w4hRlKx8KTxbWyAAKT/MhUMtqbIqh5mb2HjhTNUbk7TFL8/MBB5zWSDohL7ed4k/UA==} + /jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} dev: false /jotai@2.12.4(@types/react@19.1.17)(react@19.1.0): @@ -18351,28 +18444,6 @@ packages: /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - /json-refs@3.0.15: - resolution: {integrity: sha512-0vOQd9eLNBL18EGl5yYaO44GhixmImes2wiYn9Z3sag3QnehWrYWlB9AFtMxCL2Bj3fyxgDYkxGFEU/chlYssw==} - engines: {node: '>=0.8'} - hasBin: true - dependencies: - commander: 4.1.1 - graphlib: 2.1.8 - js-yaml: 3.14.1 - lodash: 4.17.21 - native-promise-only: 0.8.1 - path-loader: 1.0.12 - slash: 3.0.0 - uri-js: 4.4.1 - transitivePeerDependencies: - - supports-color - dev: true - - /json-schema-to-zod@2.4.1: - resolution: {integrity: sha512-aMoez9TxgnfLAIZaWTPaQ+j7rOt1K9Ew/TBI85XcnhcFlo/47b1MDgpi4r07XndLSZWOX/KsJiRJvhdzSvo2Dw==} - hasBin: true - dev: true - /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true @@ -18743,11 +18814,6 @@ packages: /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - /load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - /loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} @@ -18801,17 +18867,6 @@ packages: chalk: 4.1.2 is-unicode-supported: 0.1.0 - /log-update@6.1.0: - resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} - engines: {node: '>=18'} - dependencies: - ansi-escapes: 7.0.0 - cli-cursor: 5.0.0 - slice-ansi: 7.1.0 - strip-ansi: 7.1.0 - wrap-ansi: 9.0.0 - dev: true - /longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} dev: false @@ -19135,18 +19190,6 @@ packages: engines: {node: '>= 0.6'} dev: false - /meilisearch@0.37.0: - resolution: {integrity: sha512-LdbK6JmRghCawrmWKJSEQF0OiE82md+YqJGE/U2JcCD8ROwlhTx0KM6NX4rQt0u0VpV0QZVG9umYiu3CSSIJAQ==} - dependencies: - cross-fetch: 3.1.8 - transitivePeerDependencies: - - encoding - dev: false - - /meilisearch@0.45.0: - resolution: {integrity: sha512-+zCzEqE+CumY4icB0Vox180adZqaNtnr60hJWGiEdmol5eWmksfY8rYsTcz87styXC2ZOg+2yF56gdH6oyIBTA==} - dev: false - /memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} @@ -19168,6 +19211,7 @@ packages: /methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + dev: false /metro-babel-transformer@0.83.2: resolution: {integrity: sha512-rirY1QMFlA1uxH3ZiNauBninwTioOgwChnRdDcbB4tgRZ+bGX9DiXoh9QdpppiaVKXdJsII932OwWXGGV4+Nlw==} @@ -19737,12 +19781,6 @@ packages: engines: {node: '>=4'} hasBin: true - /mime@2.6.0: - resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} - engines: {node: '>=4.0.0'} - hasBin: true - dev: true - /mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -19762,42 +19800,35 @@ packages: engines: {node: '>=12'} dev: true - /mimic-function@5.0.1: - resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} - engines: {node: '>=18'} - dev: true - /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} dev: true - /miniflare@3.20241106.1: - resolution: {integrity: sha512-dM3RBlJE8rUFxnqlPCaFCq0E7qQqEQvKbYX7W/APGCK+rLcyLmEBzC4GQR/niXdNM/oV6gdg9AA50ghnn2ALuw==} + /miniflare@3.20250718.2: + resolution: {integrity: sha512-cW/NQPBKc+fb0FwcEu+z/v93DZd+/6q/AF0iR0VFELtNPOsCvLalq6ndO743A7wfZtFxMxvuDQUXNx3aKQhOwA==} engines: {node: '>=16.13'} hasBin: true dependencies: '@cspotcode/source-map-support': 0.8.1 - acorn: 8.15.0 - acorn-walk: 8.3.4 - capnp-ts: 0.7.0 + acorn: 8.14.0 + acorn-walk: 8.3.2 exit-hook: 2.2.1 glob-to-regexp: 0.4.1 stoppable: 1.1.0 - undici: 5.28.4 - workerd: 1.20241106.1 + undici: 5.29.0 + workerd: 1.20250718.0 ws: 8.18.0 youch: 3.3.4 - zod: 3.24.1 + zod: 3.22.3 transitivePeerDependencies: - bufferutil - - supports-color - utf-8-validate dev: true - /miniflare@3.20250718.2: - resolution: {integrity: sha512-cW/NQPBKc+fb0FwcEu+z/v93DZd+/6q/AF0iR0VFELtNPOsCvLalq6ndO743A7wfZtFxMxvuDQUXNx3aKQhOwA==} - engines: {node: '>=16.13'} + /miniflare@4.20251210.0: + resolution: {integrity: sha512-k6kIoXwGVqlPZb0hcn+X7BmnK+8BjIIkusQPY22kCo2RaQJ/LzAjtxHQdGXerlHSnJyQivDQsL6BJHMpQfUFyw==} + engines: {node: '>=18.0.0'} hasBin: true dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -19805,12 +19836,13 @@ packages: acorn-walk: 8.3.2 exit-hook: 2.2.1 glob-to-regexp: 0.4.1 + sharp: 0.33.5 stoppable: 1.1.0 - undici: 5.29.0 - workerd: 1.20250718.0 + undici: 7.14.0 + workerd: 1.20251210.0 ws: 8.18.0 - youch: 3.3.4 - zod: 3.24.1 + youch: 4.1.0-beta.10 + zod: 3.22.3 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -19872,11 +19904,6 @@ packages: dependencies: minipass: 7.1.2 - /mkdirp@0.3.0: - resolution: {integrity: sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==} - deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.) - dev: false - /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -19988,15 +20015,11 @@ packages: hasBin: true dev: false - /nanostores@0.11.4: - resolution: {integrity: sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==} - engines: {node: ^18.0.0 || >=20.0.0} + /nanostores@1.1.0: + resolution: {integrity: sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==} + engines: {node: ^20.0.0 || >=22.0.0} dev: false - /native-promise-only@0.8.1: - resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==} - dev: true - /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -20077,13 +20100,6 @@ packages: /node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - /nopt@1.0.10: - resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} - hasBin: true - dependencies: - abbrev: 1.1.1 - dev: false - /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -20229,10 +20245,6 @@ packages: ufo: 1.6.1 dev: false - /ohash@1.1.6: - resolution: {integrity: sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==} - dev: true - /ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} @@ -20280,13 +20292,6 @@ packages: mimic-fn: 4.0.0 dev: true - /onetime@7.0.0: - resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} - engines: {node: '>=18'} - dependencies: - mimic-function: 5.0.1 - dev: true - /oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} dev: false @@ -20542,15 +20547,6 @@ packages: engines: {node: '>=12'} dev: true - /path-loader@1.0.12: - resolution: {integrity: sha512-n7oDG8B+k/p818uweWrOixY9/Dsr89o2TkCm6tOTex3fpdo2+BFDgR+KpB37mGKBRsBAlR8CIJMFN0OEy/7hIQ==} - dependencies: - native-promise-only: 0.8.1 - superagent: 7.1.6 - transitivePeerDependencies: - - supports-color - dev: true - /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -20779,10 +20775,6 @@ packages: xtend: 4.0.2 dev: false - /preact@10.19.6: - resolution: {integrity: sha512-gympg+T2Z1fG1unB8NH29yHJwnEaCH37Z32diPDku316OTnRPeMbiRV9kTrfZpocXjdfnWuFUl/Mj4BHaf6gnw==} - dev: false - /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -20834,11 +20826,6 @@ packages: ansi-styles: 5.2.0 react-is: 18.2.0 - /pretty-hrtime@1.0.3: - resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} - engines: {node: '>= 0.8'} - dev: true - /printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} dev: true @@ -20941,17 +20928,6 @@ packages: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} dev: true - /pvtsutils@1.3.6: - resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} - dependencies: - tslib: 2.8.1 - dev: false - - /pvutils@1.1.3: - resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} - engines: {node: '>=6.0.0'} - dev: false - /qrcode-terminal@0.11.0: resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==} hasBin: true @@ -20961,10 +20937,6 @@ packages: engines: {node: '>=0.6'} dependencies: side-channel: 1.0.4 - - /qs@6.9.7: - resolution: {integrity: sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==} - engines: {node: '>=0.6'} dev: false /query-string@7.1.3: @@ -21080,36 +21052,6 @@ packages: react: 19.1.0 dev: false - /react-instantsearch-core@7.15.8(algoliasearch@5.35.0)(react@19.0.0): - resolution: {integrity: sha512-A9AV5iurHw4QwRIraWMcBXJvFUm+lsKg8nPmvB+JVnM4nY9eJqthPiM9SOuTe1xIPvj3Gnn/nMBVxj0RB6h1IA==} - peerDependencies: - algoliasearch: '>= 3.1 < 6' - react: '>= 16.8.0 < 20' - dependencies: - '@babel/runtime': 7.28.3 - algoliasearch: 5.35.0 - algoliasearch-helper: 3.25.0(algoliasearch@5.35.0) - instantsearch.js: 4.78.3(algoliasearch@5.35.0) - react: 19.0.0 - use-sync-external-store: 1.4.0(react@19.0.0) - dev: false - - /react-instantsearch@7.15.8(algoliasearch@5.35.0)(react-dom@19.0.0)(react@19.0.0): - resolution: {integrity: sha512-mQUQ32e60dLp/Cumw02OAmcEZox1QeaVrjf/YonEywtYQdJR/tUIuctP3g6yJcWxsRg5gmAWprGrpYMY0OQZFQ==} - peerDependencies: - algoliasearch: '>= 3.1 < 6' - react: '>= 16.8.0 < 20' - react-dom: '>= 16.8.0 < 20' - dependencies: - '@babel/runtime': 7.26.7 - algoliasearch: 5.35.0 - instantsearch-ui-components: 0.11.1 - instantsearch.js: 4.78.3(algoliasearch@5.35.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - react-instantsearch-core: 7.15.8(algoliasearch@5.35.0)(react@19.0.0) - dev: false - /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -22002,14 +21944,6 @@ packages: onetime: 5.1.2 signal-exit: 3.0.7 - /restore-cursor@5.1.0: - resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} - engines: {node: '>=18'} - dependencies: - onetime: 7.0.0 - signal-exit: 4.1.0 - dev: true - /restructure@3.0.2: resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} dev: false @@ -22116,8 +22050,8 @@ packages: '@rollup/rollup-win32-x64-msvc': 4.53.3 fsevents: 2.3.3 - /rou3@0.5.1: - resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} + /rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} dev: false /rtl-detect@1.1.2: @@ -22234,10 +22168,6 @@ packages: ajv-keywords: 5.1.0(ajv@8.17.1) dev: true - /search-insights@2.17.3: - resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} - dev: false - /secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} dev: true @@ -22248,14 +22178,6 @@ packages: parseley: 0.12.1 dev: false - /selfsigned@2.4.1: - resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} - engines: {node: '>=10'} - dependencies: - '@types/node-forge': 1.3.11 - node-forge: 1.3.1 - dev: true - /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -22485,7 +22407,6 @@ packages: '@img/sharp-win32-ia32': 0.33.5 '@img/sharp-win32-x64': 0.33.5 dev: true - optional: true /sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} @@ -22648,22 +22569,6 @@ packages: engines: {node: '>=14.16'} dev: true - /slice-ansi@5.0.0: - resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} - engines: {node: '>=12'} - dependencies: - ansi-styles: 6.2.1 - is-fullwidth-code-point: 4.0.0 - dev: true - - /slice-ansi@7.1.0: - resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} - engines: {node: '>=18'} - dependencies: - ansi-styles: 6.2.1 - is-fullwidth-code-point: 5.0.0 - dev: true - /slugify@1.6.6: resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} engines: {node: '>=8.0.0'} @@ -22927,6 +22832,7 @@ packages: emoji-regex: 10.4.0 get-east-asian-width: 1.3.0 strip-ansi: 7.1.0 + dev: false /string.prototype.matchall@4.0.10: resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==} @@ -23139,24 +23045,9 @@ packages: resolution: {integrity: sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - /superagent@7.1.6: - resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} - engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net - dependencies: - component-emitter: 1.3.1 - cookiejar: 2.1.4 - debug: 4.4.3 - fast-safe-stringify: 2.1.1 - form-data: 4.0.0 - formidable: 2.1.2 - methods: 1.1.2 - mime: 2.6.0 - qs: 6.11.0 - readable-stream: 3.6.2 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color + /supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} dev: true /supports-color@5.5.0: @@ -23177,11 +23068,6 @@ packages: dependencies: has-flag: 4.0.0 - /supports-color@9.4.0: - resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} - engines: {node: '>=12'} - dev: true - /supports-hyperlinks@2.3.0: resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} engines: {node: '>=8'} @@ -23275,11 +23161,6 @@ packages: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - /terminal-size@4.0.0: - resolution: {integrity: sha512-rcdty1xZ2/BkWa4ANjWRp4JGpda2quksXIHgn5TMjNBPZfwzJIgR68DKfSYiTL+CZWowDX/sbOo5ME/FRURvYQ==} - engines: {node: '>=18'} - dev: true - /terser-webpack-plugin@5.3.14(webpack@5.101.3): resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} engines: {node: '>= 10.13.0'} @@ -23455,8 +23336,8 @@ packages: typescript: 5.9.2 dev: true - /ts-fsrs@4.5.1: - resolution: {integrity: sha512-JxBpNNl1jZRfS4xeDTecoGW2BZQhqXo/MGrUetTFySB9+uGRZF9jLrNf/wcC8ie62odN9lhB8WtBnksYBb5XUA==} + /ts-fsrs@5.2.3: + resolution: {integrity: sha512-R3IjceC9WfnvUin6Nx+DwqEzh3Qil6Gg2yEHqvocUcC7Nbi+xDrFg/1fKaYBT0tJedDnDAguXMSX0hijhi859w==} engines: {node: '>=18.0.0'} /ts-interface-checker@0.1.13: @@ -23771,16 +23652,6 @@ packages: which-boxed-primitive: 1.1.1 dev: true - /unconfig@0.6.0: - resolution: {integrity: sha512-4C67J0nIF2QwSXty2kW3zZx1pMZ3iXabylvJWWgHybWVUcMf9pxwsngoQt0gC+AVstRywFqrRBp3qOXJayhpOw==} - dependencies: - '@antfu/utils': 0.7.10 - defu: 6.1.4 - importx: 0.5.1 - transitivePeerDependencies: - - supports-color - dev: true - /uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} dev: false @@ -23788,13 +23659,6 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - /undici@5.28.4: - resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} - engines: {node: '>=14.0'} - dependencies: - '@fastify/busboy': 2.1.0 - dev: true - /undici@5.29.0: resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} engines: {node: '>=14.0'} @@ -23806,13 +23670,9 @@ packages: resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==} engines: {node: '>=18.17'} - /unenv-nightly@2.0.0-20241121-161142-806b5c0: - resolution: {integrity: sha512-RnFOasE/O0Q55gBkNB1b84OgKttgLEijGO0JCWpbn+O4XxpyCQg89NmcqQ5RGUiy4y+rMIrKzePTquQcLQF5pQ==} - dependencies: - defu: 6.1.4 - ohash: 1.1.6 - pathe: 1.1.2 - ufo: 1.6.1 + /undici@7.14.0: + resolution: {integrity: sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==} + engines: {node: '>=20.18.1'} dev: true /unenv@2.0.0-rc.14: @@ -23825,6 +23685,12 @@ packages: ufo: 1.6.1 dev: true + /unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + dependencies: + pathe: 2.0.3 + dev: true + /unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -24189,14 +24055,6 @@ packages: react: 19.1.0 tslib: 2.8.1 - /use-sync-external-store@1.4.0(react@19.0.0): - resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - dependencies: - react: 19.0.0 - dev: false - /use-sync-external-store@1.5.0(react@19.0.0): resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} peerDependencies: @@ -24247,11 +24105,6 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - /validator@13.12.0: - resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} - engines: {node: '>= 0.10'} - dev: true - /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -24404,7 +24257,7 @@ packages: yaml: optional: true dependencies: - esbuild: 0.25.2 + esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.3 @@ -24876,10 +24729,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - dev: true - /workbox-background-sync@7.3.0: resolution: {integrity: sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==} dependencies: @@ -25026,19 +24875,6 @@ packages: workbox-core: 7.3.0 dev: true - /workerd@1.20241106.1: - resolution: {integrity: sha512-1GdKl0kDw8rrirr/ThcK66Kbl4/jd4h8uHx5g7YHBrnenY5SX1UPuop2cnCzYUxlg55kPjzIqqYslz1muRFgFw==} - engines: {node: '>=16'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20241106.1 - '@cloudflare/workerd-darwin-arm64': 1.20241106.1 - '@cloudflare/workerd-linux-64': 1.20241106.1 - '@cloudflare/workerd-linux-arm64': 1.20241106.1 - '@cloudflare/workerd-windows-64': 1.20241106.1 - dev: true - /workerd@1.20250718.0: resolution: {integrity: sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==} engines: {node: '>=16'} @@ -25052,6 +24888,19 @@ packages: '@cloudflare/workerd-windows-64': 1.20250718.0 dev: true + /workerd@1.20251210.0: + resolution: {integrity: sha512-9MUUneP1BnRE9XAYi94FXxHmiLGbO75EHQZsgWqSiOXjoXSqJCw8aQbIEPxCy19TclEl/kHUFYce8ST2W+Qpjw==} + engines: {node: '>=16'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20251210.0 + '@cloudflare/workerd-darwin-arm64': 1.20251210.0 + '@cloudflare/workerd-linux-64': 1.20251210.0 + '@cloudflare/workerd-linux-arm64': 1.20251210.0 + '@cloudflare/workerd-windows-64': 1.20251210.0 + dev: true + /wrangler@3.114.15: resolution: {integrity: sha512-OpGikaV6t7AGXZImtGnVXI8WUnqBMFBCQcZzqKmQi0T/pZ5h8iSKhEZf7ItVB8bAG56yswHnWWYyANWF/Jj/JA==} engines: {node: '>=16.17.0'} @@ -25080,40 +24929,28 @@ packages: - utf-8-validate dev: true - /wrangler@3.91.0: - resolution: {integrity: sha512-Hdzn6wbY9cz5kL85ZUvWLwLIH7nPaEVRblfms40jhRf4qQO/Zf74aFlku8rQFbe8/2aVZFaxJVfBd6JQMeMSBQ==} - engines: {node: '>=16.17.0'} + /wrangler@4.54.0: + resolution: {integrity: sha512-bANFsjDwJLbprYoBK+hUDZsVbUv2SqJd8QvArLIcZk+fPq4h/Ohtj5vkKXD3k0s2bD1DXLk08D+hYmeNH+xC6A==} + engines: {node: '>=20.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20241106.0 + '@cloudflare/workers-types': ^4.20251210.0 peerDependenciesMeta: '@cloudflare/workers-types': optional: true dependencies: - '@cloudflare/kv-asset-handler': 0.3.4 - '@cloudflare/workers-shared': 0.9.0 - '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) - '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) + '@cloudflare/kv-asset-handler': 0.4.1 + '@cloudflare/unenv-preset': 2.7.13(unenv@2.0.0-rc.24)(workerd@1.20251210.0) blake3-wasm: 2.1.5 - chokidar: 4.0.1 - date-fns: 4.1.0 - esbuild: 0.17.19 - itty-time: 1.0.6 - miniflare: 3.20241106.1 - nanoid: 3.3.11 + esbuild: 0.27.0 + miniflare: 4.20251210.0 path-to-regexp: 6.3.0 - resolve: 1.22.8 - resolve.exports: 2.0.2 - selfsigned: 2.4.1 - source-map: 0.6.1 - unenv: /unenv-nightly@2.0.0-20241121-161142-806b5c0 - workerd: 1.20241106.1 - xxhash-wasm: 1.1.0 + unenv: 2.0.0-rc.24 + workerd: 1.20251210.0 optionalDependencies: fsevents: 2.3.3 transitivePeerDependencies: - bufferutil - - supports-color - utf-8-validate dev: true @@ -25140,6 +24977,7 @@ packages: ansi-styles: 6.2.1 string-width: 7.2.0 strip-ansi: 7.1.0 + dev: false /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -25251,6 +25089,7 @@ packages: /xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + dev: false /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} @@ -25336,6 +25175,13 @@ packages: engines: {node: '>=18'} dev: false + /youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + dev: true + /youch@3.3.4: resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} dependencies: @@ -25344,25 +25190,50 @@ packages: stacktracey: 2.1.8 dev: true - /zod-to-json-schema@3.24.5(zod@3.24.1): - resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} + /youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.12 + cookie: 1.1.1 + youch-core: 0.3.3 + dev: true + + /zod-to-json-schema@3.25.0(zod@3.25.76): + resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} peerDependencies: - zod: 3.24.1 + zod: ^3.25 || ^4 dependencies: - zod: 3.24.1 + zod: 3.25.76 - /zod-to-ts@1.2.0(typescript@5.9.2)(zod@3.24.1): + /zod-to-ts@1.2.0(typescript@5.9.2)(zod@3.25.76): resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} peerDependencies: typescript: ^4.9.4 || ^5.0.2 - zod: 3.24.1 + zod: ^3 dependencies: typescript: 5.9.2 - zod: 3.24.1 + zod: 3.25.76 dev: false + /zod@3.22.3: + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + dev: true + /zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + dev: true + + /zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + /zod@4.0.17: + resolution: {integrity: sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==} + + /zod@4.2.1: + resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} + dev: false /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} diff --git a/scripts/update-indexes.sh b/scripts/update-indexes.sh deleted file mode 100755 index f14fb126..00000000 --- a/scripts/update-indexes.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash - -set -e - -if [ -z "$MEILISEARCH_HOST" ] || [ -z "$MEILISEARCH_MASTER_KEY" ]; then - echo "Please set the MEILISEARCH_HOST and MEILISEARCH_MASTER_KEY environment variables." - exit 1 -fi - -SORTABLE_ATTRIBUTES='[ - "flashcard.due_timestamp", - "flashcard_reverse.due_timestamp" -]' - -FILTERABLE_ATTRIBUTES='[ - "flashcard.due_timestamp", - "flashcard_reverse.due_timestamp", - "tags", - "type" -]' - -# The maximinum number of hits to return for a search query. -MAX_TOTAL_HITS=5000 - -echo "Updating indexes for all users..." -echo - -# Fetch all indexes -INDEXES=$(curl -s -X GET "$MEILISEARCH_HOST/indexes?limit=1000" -H "Authorization: Bearer $MEILISEARCH_MASTER_KEY" | jq -r '.results[].uid') - -echo "Found indexes: " -echo [$INDEXES] -echo - -for index in $INDEXES; do - echo "Updating filterable attributes for index: $index" - # Update filterable attributes - response=$(curl \ - -X PUT "$MEILISEARCH_HOST/indexes/$index/settings/filterable-attributes" \ - -H "Authorization: Bearer $MEILISEARCH_MASTER_KEY" \ - -H "Content-Type: application/json" \ - -s \ - --data-binary "$FILTERABLE_ATTRIBUTES") - - if [[ $response =~ "code" && $response =~ "type" ]]; then - echo $response | jq - echo "Error updating filterable attributes for index: $index" - exit 1 - fi - - echo "Updating sortable attributes for index: $index" - # Update sortable attributes - response=$(curl \ - -X PUT "$MEILISEARCH_HOST/indexes/$index/settings/sortable-attributes" \ - -H "Authorization: Bearer $MEILISEARCH_MASTER_KEY" \ - -H "Content-Type: application/json" \ - -s \ - --data-binary "$SORTABLE_ATTRIBUTES") - - if [[ $response =~ "code" && $response =~ "type" ]]; then - echo $response | jq - echo "Error updating sortable attributes for index: $index" - exit 1 - fi - - echo "Updating pagination settings for index: $index" - # Update pagination settings - response=$(curl \ - -X PATCH "$MEILISEARCH_HOST/indexes/$index/settings/pagination" \ - -H "Authorization: Bearer $MEILISEARCH_MASTER_KEY" \ - -H "Content-Type: application/json" \ - -s \ - --data-binary '{ "maxTotalHits": '$MAX_TOTAL_HITS' }') - - if [[ $response =~ "code" && $response =~ "type" ]]; then - echo $response | jq - echo "Error updating pagination settings for index: $index" - exit 1 - fi - - echo -done - -echo "Successfully updated indexes for all users." diff --git a/turbo.json b/turbo.json index cd61ef45..23ab6c5a 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,7 @@ { "$schema": "https://turbo.build/schema.json", "globalDependencies": ["**/.env.*local"], + "ui": "stream", "tasks": { "build": { "dependsOn": ["^build"],