diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..7e185a6d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +# Continuous integration for the GitHub mirror (pushes go to Codeberg + GitHub). +# Runs the same gate as .husky/pre-push plus tests and dead-code analysis. +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + checks: + runs-on: ubuntu-latest + timeout-minutes: 20 + services: + postgres: + # Matches production Neon major (17) and docker-compose.yml. + image: postgres:17-alpine + env: + POSTGRES_USER: teqo + POSTGRES_PASSWORD: teqo + POSTGRES_DB: teqo_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U teqo" + --health-interval 5s + --health-timeout 5s + --health-retries 12 + env: + # Tests load .env.test themselves (database teqo_test on localhost:5432). + # These are only for the migrate step, which reads the process env. + DATABASE_URL: postgresql://teqo:teqo@localhost:5432/teqo_test + PAYLOAD_SECRET: test-only-secret-not-used-in-production + NEXT_PUBLIC_SITE_URL: http://localhost:3000 + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint (zero warnings) + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck + + - name: Dead code (knip) + run: pnpm exec knip + + - name: Unit tests + run: pnpm test:unit + + - name: Migrate test database + run: pnpm migrate + + - name: Integration tests + run: pnpm test:int diff --git a/.impeccable/config.json b/.impeccable/config.json index 5fc7123b..8e40de01 100644 --- a/.impeccable/config.json +++ b/.impeccable/config.json @@ -11,6 +11,15 @@ ], "createdAt": "2026-07-21T01:54:11.928Z", "reason": "Pre-existing inline hex in the password-reset EMAIL HTML template; transactional email cannot use CSS tokens" + }, + { + "rule": "broken-image", + "value": "*", + "files": [ + "src/components/MetaPixel.tsx" + ], + "createdAt": "2026-07-23T22:40:00.000Z", + "reason": "Meta's documented noscript tracking pixel: a hidden 1x1 whose src is a templated facebook.com/tr URL — not a rendered image" } ] } diff --git a/docs/IMPROVE-CODE-QUALITY-PLAN.md b/docs/IMPROVE-CODE-QUALITY-PLAN.md new file mode 100644 index 00000000..3c1dc2ef --- /dev/null +++ b/docs/IMPROVE-CODE-QUALITY-PLAN.md @@ -0,0 +1,46 @@ +# Improve Code Quality Plan + +## Context + +- **Started:** 2026-07-23 +- **Trigger:** full-codebase engineering-standards review (five parallel audits: structure/dead code, RBAC, client boundary/state, loading UX, caching) of a rapidly built ("vibe coded") codebase. +- **Scope:** whole repo; production is live (Neon Postgres with real citizens' PII), so security ordered first. +- **Method:** `improve-code-quality` journey, compressed into the seven delivery phases below (mapping to the skill's phases noted per row). The audits replaced a separate intake; the existing test suite (238 unit + 345 int + 5 e2e specs) is the safety net. +- **Companion docs:** [TECH-DEBT.md](TECH-DEBT.md) (ledger), [TESTING.md](TESTING.md) (safety-net map), [roadmap.md](roadmap.md) (canonical product/engineering backlog — larger debts become roadmap items per the `capture-review-debts` convention). + +## Phase Status + +| Phase | Content | Skill phases | Status | Date | +| ----- | ------- | ------------ | ------ | ---- | +| 0 | Security lockdown: collection access defaults, leader route gates, broken leader-supporter create, candidate alignment | — (P0 pre-work) | done | 2026-07-23 | +| 1 | Tooling gate: knip + dead code, CI, tsconfig, zero lint warnings, `as never` ban, docs artifacts | 1 (net) + 2 (readability) + 6 (habits) | in-progress | 2026-07-23 | +| 2 | Caching: React `cache()` dedup, committed election-aggregate artifact, `unstable_cache`, select/depth trims | 8 (sizing) | pending | | +| 3 | Loading feedback: task-toggle revalidation bug, map compare transition, result-region pending, Suspense streaming | — (UX correctness) | pending | | +| 4 | Client boundary/state: map hover isolation, bundle slimming, provider narrowing, composition splits | 4 (deep modules, UI) | pending | | +| 5 | Structure: split campaignAccess + supporter actions, dedupe clones, utilities organization, finish rename | 3 (refactoring) + 4 (deep modules) | pending | | +| 6 | Documentation: engineering-standards rule, AGENTS.md drift fixes, debt registration | 6 (habits) | pending | | + +Statuses: pending · in-progress · awaiting-evidence · done · deferred: \ · skipped: \ + +## Key Decisions + +| Date | Phase | Decision | Rationale | +| ---- | ----- | -------- | --------- | +| 2026-07-23 | 0 | Collection access lockdown before anything else | Payload defaults allowed any authenticated user (incl. campaign JWTs) to CRUD `users`/`signature`/`subscription`/`consent` via `/api` — live PII | +| 2026-07-23 | 0 | Keep pinned test contracts; stale tests updated, not silently changed code | Two leader-phone tests encoded the pre-remodel model; code was right, tests were stale | +| 2026-07-23 | 1 | `as never` banned repo-wide (ESLint `no-restricted-syntax`) | It silences all type checking; replaced by typed helpers (`hookFilledCreateData`, tests' `stub`) that keep property-level checking | +| 2026-07-23 | 1 | Lint gate is zero warnings (`--max-warnings=0`) | Warnings that never fail teach everyone to ignore the linter | +| 2026-07-23 | 1 | knip: files/dependencies = error; unused exports = warn (ledgered) | 158 export findings are dominated by god modules Phase 5 splits; failing CI on them day one would freeze the repo | +| 2026-07-23 | 1 | CI on the GitHub mirror (`.github/workflows/ci.yml`) | Pushes go to Codeberg + GitHub; GitHub Actions runs on the mirror with a Postgres 17 service | +| 2026-07-23 | 2 | Immutable TSE aggregates become a committed artifact (script-generated), not build-time computation | Vercel builds must not depend on prod DB content (TSE seeds are local-only); provenance stays explicit like `build:geometries` | +| 2026-07-23 | 2 | Salvador is the only zone-split municipality in the artifact | 2026-07-23 remodel: Camaçari is a single municipality (ZE 170/171 aggregated) | + +## Next Actions + +- [x] Phase 0 shipped (commit `fe0fb1c`) +- [ ] Phase 1: finish tests `as never` conversion; full verify (lint/typecheck/test/build); commit +- [ ] Phase 2 — caching +- [ ] Phase 3 — loading feedback +- [ ] Phase 4 — client boundary +- [ ] Phase 5 — structure +- [ ] Phase 6 — documentation; register leftovers in TECH-DEBT.md Debt Ledger diff --git a/docs/TECH-DEBT.md b/docs/TECH-DEBT.md new file mode 100644 index 00000000..d0151d27 --- /dev/null +++ b/docs/TECH-DEBT.md @@ -0,0 +1,54 @@ +# Tech Debt + +Ledger of known engineering debt. Items large enough to be scheduled work become entries in [roadmap.md](roadmap.md) (linked in the Ref column); this file tracks the full inventory so nothing is lost. Sourced initially from the 2026-07-23 engineering-standards audits. + +## Debt Ledger + +| Item | Location | Type | Risk | Effort | Priority | Status | Ref | +| ---- | -------- | ---- | ---- | ------ | -------- | ------ | --- | +| 158 unused exports reported by knip (warn-level) | god modules, ui kit families | dead code | low | M | P2 | open — drive `exports` rule to error after splits | Phase 5 | +| knip loader noise: `Error loading src/payload.config.ts` (server-only guard) | knip run output | tooling noise | none (exit 0, static graph resolves) | S | P3 | open | | +| Missing-consent window fixture releases its exclusive lease during the observation window — readers can interleave (rare flake). Full fix: session-level advisory lock spanning delete→op→restore + updated fault-path pins | `tests/helpers/testDatabaseLease.ts` (`withMissingInviteConsentFixture`) | test-infra race | low (ms-wide window; shared leases added 2026-07-23 narrow it) | M | P2 | open | | +| `users` collection has no roles (all admins omnipotent) | `src/collections/Users.ts` | RBAC | medium (small trusted admin set) | M (schema migration) | P1 before widening `/admin` | open | AGENTS Known Gaps #1 | +| Any campaign user can enumerate staff names/roles (`canReadCampaignUsers`) | `src/utilities/campaignAccess.ts` | info disclosure | low | S | P2 | open | | +| `removeSupporterData` deletes via `overrideAccess: true` while `canDeleteSupporter` is admin-only | `src/app/(campaign)/campanha/actions/supporter.ts` | access asymmetry (likely intentional LGPD path) | low | S | P3 | open — document or align | | +| Legacy public consent flows hardcode document IDs (`consent: 2`) | `src/app/(frontend)/actions/submitWhatsapp.ts` | correctness | medium | S | P1 | open | AGENTS Known Gaps #2 | +| E2E suite thin (5 specs; most campaign CRUD int-only) | `tests/e2e/` | coverage | medium | L | P2 | open | | +| CI runs on GitHub mirror only (no Codeberg/Woodpecker pipeline) | `.github/workflows/ci.yml` | process | low | M | P3 | open | | +| Post listing cache holds all published posts, filters in memory | `src/utilities/posts.ts` | scale (fine at ~tens of posts) | low | M | P3 | open — watch corpus growth | | +| Petition `generateStaticParams` uses uncached `getDocuments` without `select`/`depth: 0` | `src/utilities/documents.ts` consumers | over-fetch (build-time) | low | S | P2 | planned (Phase 2) | | +| Compare mode (`?compare=`) still queries per candidate uncached cross-request | `src/utilities/municipalityMapData.ts` | perf | low | M | P3 | open — React `cache()` dedups in-request; add `unstable_cache` if profiling warrants | | + +## Smell Inventory + +| Smell | Location | Refactoring | Status | +| ----- | -------- | ----------- | ------ | +| God module: RBAC for every collection in one file (964 lines, ~80 exports) | `src/utilities/campaignAccess.ts` | split per domain under `src/utilities/access/` | planned (Phase 5) | +| God module: ~71 exports of electoral math | `src/lib/electionInsights.ts` | split by concern when next touched | ledgered | +| Multi-feature action bag: CRUD + CSV import pipeline + LGPD in one file (722 lines) | `src/app/(campaign)/campanha/actions/supporter.ts` | split CRUD vs import | planned (Phase 5) | +| Clone twins: create/update/staff-assert/unique-conflict skeleton | `actions/organization.ts` ↔ `actions/stateDeputy.ts` | extract shared factory | planned (Phase 5) | +| 13 near-identical `formActions.ts` try/catch→mapError→redirect ladders | `src/app/(campaign)/campanha/(app)/*/formActions.ts` | shared form-action wrapper | planned (Phase 5) | +| Flat 80+-file `src/utilities/` junk drawer; `lib/` vs `utilities/` boundary undocumented | `src/utilities/`, `src/lib/` | document convention + move misfiled modules | planned (Phase 5/6) | +| Praça→Município rename leftovers in identifiers (`PLAZA_MAP_YEARS`, `ACCESSIBLE_PLAZA_IDS_CONTEXT_KEY`, …) | map/list utilities | finish rename | planned (Phase 5) | +| Props→state `useEffect` mirrors | `MunicipalityListExpectedVotesControl`, comboboxes | key-remount or fully controlled | planned (Phase 4) | +| Detail-page RSCs mixing loading + tab UI + forms (300+ lines) | `municipios/[slug]/page.tsx`, `planos/[slug]/page.tsx` | extract tab loaders/sections | ledgered | + +## Sprout / Wrap Register + +| Host | Sprouted/Wrapped | Notes | +| ---- | ---------------- | ----- | +| (none yet) | | | + +## Adopted Conventions + +- **Zero lint warnings.** `pnpm lint` runs with `--max-warnings=0`; deliberate placeholders use a `_` prefix (`argsIgnorePattern`). +- **`as never` is banned** (ESLint `no-restricted-syntax`). Production code uses typed helpers (e.g. `hookFilledCreateData` for hook-completed create data); tests use `stub(partial)` which keeps property-level checking. A justified `eslint-disable-next-line` is the only escape hatch. +- **Dead code is deleted, not commented.** knip runs in CI (files/dependencies at error level). After any change: `pnpm exec knip` and remove what died. +- **Collections always declare `access` explicitly.** Payload's default ("any authenticated user") is treated as a bug — see `payloadAdminOnly`. +- **Generated migration signatures are exempt** from unused-args linting (`src/migrations/**` override); never hand-edit migration content for lint cosmetics. +- **Shared consent fixture rows are leased, never owned** in integration tests (`tests/helpers/testDatabaseLease.ts`); deleting them steals from parallel spec files. + +## Debt Budget & Broken-Windows Policy + +- New warnings/`as never`/unused exports fail or get ledgered the same day — no untracked `// TODO`s. +- Each delivery that runs `/simplify` registers surviving debts here or in `docs/plans/escala-dry-pos-*.md` (existing convention), whichever the item's size warrants. diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 00000000..b036969b --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,32 @@ +# Testing + +## Test Strategy + +Three layers, all local-database-only (see `local-database` skill; tests refuse any database not ending in `_test`): + +- **Unit** (`tests/unit/**/*.unit.spec.ts`, 33+ specs): pure logic; runs with a deliberately invalid `DATABASE_URL` so nothing can touch a database. +- **Integration** (`tests/int/**/*.int.spec.ts`, 42+ specs): real Payload + Postgres (`teqo_test`); campaign fixtures allocate seeded municipalities via a Postgres sequence so parallel spec files never share one, and purge residue on claim. Shared consent rows with stable keys are leased (`tests/helpers/testDatabaseLease.ts`), never owned. +- **E2E** (`tests/e2e`, Playwright, 5 specs): smoke-level; most campaign behavior is pinned at the int layer. + +CI (`.github/workflows/ci.yml`) runs lint (zero warnings), typecheck, knip, unit, migrate, int against a Postgres 17 service. + +## Safety Net Map + +Modules the 2026-07 hardening phases touch, and where their behavior is pinned: + +| Module | Pinned behaviors | Test files | Gaps | +| ------ | ---------------- | ---------- | ---- | +| `campaignAccess.ts` (RBAC) | per-role municipality/leadership/pledge/supporter/demand/plan scoping; leader lockdown; staff field asymmetry | `campaignMunicipalityAccess`, `campaignVotePledge`, `campaignSupporter`, `campaignUserPhone`, `campaignActionPlan`, `campaignDemandWorkflow`, `collectionAccessLockdown` (int) | no dedicated per-export suite; covered through collection behavior | +| Collection access lockdown | campaign JWT denied on `users`/PII/CMS writes; anonymous read of public content; admin unaffected | `collectionAccessLockdown.int.spec.ts` | REST-transport variant untested (Local API with `overrideAccess: false` is equivalent) | +| `municipalityMapData.ts` | bundle scoping by URL filters + advisor access; `validVotesByYear` incl. 2026 reuse of 2022; null on empty scope | `municipalityMapData.int.spec.ts` | zone breakdown shape | +| `votePledgeData.ts` | `estimated[scenario] ?? declared` aggregation; role scoping; leader denial | `campaignVotePledge.int.spec.ts` | | +| Supporter import (token/bulk) | preview→confirm flow, dedup by phone, consent fail-closed | `campaignSupporter.int.spec.ts` | `supporterImportBulk`/`supporterImportToken` internals only via flow | +| Leader supporter create | linked-municipality scope; out-of-scope rejection; staff rejection | `collectionAccessLockdown.int.spec.ts` | | +| Posts visibility/caching | `isPostVisible` fail-closed; cached listing | (indirect via routes) | no dedicated `posts` spec — backlog | + +## Characterization Backlog + +- [ ] `src/utilities/posts.ts` — dedicated spec for `isPostVisible` fail-closed matrix and cache wrappers. +- [ ] `actionPlanViewModels.ts` + other `actionPlan*` utilities (~1k lines) — currently untested directly. +- [ ] `campaignDemandData.ts` / `leadershipData.ts` / `organizationData.ts` / `stateDeputyData.ts` list loaders. +- [ ] Election aggregate artifact (Phase 2): snapshot test pinning artifact ↔ `municipalityCatalog` key consistency. diff --git a/eslint.config.mjs b/eslint.config.mjs index 45960655..8f4c77c3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -141,6 +141,40 @@ const eslintConfig = [ 'local/use-server-async-exports': 'error', }, }, + { + // Underscore-prefixed bindings are deliberate placeholders (unused action + // state, ignored tuple slots); everything else unused is dead code. + files: ['**/*.{js,mjs,jsx,ts,tsx}'], + rules: { + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + args: 'all', + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + // `as never` silences every type check on the expression (worse than + // `any`). Fix the types instead; a justified eslint-disable comment is + // the only sanctioned escape hatch. + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSAsExpression > TSNeverKeyword', + message: + 'Do not cast with `as never` — it disables type checking entirely. Fix the types or use a narrowly-typed helper.', + }, + ], + }, + }, + { + // Payload generates migration signatures with unused destructured args. + files: ['src/migrations/**/*.ts'], + rules: { + '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], + }, + }, prettier, // Override default ignores of eslint-config-next. globalIgnores([ diff --git a/knip.json b/knip.json new file mode 100644 index 00000000..3eb8e869 --- /dev/null +++ b/knip.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "entry": [ + "src/payload.config.ts", + "src/migrations/index.ts", + "src/app/(payload)/admin/importMap.js", + "scripts/*.mjs", + "eslint-local-rules.mjs" + ], + "project": ["src/**/*.{ts,tsx,js}", "scripts/**/*.mjs"], + "ignore": ["src/payload-types.ts", "src/migrations/**"], + "paths": { + "server-only": ["tests/helpers/serverOnly.ts"] + }, + "ignoreBinaries": ["playwright"], + "ignoreDependencies": [ + "tsx", + "sharp", + "tailwindcss-animate", + "tw-animate-css", + "shadcn", + "eslint-config-next", + "prettier" + ], + "rules": { + "exports": "warn", + "types": "warn", + "enumMembers": "warn", + "duplicates": "warn" + } +} diff --git a/package.json b/package.json index 337881df..c807c32a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "db:seed:tse": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-tse-results.mjs", "db:seed:consent": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-consent.mjs", "build:demographics": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-demographics.mjs", + "build:election-aggregates": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/build-election-aggregates.mjs", "build:geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-geometries.mjs", "build:tse-city-codes": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-tse-city-codes.mjs", "dev": "node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev", @@ -38,7 +39,9 @@ "migrate": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload migrate", "migrate:create": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload migrate:create", "migrate:status": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload migrate:status", - "lint": "cross-env NODE_OPTIONS=--no-deprecation eslint --cache --cache-location .next/cache/eslint/", + "knip": "knip", + "knip:production": "knip --production", + "lint": "cross-env NODE_OPTIONS=--no-deprecation eslint --cache --cache-location .next/cache/eslint/ --max-warnings=0", "lint:fix": "cross-env NODE_OPTIONS=--no-deprecation eslint --fix", "payload": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload", "start": "cross-env NODE_OPTIONS=--no-deprecation next start", @@ -62,10 +65,6 @@ "@payloadcms/richtext-lexical": "3.82.0", "@payloadcms/storage-vercel-blob": "3.82.0", "@payloadcms/ui": "3.82.0", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slot": "^1.2.4", "@vercel/analytics": "^1.6.1", "@vercel/speed-insights": "^1.3.1", "class-variance-authority": "^0.7.1", @@ -101,6 +100,7 @@ "@types/geojson": "^7946.0.16", "@types/leaflet": "^1.9.21", "@types/node": "22.19.9", + "@types/pg": "^8.10.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/topojson-client": "^3.1.5", @@ -117,6 +117,8 @@ "eslint-plugin-check-file": "^3.3.1", "husky": "^9.1.7", "jsdom": "28.0.0", + "knip": "^6.27.0", + "pg": "^8.22.0", "postcss": "^8.5.6", "prettier": "^3.4.2", "schema-dts": "^2.0.0", @@ -126,7 +128,6 @@ "tsx": "4.21.0", "typescript": "npm:@typescript/typescript6@6.0.2", "typescript-7": "npm:typescript@7.0.2", - "typescript-eslint": "^8.64.0", "vite-tsconfig-paths": "6.0.5", "vitest": "4.0.18", "yauzl": "^3.4.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b2b9a23..dac67e68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,18 +38,6 @@ importers: '@payloadcms/ui': specifier: 3.82.0 version: 3.82.0(@types/react@19.2.14)(@typescript/typescript6@6.0.2)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-label': - specifier: ^2.1.8 - version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-select': - specifier: ^2.2.6 - version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-separator': - specifier: ^1.1.8 - version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': - specifier: ^1.2.4 - version: 1.2.4(@types/react@19.2.14)(react@19.2.4) '@vercel/analytics': specifier: ^1.6.1 version: 1.6.1(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(react@19.2.4) @@ -150,6 +138,9 @@ importers: '@types/node': specifier: 22.19.9 version: 22.19.9 + '@types/pg': + specifier: ^8.10.2 + version: 8.10.2 '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -176,28 +167,34 @@ importers: version: 2.3.1 '@vitejs/plugin-react': specifier: 4.5.2 - version: 4.5.2(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.5.2(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0)) csv-parse: specifier: ^7.0.1 version: 7.0.1 eslint: specifier: ^9.39.2 - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@2.7.0) eslint-config-next: specifier: 15.4.11 - version: 15.4.11(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) + version: 15.4.11(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + version: 10.1.8(eslint@9.39.2(jiti@2.7.0)) eslint-plugin-check-file: specifier: ^3.3.1 - version: 3.3.1(eslint@9.39.2(jiti@2.6.1)) + version: 3.3.1(eslint@9.39.2(jiti@2.7.0)) husky: specifier: ^9.1.7 version: 9.1.7 jsdom: specifier: 28.0.0 version: 28.0.0(@noble/hashes@1.8.0) + knip: + specifier: ^6.27.0 + version: 6.27.0 + pg: + specifier: ^8.22.0 + version: 8.22.0 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -225,15 +222,12 @@ importers: typescript-7: specifier: npm:typescript@7.0.2 version: typescript@7.0.2 - typescript-eslint: - specifier: ^8.64.0 - version: 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) vite-tsconfig-paths: specifier: 6.0.5 - version: 6.0.5(@typescript/typescript6@6.0.2)(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.0.5(@typescript/typescript6@6.0.2)(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 4.0.18 - version: 4.0.18(@types/node@22.19.9)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@22.19.9)(happy-dom@20.8.3)(jiti@2.7.0)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0) yauzl: specifier: ^3.4.0 version: 3.4.0 @@ -518,15 +512,30 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 + '@emnapi/core@1.11.0': + resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + '@emnapi/runtime@1.11.0': + resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -1588,6 +1597,12 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@next/env@15.4.11': resolution: {integrity: sha512-mIYp/091eYfPFezKX7ZPTWqrmSXq+ih6+LcUyKvLmeLQGhlPtot33kuEOd4U+xAA7sFfj21+OtCpIZx0g5SpvQ==} @@ -1686,6 +1701,239 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@oxc-parser/binding-android-arm-eabi@0.137.0': + resolution: {integrity: sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.137.0': + resolution: {integrity: sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.137.0': + resolution: {integrity: sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.137.0': + resolution: {integrity: sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.137.0': + resolution: {integrity: sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.137.0': + resolution: {integrity: sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.137.0': + resolution: {integrity: sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.137.0': + resolution: {integrity: sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.137.0': + resolution: {integrity: sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.137.0': + resolution: {integrity: sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.137.0': + resolution: {integrity: sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.137.0': + resolution: {integrity: sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.137.0': + resolution: {integrity: sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.137.0': + resolution: {integrity: sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.137.0': + resolution: {integrity: sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.137.0': + resolution: {integrity: sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.137.0': + resolution: {integrity: sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.137.0': + resolution: {integrity: sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.137.0': + resolution: {integrity: sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.137.0': + resolution: {integrity: sha512-2AsevxlvNN4WKxpEn3RtqD5zbqMaXF+T7JXblsP4gVuY+vC9dXS4ED/PwfRCliFqoeisYS3Iro4DHzxr0TEvVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + + '@oxc-resolver/binding-android-arm-eabi@11.21.3': + resolution: {integrity: sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.21.3': + resolution: {integrity: sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.21.3': + resolution: {integrity: sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.21.3': + resolution: {integrity: sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.21.3': + resolution: {integrity: sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': + resolution: {integrity: sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': + resolution: {integrity: sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': + resolution: {integrity: sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.21.3': + resolution: {integrity: sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': + resolution: {integrity: sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': + resolution: {integrity: sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': + resolution: {integrity: sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': + resolution: {integrity: sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.21.3': + resolution: {integrity: sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.21.3': + resolution: {integrity: sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.21.3': + resolution: {integrity: sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.21.3': + resolution: {integrity: sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': + resolution: {integrity: sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.21.3': + resolution: {integrity: sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==} + cpu: [x64] + os: [win32] + '@payloadcms/db-postgres@3.82.0': resolution: {integrity: sha512-FzIRFpytQc8j6UjMpCYKN5Vz+sjw6EgOgsLzVtae6ihbJcHnEWeLXsV9ELd/r1349nNNqyFycRbJVpXbb+rhhw==} peerDependencies: @@ -2051,19 +2299,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-label@2.1.8': - resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-menu@2.1.16': resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} peerDependencies: @@ -2285,19 +2520,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-separator@1.1.8': - resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-slider@1.3.6': resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} peerDependencies: @@ -2791,6 +3013,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} @@ -2920,14 +3145,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/eslint-plugin@8.64.0': - resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.64.0 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.55.0': resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2935,45 +3152,22 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.64.0': - resolution: {integrity: sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.55.0': resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.64.0': - resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.55.0': resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.64.0': - resolution: {integrity: sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.55.0': resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.64.0': - resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.55.0': resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2981,33 +3175,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.64.0': - resolution: {integrity: sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.55.0': resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.64.0': - resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.55.0': resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.64.0': - resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.55.0': resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3015,21 +3192,10 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.64.0': - resolution: {integrity: sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.55.0': resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.64.0': - resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/typescript-aix-ppc64@7.0.2': resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} engines: {node: '>=16.20.0'} @@ -4223,10 +4389,6 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@5.0.1: - resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@9.39.2: resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4338,6 +4500,9 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -4399,6 +4564,11 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -4488,6 +4658,9 @@ packages: get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} @@ -4898,6 +5071,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} @@ -4987,6 +5164,11 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + knip@6.27.0: + resolution: {integrity: sha512-CngYEYrD0n20N06FXA8n3u/0Wnnugoa+B9k14OP+iKIgkCHuzvIdsP3nfwjhByoc1WfogpxfiriMboAXFETDUw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -5453,6 +5635,13 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + oxc-parser@0.137.0: + resolution: {integrity: sha512-yFImD+WLElJpLKy8llG1qe4DCmMsL18peRp8XP1JKfig/gISbJkglnpDtX2aTmAn10kZF7164HbN2H8QPsXxGg==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.21.3: + resolution: {integrity: sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -5534,9 +5723,15 @@ packages: pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + pg-connection-string@2.11.0: resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==} + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -5550,9 +5745,17 @@ packages: peerDependencies: pg: '>=8.0' + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + pg-protocol@1.11.0: resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} @@ -5570,6 +5773,15 @@ packages: pg-native: optional: true + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} @@ -5584,6 +5796,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} @@ -6057,6 +6273,10 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + smol-toml@1.7.0: + resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==} + engines: {node: '>= 18'} + sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -6262,6 +6482,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinyrainbow@3.0.3: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} @@ -6323,12 +6547,6 @@ packages: peerDependencies: typescript: '>=4.8.4' - ts-api-utils@2.5.0: - resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - ts-essentials@10.0.3: resolution: {integrity: sha512-/FrVAZ76JLTWxJOERk04fm8hYENDo0PWSP3YLQKxevLwWtxemGcl5JJEzN4iqfDlRve0ckyfFaOBu4xbNH/wZw==} peerDependencies: @@ -6396,13 +6614,6 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.64.0: - resolution: {integrity: sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -6417,6 +6628,10 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} + unbash@4.0.3: + resolution: {integrity: sha512-3cudTErfToSc4Ggv8XGXVNVli/xHKUtUZvaY5UVwhOcUPbQGz7PeaEnT/SAVgNziZtX67KEN9swMUYkLghxA1w==} + engines: {node: '>=14'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -6626,6 +6841,10 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -6739,8 +6958,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true @@ -7124,12 +7343,34 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 + '@emnapi/core@1.11.0': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 @@ -7140,6 +7381,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.28.6 @@ -7436,9 +7682,9 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.7.0))': dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -8009,6 +8255,20 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': + dependencies: + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@next/env@15.4.11': {} '@next/env@15.5.12': {} @@ -8072,6 +8332,133 @@ snapshots: '@open-draft/until@2.1.0': {} + '@oxc-parser/binding-android-arm-eabi@0.137.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.137.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.137.0': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.137.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.137.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.137.0': + optional: true + + '@oxc-project/types@0.137.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.21.3': + optional: true + + '@oxc-resolver/binding-android-arm64@11.21.3': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.21.3': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.21.3': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.21.3': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.21.3': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.21.3': + dependencies: + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.21.3': + optional: true + '@payloadcms/db-postgres@3.82.0(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))': dependencies: '@payloadcms/drizzle': 3.82.0(@types/pg@8.10.2)(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(pg@8.16.3) @@ -8607,15 +8994,6 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -8897,15 +9275,6 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/number': 1.1.1 @@ -9326,6 +9695,11 @@ snapshots: tslib: 2.8.1 optional: true + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/acorn@4.0.6': dependencies: '@types/estree': 1.0.8 @@ -9405,7 +9779,7 @@ snapshots: '@types/pg@8.10.2': dependencies: '@types/node': 22.19.9 - pg-protocol: 1.11.0 + pg-protocol: 1.15.0 pg-types: 4.1.0 '@types/react-dom@19.2.3(@types/react@19.2.14)': @@ -9462,15 +9836,15 @@ snapshots: dependencies: '@types/node': 22.19.9 - '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0))': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/parser': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/type-utils': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/utils': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/type-utils': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) + '@typescript-eslint/utils': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) '@typescript-eslint/visitor-keys': 8.55.0 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(@typescript/typescript6@6.0.2) @@ -9478,42 +9852,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/type-utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/visitor-keys': 8.64.0 - eslint: 9.39.2(jiti@2.6.1) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': + '@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0))': dependencies: '@typescript-eslint/scope-manager': 8.55.0 '@typescript-eslint/types': 8.55.0 '@typescript-eslint/typescript-estree': 8.55.0(@typescript/typescript6@6.0.2) '@typescript-eslint/visitor-keys': 8.55.0 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': - dependencies: - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/visitor-keys': 8.64.0 - debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color @@ -9527,61 +9873,29 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.64.0(@typescript/typescript6@6.0.2)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.64.0 - debug: 4.4.3 - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - '@typescript-eslint/scope-manager@8.55.0': dependencies: '@typescript-eslint/types': 8.55.0 '@typescript-eslint/visitor-keys': 8.55.0 - '@typescript-eslint/scope-manager@8.64.0': - dependencies: - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/visitor-keys': 8.64.0 - '@typescript-eslint/tsconfig-utils@8.55.0(@typescript/typescript6@6.0.2)': dependencies: typescript: '@typescript/typescript6@6.0.2' - '@typescript-eslint/tsconfig-utils@8.64.0(@typescript/typescript6@6.0.2)': - dependencies: - typescript: '@typescript/typescript6@6.0.2' - - '@typescript-eslint/type-utils@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': + '@typescript-eslint/type-utils@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0))': dependencies: '@typescript-eslint/types': 8.55.0 '@typescript-eslint/typescript-estree': 8.55.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/utils': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) ts-api-utils: 2.4.0(@typescript/typescript6@6.0.2) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': - dependencies: - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) - ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - '@typescript-eslint/types@8.55.0': {} - '@typescript-eslint/types@8.64.0': {} - '@typescript-eslint/typescript-estree@8.55.0(@typescript/typescript6@6.0.2)': dependencies: '@typescript-eslint/project-service': 8.55.0(@typescript/typescript6@6.0.2) @@ -9597,39 +9911,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.64.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/utils@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0))': dependencies: - '@typescript-eslint/project-service': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/tsconfig-utils': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/visitor-keys': 8.64.0 - debug: 4.4.3 - minimatch: 10.2.4 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.55.0 '@typescript-eslint/types': 8.55.0 '@typescript-eslint/typescript-estree': 8.55.0(@typescript/typescript6@6.0.2) - eslint: 9.39.2(jiti@2.6.1) - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color @@ -9639,11 +9927,6 @@ snapshots: '@typescript-eslint/types': 8.55.0 eslint-visitor-keys: 4.2.1 - '@typescript-eslint/visitor-keys@8.64.0': - dependencies: - '@typescript-eslint/types': 8.64.0 - eslint-visitor-keys: 5.0.1 - '@typescript/typescript-aix-ppc64@7.0.2': optional: true @@ -9785,7 +10068,7 @@ snapshots: next: 15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4) react: 19.2.4 - '@vitejs/plugin-react@4.5.2(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@4.5.2(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -9793,7 +10076,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.11 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -9806,14 +10089,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2) - vite: 7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0) '@vitest/pretty-format@4.0.18': dependencies: @@ -10647,19 +10930,19 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@15.4.11(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)): + eslint-config-next@15.4.11(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)): dependencies: '@next/eslint-plugin-next': 15.4.11 '@rushstack/eslint-patch': 1.16.1 - '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/parser': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) + '@typescript-eslint/parser': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) + eslint: 9.39.2(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.7.0)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.7.0)) + eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.7.0)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.7.0)) optionalDependencies: typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: @@ -10667,9 +10950,9 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.7.0)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) eslint-import-resolver-node@0.3.9: dependencies: @@ -10679,38 +10962,39 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) get-tsconfig: 4.13.6 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/parser': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) + eslint: 9.39.2(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-check-file@3.3.1(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-check-file@3.3.1(eslint@9.39.2(jiti@2.7.0)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) is-glob: 4.0.3 micromatch: 4.0.8 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -10719,9 +11003,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.7.0)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -10733,13 +11017,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/parser': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.7.0)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -10749,7 +11033,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -10758,11 +11042,11 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.7.0)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) - eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.7.0)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -10770,7 +11054,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.2 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -10793,11 +11077,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint-visitor-keys@5.0.1: {} - - eslint@9.39.2(jiti@2.6.1): + eslint@9.39.2(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 @@ -10832,7 +11114,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.6.1 + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -10978,10 +11260,18 @@ snapshots: dependencies: reusify: 1.1.0 + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -11053,6 +11343,10 @@ snapshots: dependencies: is-callable: 1.2.7 + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -11137,6 +11431,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -11504,6 +11802,8 @@ snapshots: jiti@2.6.1: {} + jiti@2.7.0: {} + jose@5.10.0: {} jose@6.2.1: {} @@ -11601,6 +11901,22 @@ snapshots: kleur@4.1.5: {} + knip@6.27.0: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + formatly: 0.3.0 + get-tsconfig: 4.14.0 + jiti: 2.7.0 + oxc-parser: 0.137.0 + oxc-resolver: 11.21.3 + picomatch: 4.0.5 + smol-toml: 1.7.0 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.17 + unbash: 4.0.3 + yaml: 2.9.0 + zod: 4.3.6 + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -12186,6 +12502,53 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + oxc-parser@0.137.0: + dependencies: + '@oxc-project/types': 0.137.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.137.0 + '@oxc-parser/binding-android-arm64': 0.137.0 + '@oxc-parser/binding-darwin-arm64': 0.137.0 + '@oxc-parser/binding-darwin-x64': 0.137.0 + '@oxc-parser/binding-freebsd-x64': 0.137.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.137.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.137.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.137.0 + '@oxc-parser/binding-linux-arm64-musl': 0.137.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.137.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.137.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.137.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.137.0 + '@oxc-parser/binding-linux-x64-gnu': 0.137.0 + '@oxc-parser/binding-linux-x64-musl': 0.137.0 + '@oxc-parser/binding-openharmony-arm64': 0.137.0 + '@oxc-parser/binding-wasm32-wasi': 0.137.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.137.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.137.0 + '@oxc-parser/binding-win32-x64-msvc': 0.137.0 + + oxc-resolver@11.21.3: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.21.3 + '@oxc-resolver/binding-android-arm64': 11.21.3 + '@oxc-resolver/binding-darwin-arm64': 11.21.3 + '@oxc-resolver/binding-darwin-x64': 11.21.3 + '@oxc-resolver/binding-freebsd-x64': 11.21.3 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.21.3 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.21.3 + '@oxc-resolver/binding-linux-arm64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-arm64-musl': 11.21.3 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-riscv64-musl': 11.21.3 + '@oxc-resolver/binding-linux-s390x-gnu': 11.21.3 + '@oxc-resolver/binding-linux-x64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-x64-musl': 11.21.3 + '@oxc-resolver/binding-openharmony-arm64': 11.21.3 + '@oxc-resolver/binding-wasm32-wasi': 11.21.3 + '@oxc-resolver/binding-win32-arm64-msvc': 11.21.3 + '@oxc-resolver/binding-win32-x64-msvc': 11.21.3 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -12290,8 +12653,13 @@ snapshots: pg-cloudflare@1.3.0: optional: true + pg-cloudflare@1.4.0: + optional: true + pg-connection-string@2.11.0: {} + pg-connection-string@2.14.0: {} + pg-int8@1.0.1: {} pg-numeric@1.0.2: {} @@ -12300,8 +12668,14 @@ snapshots: dependencies: pg: 8.16.3 + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + pg-protocol@1.11.0: {} + pg-protocol@1.15.0: {} + pg-types@2.2.0: dependencies: pg-int8: 1.0.1 @@ -12330,6 +12704,16 @@ snapshots: optionalDependencies: pg-cloudflare: 1.3.0 + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + pgpass@1.0.5: dependencies: split2: 4.2.0 @@ -12340,6 +12724,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.5: {} + pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -13024,6 +13410,8 @@ snapshots: sisteransi@1.0.5: {} + smol-toml@1.7.0: {} + sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 @@ -13216,6 +13604,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + tinyrainbow@3.0.3: {} tldts-core@7.0.23: {} @@ -13275,10 +13668,6 @@ snapshots: dependencies: typescript: '@typescript/typescript6@6.0.2' - ts-api-utils@2.5.0(@typescript/typescript6@6.0.2): - dependencies: - typescript: '@typescript/typescript6@6.0.2' - ts-essentials@10.0.3(@typescript/typescript6@6.0.2): optionalDependencies: typescript: '@typescript/typescript6@6.0.2' @@ -13363,17 +13752,6 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)): - dependencies: - '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - eslint: 9.39.2(jiti@2.6.1) - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - typescript@6.0.3: {} typescript@7.0.2: @@ -13401,6 +13779,8 @@ snapshots: uint8array-extras@1.5.0: {} + unbash@4.0.3: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -13528,17 +13908,17 @@ snapshots: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 - vite-tsconfig-paths@6.0.5(@typescript/typescript6@6.0.2)(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2)): + vite-tsconfig-paths@6.0.5(@typescript/typescript6@6.0.2)(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(@typescript/typescript6@6.0.2) - vite: 7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - typescript - vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -13549,16 +13929,16 @@ snapshots: optionalDependencies: '@types/node': 22.19.9 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 2.7.0 lightningcss: 1.30.2 sass: 1.77.4 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.9.0 - vitest@4.0.18(@types/node@22.19.9)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@types/node@22.19.9)(happy-dom@20.8.3)(jiti@2.7.0)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -13575,7 +13955,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.9 @@ -13598,6 +13978,8 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + walk-up-path@4.0.0: {} + web-streams-polyfill@3.3.3: {} webidl-conversions@8.0.1: {} @@ -13712,8 +14094,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.2: - optional: true + yaml@2.9.0: {} yargs-parser@21.1.1: {} diff --git a/scripts/build-bahia-demographics.mjs b/scripts/build-bahia-demographics.mjs index 7133abb3..2a03dfcb 100644 --- a/scripts/build-bahia-demographics.mjs +++ b/scripts/build-bahia-demographics.mjs @@ -130,7 +130,7 @@ const buildDemographicsByCode = ({ ageCategoryRows, femaleRows, medianRows }) => current.medianAge = Number.isFinite(median) ? median : null } - for (const [code, record] of byCode.entries()) { + for (const record of byCode.values()) { record.population = Object.values(record.ageBands).reduce((sum, value) => sum + value, 0) record.sexShareFemale = record.population > 0 ? record.femalePopulation / record.population : 0 diff --git a/scripts/rename-plaza-to-municipality.mjs b/scripts/rename-plaza-to-municipality.mjs deleted file mode 100644 index 1a937c48..00000000 --- a/scripts/rename-plaza-to-municipality.mjs +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env node -/** - * One-shot mechanical rename: plaza → municipality across src/ and tests/. - * Excludes frozen historical migration artifacts. - */ -import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs' -import { dirname, join, relative } from 'path' - -const ROOT = join(import.meta.dirname, '..') - -const EXCLUDE_PATH_FRAGMENTS = [ - 'src/migrations/data/plazaCatalog20260721.ts', - 'src/migrations/20260721_020109_remodel_plazas.ts', - 'src/migrations/20260721_020109_remodel_plazas.json', - 'src/migrations/20260721_133444_add_plaza_expected_votes.ts', - 'src/migrations/20260721_133444_add_plaza_expected_votes.json', - 'node_modules/', - '.git/', - 'scripts/rename-plaza-to-municipality.mjs', -] - -const shouldExclude = (relPath) => EXCLUDE_PATH_FRAGMENTS.some((frag) => relPath.includes(frag)) - -const renamePathSegment = (segment) => - segment - .replace(/PlazaUpdate/g, 'MunicipalityUpdate') - .replace(/plazaUpdate/g, 'municipalityUpdate') - .replace(/Plaza/g, 'Municipality') - .replace(/plaza/g, 'municipality') - .replace(/^pracas$/, 'municipios') - -function walk(dir, files = []) { - for (const entry of readdirSync(dir)) { - const full = join(dir, entry) - const rel = relative(ROOT, full) - if (shouldExclude(rel)) continue - const st = statSync(full) - if (st.isDirectory()) { - if (entry === 'node_modules' || entry === '.git') continue - walk(full, files) - } else if (/\.(ts|tsx|js|jsx|json|md|mdc)$/.test(entry)) { - files.push(full) - } - } - return files -} - -// Rename directories bottom-up -function collectDirs(dir, dirs = []) { - for (const entry of readdirSync(dir)) { - const full = join(dir, entry) - const rel = relative(ROOT, full) - if (shouldExclude(rel)) continue - if (statSync(full).isDirectory()) { - collectDirs(full, dirs) - if (/plaza|Plaza|pracas/i.test(entry)) dirs.push(full) - } - } - return dirs -} - -const dirRenames = collectDirs(join(ROOT, 'src')) - .concat(collectDirs(join(ROOT, 'tests'))) - .sort((a, b) => b.length - a.length) - -for (const dir of dirRenames) { - const parent = dirname(dir) - const base = renamePathSegment(dir.split('/').pop()) - const target = join(parent, base) - if (dir !== target) { - mkdirSync(dirname(target), { recursive: true }) - try { - renameSync(dir, target) - console.log('DIR', relative(ROOT, dir), '->', relative(ROOT, target)) - } catch (e) { - // already renamed - } - } -} - -// Rename files -const allFiles = walk(join(ROOT, 'src')).concat(walk(join(ROOT, 'tests'))) -const fileRenames = allFiles - .filter((f) => /plaza|Plaza|pracas/i.test(f.split('/').pop())) - .sort((a, b) => b.length - a.length) - -for (const file of fileRenames) { - const parent = dirname(file) - const base = renamePathSegment(file.split('/').pop()) - const target = join(parent, base) - if (file !== target) { - try { - renameSync(file, target) - console.log('FILE', relative(ROOT, file), '->', relative(ROOT, target)) - } catch { - /* noop */ - } - } -} - -const REPLACEMENTS = [ - ['/campanha/pracas', '/campanha/municipios'], - ["collection: 'plazaUpdate'", "collection: 'municipalityUpdate'"], - ["collection: 'plaza'", "collection: 'municipality'"], - ["relationTo: 'plazaUpdate'", "relationTo: 'municipalityUpdate'"], - ["relationTo: 'plaza'", "relationTo: 'municipality'"], - ['slug: plazaUpdate', 'slug: municipalityUpdate'], - ['slug: plaza', 'slug: municipality'], - ["'plazaUpdate'", "'municipalityUpdate'"], - ["'plaza'", "'municipality'"], - ['PlazaUpdate', 'MunicipalityUpdate'], - ['plazaUpdate', 'municipalityUpdate'], - ['PlazaEstimateScenario', 'MunicipalityEstimateScenario'], - ['plazaEstimateScenario', 'municipalityEstimateScenario'], - ['PlazaZoneNeighborhoods', 'MunicipalityZoneNeighborhoods'], - ['plazaZoneNeighborhoods', 'municipalityZoneNeighborhoods'], - ['plaza-zone-neighborhoods', 'municipality-zone-neighborhoods'], - ['plaza-catalog', 'municipality-catalog'], - ['plazaStaffFormActions', 'municipalityStaffFormActions'], - ['plazaStaffEditMessages', 'municipalityStaffEditMessages'], - ['plazaStaffVoteTotal', 'municipalityStaffVoteTotal'], - ['StaffPlazaVotes', 'StaffMunicipalityVotes'], - ['aggregatePledgesByPlaza', 'aggregatePledgesByMunicipality'], - ['loadPlazaListPageBundle', 'loadMunicipalityListPageBundle'], - ['loadPlazaMapBundle', 'loadMunicipalityMapBundle'], - ['buildPlazaMapBundleFromPlazas', 'buildMunicipalityMapBundleFromMunicipalities'], - ['buildPlazaMapBundle', 'buildMunicipalityMapBundle'], - ['buildPlazaListWhere', 'buildMunicipalityListWhere'], - ['shouldUpdatePlazaSearchUrl', 'shouldUpdateMunicipalitySearchUrl'], - ['buildPlazaFiltersKey', 'buildMunicipalityFiltersKey'], - ['getAccessiblePlazaIds', 'getAccessibleMunicipalityIds'], - ['canAssignPlazaAdvisors', 'canAssignMunicipalityAdvisors'], - ['canManagePlazaAdvisors', 'canManageMunicipalityAdvisors'], - ['canCreatePlaza', 'canCreateMunicipality'], - ['canDeletePlaza', 'canDeleteMunicipality'], - ['canReadPlaza', 'canReadMunicipality'], - ['canUpdatePlaza', 'canUpdateMunicipality'], - ['validatePlazaAdvisors', 'validateMunicipalityAdvisors'], - ['plazaCatalogEntriesForCity', 'municipalityCatalogEntriesForCity'], - ['getPlazaCatalogEntry', 'getMunicipalityCatalogEntry'], - ['plazaCatalog', 'municipalityCatalog'], - ['isPlazaSlug', 'isMunicipalitySlug'], - ['PlazaCatalogEntry', 'MunicipalityCatalogEntry'], - ['PlazaKind', 'MunicipalityKind'], - ['ZONE_PLAZA_CITIES', 'ZONE_MUNICIPALITY_CITIES'], - ['zonePlazaCitySet', 'zoneMunicipalityCitySet'], - ['plazasByIbgeCode', 'municipalitiesByIbgeCode'], - ['resolvePlazaMapNavigation', 'resolveMunicipalityMapNavigation'], - ['plazaMapNavigation', 'municipalityMapNavigation'], - ['plazaRevalidation', 'municipalityRevalidation'], - ['plazaSlug', 'municipalitySlug'], - ['plazaPageData', 'municipalityPageData'], - ['plazaViewModels', 'municipalityViewModels'], - ['plazaMapData', 'municipalityMapData'], - ['plazaUi', 'municipalityUi'], - ['plazaElectoralBaseline', 'municipalityElectoralBaseline'], - ['plazaElectionGeography', 'municipalityElectionGeography'], - ['plazaCandidateComparison', 'municipalityCandidateComparison'], - ['plazaDetailTabUi', 'municipalityDetailTabUi'], - ['campaignPlazaAccess', 'campaignMunicipalityAccess'], - ['campaignPlazas', 'campaignMunicipalities'], - ['campaignPlazaUpdate', 'campaignMunicipalityUpdate'], - ['updatePlazaStrategy', 'updateMunicipalityStrategy'], - ['PlazaMapBundle', 'MunicipalityMapBundle'], - ['plazaMapBundle', 'municipalityMapBundle'], - ['PlazaList', 'MunicipalityList'], - ['PlazaFilters', 'MunicipalityFilters'], - ['PlazaMap', 'MunicipalityMap'], - ['PlazaStrategy', 'MunicipalityStrategy'], - ['PlazaBaseline', 'MunicipalityBaseline'], - ['PlazaCandidate', 'MunicipalityCandidate'], - ['PlazaAdvisor', 'MunicipalityAdvisor'], - ['PlazaLeadership', 'MunicipalityLeadership'], - ['PlazaPledges', 'MunicipalityPledges'], - ['PlazaTab', 'MunicipalityTab'], - ['PlazaUpdate', 'MunicipalityUpdate'], - ['plazas', 'municipalities'], - ['Plaza', 'Municipality'], - ['plaza', 'municipality'], -] - -const contentFiles = walk(join(ROOT, 'src')) - .concat(walk(join(ROOT, 'tests'))) - .concat( - [ - join(ROOT, 'AGENTS.md'), - join(ROOT, 'PRODUCT.md'), - join(ROOT, 'docs/roadmap.md'), - join(ROOT, '.cursor/rules/projects/nucleos-eleitorais.mdc'), - ].filter((f) => { - try { - statSync(f) - return true - } catch { - return false - } - }), - ) - -for (const file of contentFiles) { - const rel = relative(ROOT, file) - if (shouldExclude(rel)) continue - let content = readFileSync(file, 'utf8') - const original = content - for (const [from, to] of REPLACEMENTS) { - content = content.split(from).join(to) - } - if (content !== original) { - writeFileSync(file, content) - console.log('CONTENT', rel) - } -} - -console.log('Done.') diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts b/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts index a47c540a..09189f55 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts @@ -3,7 +3,7 @@ import { redirect } from 'next/navigation' import { createStateDeputy } from '@/app/(campaign)/campanha/actions/stateDeputy' -import { nullableFormText, optionalFormText, requiredFormText } from '@/lib/formData' +import { optionalFormText, requiredFormText } from '@/lib/formData' import { mapCampaignFormActionError, type CampaignFormActionState, diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/nova/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/nova/page.tsx index 29fac025..86df6742 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/nova/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/nova/page.tsx @@ -1,4 +1,3 @@ -import config from '@payload-config' import { ArrowLeftIcon } from 'lucide-react' import Link from 'next/link' import { redirect } from 'next/navigation' diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx index da285424..070e8904 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx @@ -6,7 +6,6 @@ import { getPayload } from 'payload' import { CampaignListPagination } from '@/components/campaign/CampaignListPagination' import { CampaignPageShell } from '@/components/campaign/CampaignPageShell' -import { Badge } from '@/components/ui/Badge' import { Button } from '@/components/ui/button' import { Empty, diff --git a/src/app/(campaign)/campanha/actions/actionPlan.ts b/src/app/(campaign)/campanha/actions/actionPlan.ts index 8dfe082f..1f36ebaa 100644 --- a/src/app/(campaign)/campanha/actions/actionPlan.ts +++ b/src/app/(campaign)/campanha/actions/actionPlan.ts @@ -13,6 +13,7 @@ import { getCampaignActionContext, reloadCampaignActor } from '@/utilities/campa import { isCampaignStaff } from '@/utilities/campaignAccess' import { withPayloadTransaction } from '@/utilities/payloadTransaction' import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks' +import { hookFilledCreateData } from '@/utilities/hookFilledData' const MAX_ACTION_PLAN_UPDATE_BODY_LENGTH = 4000 const MAX_ACTION_PLAN_RESULT_SUMMARY_LENGTH = 6000 @@ -26,7 +27,7 @@ export const createActionPlanRecord = async ( return payload.create({ collection: 'actionPlan', - data: data as never, + data: hookFilledCreateData<'actionPlan'>(data), depth: 0, user: actor, overrideAccess: false, diff --git a/src/app/(campaign)/campanha/actions/demand.ts b/src/app/(campaign)/campanha/actions/demand.ts index 1df9bf26..68085ed3 100644 --- a/src/app/(campaign)/campanha/actions/demand.ts +++ b/src/app/(campaign)/campanha/actions/demand.ts @@ -21,6 +21,7 @@ import { } from '@/utilities/campaignActionContext' import { withPayloadTransaction } from '@/utilities/payloadTransaction' import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks' +import { hookFilledCreateData } from '@/utilities/hookFilledData' export const createCampaignDemandRecord = async ( payload: Payload, @@ -38,7 +39,7 @@ export const createCampaignDemandRecord = async ( // links a leader's own leadership and enforces the initial status. return payload.create({ collection: 'campaignDemand', - data: data as never, + data: hookFilledCreateData<'campaignDemand'>(data), depth: 0, user: currentActor, overrideAccess: false, diff --git a/src/app/(campaign)/campanha/actions/municipalityUpdate.ts b/src/app/(campaign)/campanha/actions/municipalityUpdate.ts index 20024099..aaa8bfc1 100644 --- a/src/app/(campaign)/campanha/actions/municipalityUpdate.ts +++ b/src/app/(campaign)/campanha/actions/municipalityUpdate.ts @@ -6,6 +6,7 @@ import { municipalityUpdateCreateSchema, type MunicipalityUpdateCreateInput } fr import type { CampaignUser } from '@/payload-types' import { getCampaignActionContext, reloadCampaignActor } from '@/utilities/campaignActionContext' import { withPayloadTransaction } from '@/utilities/payloadTransaction' +import { hookFilledCreateData } from '@/utilities/hookFilledData' export const createMunicipalityUpdateRecord = async ( payload: Payload, @@ -21,7 +22,7 @@ export const createMunicipalityUpdateRecord = async ( return payload.create({ collection: 'municipalityUpdate', - data: data as never, + data: hookFilledCreateData<'municipalityUpdate'>(data), depth: 0, user: currentActor, overrideAccess: false, diff --git a/src/app/(campaign)/campanha/actions/organization.ts b/src/app/(campaign)/campanha/actions/organization.ts index e3d76a5b..d4bfc1e1 100644 --- a/src/app/(campaign)/campanha/actions/organization.ts +++ b/src/app/(campaign)/campanha/actions/organization.ts @@ -10,6 +10,7 @@ import { } from '@/lib/schemas/organization' import type { CampaignUser } from '@/payload-types' import { getCampaignActionContext, reloadStaffActor } from '@/utilities/campaignActionContext' +import { hookFilledCreateData } from '@/utilities/hookFilledData' const assertStaffActor = (payload: Payload, actor: CampaignUser): Promise => reloadStaffActor(payload, actor, 'Somente a coordenação e a assessoria gerenciam organizações.') @@ -30,7 +31,7 @@ export const createOrganizationRecord = async ( try { return await payload.create({ collection: 'organization', - data: data as never, + data: hookFilledCreateData<'organization'>(data), depth: 0, user: currentActor, overrideAccess: false, diff --git a/src/app/(campaign)/campanha/actions/stateDeputy.ts b/src/app/(campaign)/campanha/actions/stateDeputy.ts index b2cb6a21..dfb92d78 100644 --- a/src/app/(campaign)/campanha/actions/stateDeputy.ts +++ b/src/app/(campaign)/campanha/actions/stateDeputy.ts @@ -10,6 +10,7 @@ import { } from '@/lib/schemas/stateDeputy' import type { CampaignUser } from '@/payload-types' import { getCampaignActionContext, reloadStaffActor } from '@/utilities/campaignActionContext' +import { hookFilledCreateData } from '@/utilities/hookFilledData' const assertStaffActor = (payload: Payload, actor: CampaignUser): Promise => reloadStaffActor(payload, actor, 'Somente a coordenação e a assessoria gerenciam dobradinhas.') @@ -30,7 +31,7 @@ export const createStateDeputyRecord = async ( try { return await payload.create({ collection: 'stateDeputy', - data: data as never, + data: hookFilledCreateData<'stateDeputy'>(data), depth: 0, user: currentActor, overrideAccess: false, diff --git a/src/components/MetaPixel.tsx b/src/components/MetaPixel.tsx index e2266943..d6009b5c 100644 --- a/src/components/MetaPixel.tsx +++ b/src/components/MetaPixel.tsx @@ -19,6 +19,8 @@ fbq('track', 'PageView'); `} + {/* Meta's noscript tracking pixel must be a plain 1×1 . */} + {/* eslint-disable-next-line @next/next/no-img-element */} { if (!interactiveKey || pathByKeyRef.current.size === 0) return restyleAllPaths() + // Restyle helpers only read refs and are recreated per render — depending + // on them would re-run this effect on every render. + // eslint-disable-next-line react-hooks/exhaustive-deps -- interactiveKey-only restyle }, [interactiveKey]) useEffect(() => { @@ -425,6 +428,9 @@ export const BahiaMap = ({ if (selectedKey) { restyleFeature(selectedKey) } + // Restyle helpers only read refs and are recreated per render — depending + // on them would re-run this effect on every render. + // eslint-disable-next-line react-hooks/exhaustive-deps -- selectedKey-only O(2) restyle }, [selectedKey]) return ( diff --git a/src/components/campaign/CreateCampaignClientDynamic.tsx b/src/components/campaign/CreateCampaignClientDynamic.tsx deleted file mode 100644 index 53fbbfea..00000000 --- a/src/components/campaign/CreateCampaignClientDynamic.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client' - -import dynamic from 'next/dynamic' -import type { ComponentType } from 'react' - -import { Skeleton } from '@/components/ui/skeleton' - -export const createCampaignClientDynamic = ( - importModule: () => Promise>>, - exportName: string, - skeletonHeightClassName: string, -) => - dynamic(() => importModule().then((module) => module[exportName]), { - ssr: false, - loading: () => ( - - ), - }) diff --git a/src/components/campaign/LazyDialogModuleFallback.tsx b/src/components/campaign/LazyDialogModuleFallback.tsx deleted file mode 100644 index d9f0c468..00000000 --- a/src/components/campaign/LazyDialogModuleFallback.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client' - -import { AlertCircleIcon } from 'lucide-react' - -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert' -import { Button } from '@/components/ui/button' -import { - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import { Spinner } from '@/components/ui/Spinner' - -export const LazyDialogModuleFallback = ({ - description, - loadingLabel, - onRetry, - status, - title, -}: { - description: string - loadingLabel: string - onRetry: () => void - status: 'idle' | 'loading' | 'error' - title: string -}) => ( - - - {title} - {description} - - {status === 'error' ? ( - <> - - - Conteúdo indisponível - Não foi possível carregar este conteúdo. - - - - - Fechar - - - - Tentar novamente - - - > - ) : ( - - - - )} - -) diff --git a/src/components/campaign/MunicipalityList.tsx b/src/components/campaign/MunicipalityList.tsx index f5dd03eb..6afa5271 100644 --- a/src/components/campaign/MunicipalityList.tsx +++ b/src/components/campaign/MunicipalityList.tsx @@ -156,7 +156,6 @@ export const MunicipalityList = ({ {municipalities.map((municipality) => { - const names = advisorNames(municipality) const hasAdvisor = municipality.advisorIDs.length > 0 return ( diff --git a/src/components/campaign/RecentVisitTracker.tsx b/src/components/campaign/RecentVisitTracker.tsx index 75df52c5..8dfc7b61 100644 --- a/src/components/campaign/RecentVisitTracker.tsx +++ b/src/components/campaign/RecentVisitTracker.tsx @@ -13,15 +13,19 @@ export const RecentVisitTracker = ({ }: { entry: Omit }) => { + // Primitive dependencies: the effect must re-run on visit identity changes, + // not on parent re-renders handing down a fresh `entry` object. + const { href, label, kind } = entry + useEffect(() => { const timer = window.setTimeout(() => { - recordRecentVisit({ ...entry, visitedAt: Date.now() }) + recordRecentVisit({ href, label, kind, visitedAt: Date.now() }) }, RECORD_DWELL_MS) return () => { window.clearTimeout(timer) } - }, [entry.href, entry.label, entry.kind]) + }, [href, label, kind]) return null } diff --git a/src/components/campaign/RecentlyVisited.tsx b/src/components/campaign/RecentlyVisited.tsx deleted file mode 100644 index 4a7dd286..00000000 --- a/src/components/campaign/RecentlyVisited.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client' - -import { useCallback, useState } from 'react' - -import { RecentlyVisitedCard } from '@/components/campaign/RecentlyVisitedCard' -import { UpcomingActionPlansCard } from '@/components/campaign/UpcomingActionPlansCard' -import { cn } from '@/lib/utils' -import type { ActionPlanUpcomingPreviewRecord } from '@/utilities/actionPlanUpcomingPreview' -import { listRecentVisits } from '@/utilities/recentVisits' - -type RecentlyVisitedProps = { - now: Date - upcomingPlans: ActionPlanUpcomingPreviewRecord[] -} - -export const RecentlyVisited = ({ now, upcomingPlans }: RecentlyVisitedProps) => { - const [hasRecent, setHasRecent] = useState(() => listRecentVisits().length > 0) - const handleVisitCountChange = useCallback((count: number) => { - setHasRecent(count > 0) - }, []) - - return ( - - - - - ) -} diff --git a/src/components/campaign/UpcomingActionPlansCard.tsx b/src/components/campaign/UpcomingActionPlansCard.tsx deleted file mode 100644 index 66872062..00000000 --- a/src/components/campaign/UpcomingActionPlansCard.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { ArrowRightIcon } from 'lucide-react' -import Link from 'next/link' - -import { Badge } from '@/components/ui/Badge' -import { Button } from '@/components/ui/button' -import { Card, CardAction, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { actionPlanKindLabels } from '@/lib/schemas/actionPlan' -import { cn } from '@/lib/utils' -import type { ActionPlanUpcomingPreviewRecord } from '@/utilities/actionPlanUpcomingPreview' -import { formatBahiaDateTimeLabel } from '@/utilities/campaignTime' - -export const UpcomingActionPlansCard = ({ - plans, - className, -}: { - plans: ActionPlanUpcomingPreviewRecord[] - className?: string -}) => ( - - - Próximos eventos - - - - Ver todos - - - - - - - {plans.length ? ( - - {plans.map((plan) => ( - - - {actionPlanKindLabels[plan.kind]} - - {formatBahiaDateTimeLabel(plan.startAt)} - {plan.municipalityName ? ` · ${plan.municipalityName}` : ''} - - - - {plan.title} - - - ))} - - ) : ( - Nenhum evento agendado - )} - - -) diff --git a/src/components/campaign/useLazyDialogModule.ts b/src/components/campaign/useLazyDialogModule.ts deleted file mode 100644 index bcb23e03..00000000 --- a/src/components/campaign/useLazyDialogModule.ts +++ /dev/null @@ -1,67 +0,0 @@ -'use client' - -import { useCallback, useEffect, useRef, useState } from 'react' - -type LazyDialogModule = { default: T } - -type LazyDialogModuleState = - | { status: 'idle'; component: null } - | { status: 'loading'; component: null } - | { status: 'ready'; component: T } - | { status: 'error'; component: null } - -export const useLazyDialogModule = ( - loadModule: () => Promise>, -) => { - const [state, setState] = useState>({ - status: 'idle', - component: null, - }) - const statusRef = useRef['status']>('idle') - const attemptRef = useRef(0) - const mountedRef = useRef(true) - - useEffect(() => { - mountedRef.current = true - return () => { - mountedRef.current = false - attemptRef.current += 1 - } - }, []) - - const updateState = useCallback((nextState: LazyDialogModuleState) => { - statusRef.current = nextState.status - setState(nextState) - }, []) - - const load = useCallback(() => { - if (statusRef.current === 'loading' || statusRef.current === 'ready') return - - const attempt = ++attemptRef.current - updateState({ status: 'loading', component: null }) - - void Promise.resolve() - .then(loadModule) - .then(({ default: component }) => { - if (!mountedRef.current || attempt !== attemptRef.current) return - updateState({ status: 'ready', component }) - }) - .catch(() => { - if (!mountedRef.current || attempt !== attemptRef.current) return - updateState({ status: 'error', component: null }) - }) - }, [loadModule, updateState]) - - const resetAfterClose = useCallback(() => { - if (statusRef.current !== 'error') return - attemptRef.current += 1 - updateState({ status: 'idle', component: null }) - }, [updateState]) - - return { - component: state.component, - load, - resetAfterClose, - status: state.status, - } -} diff --git a/src/components/ui/DropdownMenu.tsx b/src/components/ui/DropdownMenu.tsx deleted file mode 100644 index 1d97dc0b..00000000 --- a/src/components/ui/DropdownMenu.tsx +++ /dev/null @@ -1,234 +0,0 @@ -'use client' - -import * as React from 'react' -import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui' - -import { cn } from '@/lib/utils' -import { CheckIcon, ChevronRightIcon } from 'lucide-react' - -const DropdownMenu = ({ ...props }: React.ComponentProps) => ( - -) - -const DropdownMenuPortal = ({ - ...props -}: React.ComponentProps) => ( - -) - -const DropdownMenuTrigger = ({ - ...props -}: React.ComponentProps) => ( - -) - -const DropdownMenuContent = ({ - className, - align = 'start', - sideOffset = 4, - ...props -}: React.ComponentProps) => ( - - - -) - -const DropdownMenuGroup = ({ - ...props -}: React.ComponentProps) => ( - -) - -const DropdownMenuItem = ({ - className, - inset, - variant = 'default', - ...props -}: React.ComponentProps & { - inset?: boolean - variant?: 'default' | 'destructive' -}) => ( - -) - -const DropdownMenuCheckboxItem = ({ - className, - children, - checked, - inset, - ...props -}: React.ComponentProps & { - inset?: boolean -}) => ( - - - - - - - {children} - -) - -const DropdownMenuRadioGroup = ({ - ...props -}: React.ComponentProps) => ( - -) - -const DropdownMenuRadioItem = ({ - className, - children, - inset, - ...props -}: React.ComponentProps & { - inset?: boolean -}) => ( - - - - - - - {children} - -) - -const DropdownMenuLabel = ({ - className, - inset, - ...props -}: React.ComponentProps & { - inset?: boolean -}) => ( - -) - -const DropdownMenuSeparator = ({ - className, - ...props -}: React.ComponentProps) => ( - -) - -const DropdownMenuShortcut = ({ className, ...props }: React.ComponentProps<'span'>) => ( - -) - -const DropdownMenuSub = ({ ...props }: React.ComponentProps) => ( - -) - -const DropdownMenuSubTrigger = ({ - className, - inset, - children, - ...props -}: React.ComponentProps & { - inset?: boolean -}) => ( - - {children} - - -) - -const DropdownMenuSubContent = ({ - className, - ...props -}: React.ComponentProps) => ( - -) - -export { - DropdownMenu, - DropdownMenuPortal, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuLabel, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubTrigger, - DropdownMenuSubContent, -} diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx deleted file mode 100644 index bceb5bea..00000000 --- a/src/components/ui/Tabs.tsx +++ /dev/null @@ -1,78 +0,0 @@ -'use client' - -import * as React from 'react' -import { cva, type VariantProps } from 'class-variance-authority' -import { Tabs as TabsPrimitive } from 'radix-ui' - -import { cn } from '@/lib/utils' - -const Tabs = ({ - className, - orientation = 'horizontal', - ...props -}: React.ComponentProps) => ( - -) - -const tabsListVariants = cva( - 'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:max-w-full group-data-horizontal/tabs:justify-start group-data-horizontal/tabs:overflow-x-auto group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none', - { - variants: { - variant: { - default: 'bg-muted', - line: 'gap-1 bg-transparent', - }, - }, - defaultVariants: { - variant: 'default', - }, - }, -) - -const TabsList = ({ - className, - variant = 'default', - ...props -}: React.ComponentProps & VariantProps) => ( - -) - -const TabsTrigger = ({ - className, - ...props -}: React.ComponentProps) => ( - -) - -const TabsContent = ({ - className, - ...props -}: React.ComponentProps) => ( - -) - -export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx deleted file mode 100644 index 9542be91..00000000 --- a/src/components/ui/select.tsx +++ /dev/null @@ -1,185 +0,0 @@ -'use client' - -import * as React from 'react' -import { Select as SelectPrimitive } from 'radix-ui' - -import { cn } from '@/lib/utils' -import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from 'lucide-react' - -function Select({ ...props }: React.ComponentProps) { - return -} - -function SelectGroup({ className, ...props }: React.ComponentProps) { - return ( - - ) -} - -function SelectValue({ ...props }: React.ComponentProps) { - return -} - -function SelectTrigger({ - className, - size = 'default', - children, - ...props -}: React.ComponentProps & { - size?: 'sm' | 'default' -}) { - return ( - - {children} - - - - - ) -} - -function SelectContent({ - className, - children, - position = 'item-aligned', - align = 'center', - ...props -}: React.ComponentProps) { - return ( - - - - - {children} - - - - - ) -} - -function SelectLabel({ className, ...props }: React.ComponentProps) { - return ( - - ) -} - -function SelectItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ) -} - -function SelectSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function SelectScrollUpButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ) -} - -function SelectScrollDownButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ) -} - -export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -} diff --git a/src/utilities/actionPlanUpcomingPreview.ts b/src/utilities/actionPlanUpcomingPreview.ts deleted file mode 100644 index 84807492..00000000 --- a/src/utilities/actionPlanUpcomingPreview.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Payload, Where } from 'payload' - -import type { ActionPlan, CampaignUser } from '@/payload-types' -import { relationshipId } from '@/utilities/relationship' - -export const actionPlanUpcomingPreviewLimit = 3 - -const upcomingActionPlanSelect = { - title: true, - slug: true, - kind: true, - startAt: true, - municipality: true, -} as const - -export type ActionPlanUpcomingPreviewRecord = { - id: number - slug: string - title: string - kind: ActionPlan['kind'] - startAt: string - municipalityName: string | null -} - -export type ActionPlanUpcomingPreviewFilters = { - municipality?: number -} - -const loadMunicipalityNamesById = async ( - payload: Pick, - municipalityIds: number[], -): Promise> => { - if (municipalityIds.length === 0) return new Map() - - // Display-name lookup: row access on the plans already gated visibility. - const result = await payload.find({ - collection: 'municipality', - where: { id: { in: municipalityIds } }, - depth: 0, - pagination: false, - select: { name: true }, - overrideAccess: true, - }) - - return new Map(result.docs.map((municipality) => [municipality.id, municipality.name])) -} - -export const loadUpcomingActionPlansPreview = async ( - payload: Pick, - user: CampaignUser, - now: Date, - options: { - limit?: number - filters?: ActionPlanUpcomingPreviewFilters - } = {}, -): Promise => { - const limit = options.limit ?? actionPlanUpcomingPreviewLimit - const filters: Where[] = [ - { status: { in: ['planejado', 'confirmado'] } }, - { startAt: { greater_than_equal: now.toISOString() } }, - ] - if (options.filters?.municipality) { - filters.push({ municipality: { equals: options.filters.municipality } }) - } - - const result = await payload.find({ - collection: 'actionPlan', - where: { and: filters }, - depth: 0, - limit, - page: 1, - sort: 'startAt', - select: upcomingActionPlanSelect, - user, - overrideAccess: false, - }) - - const municipalityIds = [ - ...new Set( - result.docs - .map((plan) => relationshipId(plan.municipality)) - .filter((id): id is number => id !== null), - ), - ] - const municipalityNamesById = await loadMunicipalityNamesById(payload, municipalityIds) - - return result.docs.map((plan) => { - const municipalityId = relationshipId(plan.municipality) - return { - id: plan.id, - slug: plan.slug, - title: plan.title, - kind: plan.kind, - startAt: plan.startAt as string, - municipalityName: municipalityId ? (municipalityNamesById.get(municipalityId) ?? null) : null, - } - }) -} diff --git a/src/utilities/electionResultsImport.ts b/src/utilities/electionResultsImport.ts index ea7a6026..af2b93c3 100644 --- a/src/utilities/electionResultsImport.ts +++ b/src/utilities/electionResultsImport.ts @@ -1,4 +1,4 @@ -import { and, eq } from '@payloadcms/db-postgres/drizzle' +import { and, eq, type AnyColumn } from '@payloadcms/db-postgres/drizzle' import type { Payload } from 'payload' import type { @@ -57,11 +57,21 @@ type PayloadDbDrizzle = { } } +/** Payload's generated Drizzle tables type columns as `unknown` — narrow with a runtime check. */ +const scopeColumn = ( + table: Record, + name: 'year' | 'office' | 'turn', +): AnyColumn => { + const column = table[name] + if (column == null) throw new Error(`Election table is missing the "${name}" column.`) + return column as AnyColumn +} + const scopeWhere = (table: Record, scope: ElectionImportScope) => and( - eq(table.year as never, scope.year), - eq(table.office as never, scope.office), - eq(table.turn as never, scope.turn), + eq(scopeColumn(table, 'year'), scope.year), + eq(scopeColumn(table, 'office'), scope.office), + eq(scopeColumn(table, 'turn'), scope.turn), ) const deleteScope = async ( diff --git a/src/utilities/federalCandidateTotalsAggregate.ts b/src/utilities/federalCandidateTotalsAggregate.ts deleted file mode 100644 index 30f3877a..00000000 --- a/src/utilities/federalCandidateTotalsAggregate.ts +++ /dev/null @@ -1,58 +0,0 @@ -import 'server-only' - -import { sql } from '@payloadcms/db-postgres' -import type { Payload } from 'payload' - -import { BASELINE_TICKET_2022, ELECTION_YEAR_2022 } from '@/lib/electionResults' -import { assertCanReadElectionData, type ElectionDataReader } from '@/utilities/campaignAccess' -import { drizzleResultRows, requirePostgresDrizzle } from '@/utilities/drizzleBulk' -import { type MunicipalityElectionGeography, zonesByCityCode } from '@/utilities/municipalityElectionGeography' - -export type FederalCandidateTotal = { - candidateNumber: number - name: string - party: string - votes: number -} - -export const loadFederalCandidateTotalsAggregated = async ( - payload: Pick, - user: ElectionDataReader, - geography: MunicipalityElectionGeography, -): Promise => { - assertCanReadElectionData(user) - - const cityClauses = [...zonesByCityCode(geography).entries()].map( - ([cityCode, zones]) => - sql`("election_candidate_vote"."city_code" = ${cityCode} AND "election_candidate_vote"."zone_number" IN (${sql.join( - zones.map((zone) => sql`${zone}`), - sql`, `, - )}))`, - ) - - const drizzle = requirePostgresDrizzle(payload, 'agregação federal') - const result = await drizzle.execute(sql` - SELECT - "election_candidate_vote"."candidate_number" AS "candidate_number", - MAX("election_candidate_vote"."candidate_name") AS "candidate_name", - COALESCE(MAX("election_candidate_vote"."party"), '') AS "party", - SUM("election_candidate_vote"."votes")::int AS "votes" - FROM "election_candidate_vote" - WHERE - "election_candidate_vote"."year" = ${ELECTION_YEAR_2022} - AND "election_candidate_vote"."state" = 'BA' - AND "election_candidate_vote"."office" = ${BASELINE_TICKET_2022.candidate.office} - AND "election_candidate_vote"."turn" = '1' - AND "election_candidate_vote"."vote_type" = 'nominal' - AND (${sql.join(cityClauses, sql` OR `)}) - GROUP BY "election_candidate_vote"."candidate_number" - ORDER BY "votes" DESC, "election_candidate_vote"."candidate_number" ASC - `) - - return drizzleResultRows(result).map((row) => ({ - candidateNumber: Number(row.candidate_number), - name: String(row.candidate_name ?? ''), - party: String(row.party), - votes: Number(row.votes ?? 0), - })) -} diff --git a/src/utilities/hookFilledData.ts b/src/utilities/hookFilledData.ts new file mode 100644 index 00000000..f5fc5f00 --- /dev/null +++ b/src/utilities/hookFilledData.ts @@ -0,0 +1,13 @@ +import type { CollectionSlug, RequiredDataFromCollectionSlug } from 'payload' + +/** + * Types zod-validated create input as a collection's full create data when the + * remaining required fields (canonical slug, authorship, workflow status) are + * filled by the collection's own beforeValidate/beforeChange hooks. + * + * Unlike an `as never` cast, property-level type checking is preserved for + * every field the caller does provide — only completeness is waived. + */ +export const hookFilledCreateData = ( + data: Partial>, +): RequiredDataFromCollectionSlug => data as RequiredDataFromCollectionSlug diff --git a/src/utilities/tseZone.ts b/src/utilities/tseZone.ts deleted file mode 100644 index a5234a13..00000000 --- a/src/utilities/tseZone.ts +++ /dev/null @@ -1,25 +0,0 @@ -export class TseZoneParseError extends Error {} - -export const sortedUniqueZoneNumbers = (values: readonly number[]): number[] => - [...new Set(values)].sort((left, right) => left - right) - -export const parseTseZoneNumbers = (value: string): number[] => { - if (!/^[\d,\s]*$/.test(value)) { - throw new TseZoneParseError('Use apenas números, vírgulas e espaços nas Zonas TSE.') - } - - return sortedUniqueZoneNumbers( - value - .split(/[,\s]+/) - .filter(Boolean) - .map((token) => { - const zoneNumber = Number(token) - if (!Number.isInteger(zoneNumber) || zoneNumber < 1 || zoneNumber > 999) { - throw new TseZoneParseError( - `Zona TSE inválida: "${token}". Use números de 1 a 999.`, - ) - } - return zoneNumber - }), - ) -} diff --git a/tests/e2e/fixtures/campaignE2EFixtures.ts b/tests/e2e/fixtures/campaignE2EFixtures.ts index 0bb26856..ef392726 100644 --- a/tests/e2e/fixtures/campaignE2EFixtures.ts +++ b/tests/e2e/fixtures/campaignE2EFixtures.ts @@ -60,7 +60,7 @@ class CampaignE2EOwnership { get: (_target, property) => { if (property === 'create') { return async (args: Parameters[0]) => { - const document = await rootPayload.create(args as never) + const document = await rootPayload.create(args) if (this.isOwnedCollection(args.collection) && typeof document.id === 'number') { this.own(args.collection, document.id) } diff --git a/tests/helpers/campaignFixtures.ts b/tests/helpers/campaignFixtures.ts index 77d4d121..ffb60bfe 100644 --- a/tests/helpers/campaignFixtures.ts +++ b/tests/helpers/campaignFixtures.ts @@ -308,7 +308,7 @@ export class CampaignFixtures { builderCounter += 1 this.runID = `${processRunID}-${builderCounter}` const trackedCreate = async (args: Parameters[0]) => { - const document = await rootPayload.create(args as never) + const document = await rootPayload.create(args) const collection = args.collection const isLeasedConsent = collection === 'consent' && @@ -609,7 +609,7 @@ export class CampaignFixtures { contact: relationshipID(input.contact), municipalities: input.municipalities.map(relationshipID), ...(input.organizations - ? { organizations: input.organizations.map((value) => relationshipID(value as never)) } + ? { organizations: input.organizations.map((value) => relationshipID(value)) } : {}), ...(input.user ? { user: relationshipID(input.user) } : {}), ...(input.createdBy ? { createdBy: relationshipID(input.createdBy) } : {}), @@ -917,7 +917,7 @@ export class CampaignFixtures { collection, where: { id: { in: ids } }, depth: 0, - req: req as never, + req, }) const errors = bulkErrors(result) if (errors.length > 0) { @@ -943,7 +943,7 @@ export class CampaignFixtures { lastUpdateAt: null, }, depth: 0, - req: req as never, + req, }) } } diff --git a/tests/helpers/stub.ts b/tests/helpers/stub.ts new file mode 100644 index 00000000..0c98be64 --- /dev/null +++ b/tests/helpers/stub.ts @@ -0,0 +1,7 @@ +/** + * Types a partial test double as the full contract it stands in for. + * + * Unlike casting through `never`, the keys the test DOES provide keep full + * property-level type checking against `T` — only completeness is waived. + */ +export const stub = (partial: Partial): T => partial as T diff --git a/tests/int/campaignActionPlan.int.spec.ts b/tests/int/campaignActionPlan.int.spec.ts index a2216fe3..567a3dc3 100644 --- a/tests/int/campaignActionPlan.int.spec.ts +++ b/tests/int/campaignActionPlan.int.spec.ts @@ -1,6 +1,11 @@ // @vitest-environment node -import { getPayload, type Payload } from 'payload' +import { + getPayload, + type Payload, + type PayloadRequest, + type RequiredDataFromCollectionSlug, +} from 'payload' import { beforeAll, describe, expect, it } from 'vitest' import { actionPlanCreateSchema, actionPlanUpdateSchema } from '@/lib/schemas/actionPlan' @@ -13,6 +18,10 @@ import { } from '@/utilities/campaignAccess' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' + +/** Create data with server-derived fields (e.g. `slug`) intentionally omitted. */ +type ActionPlanCreateData = RequiredDataFromCollectionSlug<'actionPlan'> let payload: Payload const campaignFixtures = installCampaignFixtures({ @@ -88,13 +97,13 @@ describe('action plan domain', () => { }) const createAsCoordinator = await canCreateActionPlan({ - req: { user: coordinator, payload, context: {} } as never, + req: stub({ user: coordinator, payload, context: {} }), }) const createAsAdvisor = await canCreateActionPlan({ - req: { user: advisor, payload, context: {} } as never, + req: stub({ user: advisor, payload, context: {} }), }) const createAsLeader = await canCreateActionPlan({ - req: { user: leaderAccount, payload, context: {} } as never, + req: stub({ user: leaderAccount, payload, context: {} }), }) expect(createAsCoordinator).toBe(true) expect(createAsAdvisor).toBe(true) @@ -102,49 +111,49 @@ describe('action plan domain', () => { const plan = await payload.create({ collection: 'actionPlan', - data: { + data: stub({ ...validPlanInput(municipality.id), advisors: [advisor.id], leadership: leadership.id, responsible: contact.id, createdBy: coordinator.id, - } as never, + }), overrideAccess: true, }) fixtures.own('actionPlan', plan.id) const coordinatorRead = await canReadActionPlan({ - req: { user: coordinator, payload, context: {} } as never, + req: stub({ user: coordinator, payload, context: {} }), }) expect(coordinatorRead).toBe(true) const advisorRead = await canReadActionPlan({ - req: { user: advisor, payload, context: {} } as never, + req: stub({ user: advisor, payload, context: {} }), }) expect(advisorRead).toEqual({ or: [{ advisors: { contains: advisor.id } }, { municipality: { in: [municipality.id] } }], }) const otherAdvisorRead = await canReadActionPlan({ - req: { user: otherAdvisor, payload, context: {} } as never, + req: stub({ user: otherAdvisor, payload, context: {} }), }) expect(otherAdvisorRead).toEqual({ or: [{ advisors: { contains: otherAdvisor.id } }, { municipality: { in: [] } }], }) const leaderRead = await canReadActionPlan({ - req: { user: leaderAccount, payload, context: {} } as never, + req: stub({ user: leaderAccount, payload, context: {} }), }) expect(leaderRead).toBe(false) const leadershipIds = await getAccessibleLeadershipIds( - { user: leaderAccount, payload, context: {} } as never, + stub({ user: leaderAccount, payload, context: {} }), leaderAccount, ) expect(leadershipIds).toEqual([]) const advisorUpdate = await canUpdateActionPlan({ - req: { user: advisor, payload, context: {} } as never, + req: stub({ user: advisor, payload, context: {} }), }) expect(advisorUpdate).toEqual({ or: [{ advisors: { contains: advisor.id } }, { municipality: { in: [municipality.id] } }], @@ -187,10 +196,10 @@ describe('action plan domain', () => { const plan = await payload.create({ collection: 'actionPlan', - data: { + data: stub({ ...validPlanInput(municipality.id), title: fixtures.value('Plano do assessor'), - } as never, + }), user: advisor, overrideAccess: false, }) @@ -213,12 +222,12 @@ describe('action plan domain', () => { await expect( payload.create({ collection: 'actionPlan', - data: { + data: stub({ ...validPlanInput(municipality.id), title: fixtures.value('Plano sem data'), startAt: null, createdBy: coordinator.id, - } as never, + }), overrideAccess: true, }), ).rejects.toThrow('Informe a data e horário de início ao planejar ou confirmar o plano.') @@ -227,13 +236,13 @@ describe('action plan domain', () => { await expect( payload.create({ collection: 'actionPlan', - data: { + data: stub({ ...validPlanInput(municipality.id), title: fixtures.value('Plano com término invertido'), startAt: start.toISOString(), endAt: new Date(start.getTime() - 3_600_000).toISOString(), createdBy: coordinator.id, - } as never, + }), overrideAccess: true, }), ).rejects.toThrow('O horário de término deve ser posterior ao de início.') @@ -255,13 +264,13 @@ describe('action plan domain', () => { const plan = await payload.create({ collection: 'actionPlan', - data: { + data: stub({ ...validPlanInput(municipality.id), title: fixtures.value('Plano Tarefas'), advisors: [advisor.id], leadership: leadership.id, tasks: [{ title: 'Levar faixas', done: false }], - } as never, + }), overrideAccess: true, }) fixtures.own('actionPlan', plan.id) @@ -272,7 +281,7 @@ describe('action plan domain', () => { id: plan.id, data: { tasks: [{ title: 'Levar faixas', done: true }], - } as never, + }, user: leaderAccount, overrideAccess: false, }), diff --git a/tests/int/campaignAuth.int.spec.ts b/tests/int/campaignAuth.int.spec.ts index 54a1c606..74213604 100644 --- a/tests/int/campaignAuth.int.spec.ts +++ b/tests/int/campaignAuth.int.spec.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs' -import type { Payload, PayloadRequest } from 'payload' +import type { Payload, PayloadRequest, RequiredDataFromCollectionSlug } from 'payload' import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { assignMunicipalityAdvisorsRecord } from '@/app/(campaign)/campanha/actions/municipality' @@ -20,6 +20,10 @@ import { import { getPayload } from 'payload' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' + +/** Create data with `role` intentionally omitted so the collection default applies. */ +type CampaignUserCreateData = RequiredDataFromCollectionSlug<'campaignUser'> let payload: Payload const campaignFixtures = installCampaignFixtures({ @@ -52,11 +56,11 @@ describe('campaign authentication foundation', () => { const username = campaignFixtures().phone() const user = await payload.create({ collection: 'campaignUser', - data: { + data: stub({ name: 'Liderança padrão', username, password: campaignFixtures().value('password'), - } as never, + }), }) expect(user.role).toBe('leader') @@ -267,11 +271,11 @@ describe('campaign authentication foundation', () => { const username = campaignFixtures().phone() const target = await payload.create({ collection: 'campaignUser', - data: { + data: stub({ name: 'Liderança administrada', username, password: campaignFixtures().value('password'), - } as never, + }), }) const visibleTarget = await payload.findByID({ @@ -383,11 +387,11 @@ describe('campaign authentication foundation', () => { await expect( payload.create({ collection: 'campaignUser', - data: { + data: stub({ name: 'Criação indevida', email: `${campaignFixtures().value('denied')}@example.com`, password: campaignFixtures().value('password'), - } as never, + }), user: actor, overrideAccess: false, }), diff --git a/tests/int/campaignFixturesRowCounts.int.spec.ts b/tests/int/campaignFixturesRowCounts.int.spec.ts index c4d45bc4..f08f82ea 100644 --- a/tests/int/campaignFixturesRowCounts.int.spec.ts +++ b/tests/int/campaignFixturesRowCounts.int.spec.ts @@ -2,20 +2,19 @@ import { createRequire } from 'node:module' -import { sql } from '@payloadcms/db-postgres' +import { sql, type MigrateUpArgs } from '@payloadcms/db-postgres' import { drizzle } from '@payloadcms/db-postgres/drizzle/node-postgres' import { getPayload, type Payload } from 'payload' +import type { Pool as PgPool } from 'pg' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { migrations } from '@/migrations' import { withCampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' const require = createRequire(import.meta.resolve('@payloadcms/db-postgres')) const { Pool } = require('pg') as { - Pool: new (options: { connectionString: string }) => { - end: () => Promise - query: (statement: string) => Promise - } + Pool: new (options: { connectionString: string }) => PgPool } const testDatabaseUrl = @@ -113,9 +112,9 @@ beforeAll(async () => { await adminPool.end() const migrationPool = new Pool({ connectionString: databaseUrl }) - const database = drizzle(migrationPool as never) + const database = drizzle>(migrationPool) for (const migration of migrations) { - await migration.up({ db: database } as never) + await migration.up(stub({ db: database })) } await migrationPool.end() diff --git a/tests/int/campaignInvite.int.spec.ts b/tests/int/campaignInvite.int.spec.ts index bf16a812..3a75203b 100644 --- a/tests/int/campaignInvite.int.spec.ts +++ b/tests/int/campaignInvite.int.spec.ts @@ -922,7 +922,10 @@ describe('campaign invite domain', () => { const actions = await import('@/app/(campaign)/campanha/actions/invite') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const invite = await createInviteThroughAction(coordinator, { leadership: leadership.id, kind: 'autopreenchimento', @@ -983,7 +986,10 @@ describe('campaign invite domain', () => { const actions = await import('@/app/(campaign)/campanha/actions/invite') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const invite = await createInviteThroughAction(coordinator, { leadership: leadership.id, kind: 'autopreenchimento', @@ -1318,7 +1324,10 @@ describe('campaign invite domain', () => { const actions = await import('@/app/(campaign)/campanha/actions/invite') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const invite = await createInviteThroughAction(coordinator, { leadership: leadership.id, kind: 'login', @@ -1370,7 +1379,10 @@ describe('campaign invite domain', () => { }) const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const account = await payload.create({ collection: 'campaignUser', data: { @@ -1425,7 +1437,10 @@ describe('campaign invite domain', () => { const { getCampaignInvitePageData } = await import('@/utilities/campaignInvitePageData') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const firstInvite = await createInviteThroughAction(coordinator, { leadership: leadership.id, kind: 'autopreenchimento', @@ -1471,7 +1486,7 @@ describe('campaign invite domain', () => { version: 1, }, }, - } as never, + }, }) const secondInvite = await createInviteThroughAction(coordinator, { leadership: leadership.id, @@ -1514,7 +1529,10 @@ describe('campaign invite domain', () => { const records = await import('@/app/(campaign)/campanha/actions/invite') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const victim = await payload.create({ collection: 'campaignUser', data: { @@ -1602,7 +1620,10 @@ describe('campaign invite domain', () => { const records = await import('@/app/(campaign)/campanha/actions/invite') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const invite = await createInviteThroughAction(coordinator, { leadership: leadership.id, kind: 'autopreenchimento', @@ -1654,7 +1675,10 @@ describe('campaign invite domain', () => { const actions = await import('@/app/(campaign)/campanha/actions/invite') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const account = await payload.create({ collection: 'campaignUser', data: { diff --git a/tests/int/campaignLeadership.int.spec.ts b/tests/int/campaignLeadership.int.spec.ts index 6b0ec1bd..f755d353 100644 --- a/tests/int/campaignLeadership.int.spec.ts +++ b/tests/int/campaignLeadership.int.spec.ts @@ -66,7 +66,7 @@ describe('campaign leadership domain', () => { createdBy: 999, user: 999, consent: 999, - } as never) + }) expect(parsed.phone).toBe('71999990000') expect(parsed.municipalities).toEqual([1, 2]) @@ -81,7 +81,7 @@ describe('campaign leadership domain', () => { contact: 999, user: 999, createdBy: 999, - } as never) + }) expect(update).toEqual({ id: 1 }) }) @@ -188,7 +188,7 @@ describe('campaign leadership domain', () => { markFirstRead = resolve }) const findSpy = vi.spyOn(payload, 'find').mockImplementation(async (args) => { - const result = await originalFind(args as never) + const result = await originalFind(args) if ( args.collection === 'contact' && 'where' in args && @@ -200,7 +200,7 @@ describe('campaign leadership domain', () => { await firstReadGate } } - return result as never + return result }) const first = createLeadershipRecord(payload, coordinator, { @@ -461,7 +461,7 @@ describe('campaign leadership domain', () => { if (args.collection === 'leadership') { throw new Error('falha forçada após contato') } - return originalCreate(args as never) + return originalCreate(args) }) await expect( diff --git a/tests/int/campaignMigrationReconciliation.int.spec.ts b/tests/int/campaignMigrationReconciliation.int.spec.ts index 7aa94ead..da9aa79c 100644 --- a/tests/int/campaignMigrationReconciliation.int.spec.ts +++ b/tests/int/campaignMigrationReconciliation.int.spec.ts @@ -2,29 +2,28 @@ import { createRequire } from 'node:module' -import { sql } from '@payloadcms/db-postgres' +import { sql, type MigrateDownArgs, type MigrateUpArgs } from '@payloadcms/db-postgres' import { drizzle } from '@payloadcms/db-postgres/drizzle/node-postgres' +import type { Pool as PgPool } from 'pg' import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { migrations } from '@/migrations' +import { stub } from '../helpers/stub' const require = createRequire(import.meta.resolve('@payloadcms/db-postgres')) const { Pool } = require('pg') as { - Pool: new (options: { connectionString: string }) => { - connect: () => Promise<{ query: (statement: string) => Promise; release: () => void }> - end: () => Promise - query: (statement: string) => Promise<{ rows: Array> }> - } + Pool: new (options: { connectionString: string }) => PgPool } const testDatabaseUrl = process.env.DATABASE_URL ?? 'postgresql://teqo:teqo@localhost:5432/teqo_test' const adminUrl = testDatabaseUrl.replace(/\/[^/]+$/, '/postgres') const consolidatedName = '20260718_010733_consolidate_campaign_schema' +type MigrationDatabase = MigrateUpArgs['db'] let databaseSequence = 0 let databaseName = '' let databasePool: InstanceType | undefined -let database: ReturnType | undefined +let database: MigrationDatabase | undefined const quoteIdentifier = (identifier: string): string => { if (!/^[a-z0-9_]+$/.test(identifier)) throw new Error(`Unsafe database identifier: ${identifier}`) @@ -77,10 +76,10 @@ beforeEach(async () => { databasePool = new Pool({ connectionString: testDatabaseUrl.replace(/\/[^/]+$/, `/${databaseName}`), }) - database = drizzle(databasePool as never) + database = drizzle>(databasePool) for (const migration of migrations) { - await migration.up({ db: database } as never) + await migration.up(stub({ db: database })) } }) @@ -139,7 +138,7 @@ describe('campaign migration existing-schema reconciliation', () => { await expect( database.transaction(async (transaction) => { - await consolidated.up({ db: transaction } as never) + await consolidated.up(stub({ db: transaction })) await transaction.execute(sql` INSERT INTO "payload_migrations" ("name", "batch", "updated_at", "created_at") VALUES (${consolidatedName}, 999, now(), now()) @@ -176,7 +175,7 @@ describe('campaign migration existing-schema reconciliation', () => { `) const checksumBefore = await schemaChecksum() - await expect(consolidated.down({ db: database } as never)).rejects.toThrow( + await expect(consolidated.down(stub({ db: database }))).rejects.toThrow( 'Refusing to roll back consolidated campaign schema', ) diff --git a/tests/int/campaignMunicipalityAccess.int.spec.ts b/tests/int/campaignMunicipalityAccess.int.spec.ts index 41dd52f5..e905a0b9 100644 --- a/tests/int/campaignMunicipalityAccess.int.spec.ts +++ b/tests/int/campaignMunicipalityAccess.int.spec.ts @@ -1,6 +1,6 @@ // @vitest-environment node -import type { Payload } from 'payload' +import type { Payload, PayloadRequest } from 'payload' import { getPayload } from 'payload' import { beforeAll, describe, expect, it } from 'vitest' @@ -15,6 +15,7 @@ import config from '@/payload.config' import { getAccessibleMunicipalityIds } from '@/utilities/campaignAccess' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' let payload: Payload const campaignFixtures = installCampaignFixtures({ @@ -117,7 +118,7 @@ describe('municipality catalog seed and per-role access', () => { }) const visible = await getAccessibleMunicipalityIds( - { payload, user: account, context: {} } as never, + stub({ payload, user: account, context: {} }), account, ) expect(visible?.sort((a, b) => a - b)).toEqual( @@ -145,7 +146,7 @@ describe('municipality catalog seed and per-role access', () => { }) const scope = await getAccessibleMunicipalityIds( - { payload, user: account, context: {} } as never, + stub({ payload, user: account, context: {} }), account, ) expect(scope).toEqual([]) @@ -259,7 +260,7 @@ describe('municipality catalog seed and per-role access', () => { ).toEqual([advisor.id]) const scope = await getAccessibleMunicipalityIds( - { payload, user: advisor, context: {} } as never, + stub({ payload, user: advisor, context: {} }), advisor, ) expect(scope).toEqual([municipality.id]) diff --git a/tests/int/campaignMunicipalityUpdate.int.spec.ts b/tests/int/campaignMunicipalityUpdate.int.spec.ts index 7d832c07..7f816c7f 100644 --- a/tests/int/campaignMunicipalityUpdate.int.spec.ts +++ b/tests/int/campaignMunicipalityUpdate.int.spec.ts @@ -1,6 +1,11 @@ // @vitest-environment node -import { getPayload, type Payload } from 'payload' +import { + getPayload, + type Payload, + type PayloadRequest, + type RequiredDataFromCollectionSlug, +} from 'payload' import { beforeAll, describe, expect, it, vi } from 'vitest' import { createMunicipalityUpdateRecord } from '@/app/(campaign)/campanha/actions/municipalityUpdate' @@ -15,6 +20,10 @@ import { } from '../helpers/testDatabaseLease' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' + +/** Create data with server-managed fields (e.g. `author`) intentionally omitted. */ +type MunicipalityUpdateCreateData = RequiredDataFromCollectionSlug<'municipalityUpdate'> let payload: Payload const campaignFixtures = installCampaignFixtures({ @@ -100,7 +109,7 @@ describe('campaign municipality update domain', () => { author: 999, createdAt: '2000-01-01T00:00:00.000Z', updatedAt: '2000-01-01T00:00:00.000Z', - } as never) + }) expect(parsed).toEqual({ municipality: 1, @@ -116,11 +125,11 @@ describe('campaign municipality update domain', () => { await expect( payload.create({ collection: 'municipalityUpdate', - data: { + data: stub({ municipality: municipality.id, kind: 'semanal', worked: 'Somente uma resposta', - } as never, + }), user: coordinator, overrideAccess: false, }), @@ -128,11 +137,11 @@ describe('campaign municipality update domain', () => { await expect( payload.create({ collection: 'municipalityUpdate', - data: { + data: stub({ municipality: municipality.id, kind: 'urgente', body: ' ', - } as never, + }), user: coordinator, overrideAccess: false, }), @@ -146,7 +155,7 @@ describe('campaign municipality update domain', () => { ? await MunicipalityUpdate.access.update({ id: 1, data: undefined, - req: { user: coordinator } as never, + req: stub({ user: coordinator }), }) : MunicipalityUpdate.access?.update const deleteAccess = @@ -154,7 +163,7 @@ describe('campaign municipality update domain', () => { ? await MunicipalityUpdate.access.delete({ id: 1, data: undefined, - req: { user: coordinator } as never, + req: stub({ user: coordinator }), }) : MunicipalityUpdate.access?.delete @@ -170,16 +179,18 @@ describe('campaign municipality update domain', () => { it('lets the coordinator create and read every update', async () => { const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const created = await createMunicipalityUpdateRecord(payload, coordinator, { + // Forged `author` goes through a widened variable: the schema must strip it. + const forgedInput = { municipality: municipality.id, - kind: 'semanal', + kind: 'semanal' as const, worked: 'Visitas concluídas', failed: 'Dois endereços fechados', needs: 'Mais panfletos', activeVolunteers: 7, newSupports: 12, author: 999, - } as never) + } + const created = await createMunicipalityUpdateRecord(payload, coordinator, forgedInput) expect(created.author).toBe(coordinator.id) const visible = await listMunicipalityUpdates(coordinator, municipality.id) @@ -278,11 +289,11 @@ describe('campaign municipality update domain', () => { await expect( payload.create({ collection: 'municipalityUpdate', - data: { + data: stub({ municipality: municipality.id, kind: 'nota', body: 'Tentativa anônima', - } as never, + }), overrideAccess: false, }), ).rejects.toThrow() @@ -537,7 +548,7 @@ describe('campaign municipality update domain', () => { ) { throw new Error('falha forçada no campo derivado') } - return originalUpdate(args as never) as never + return originalUpdate(args) }) try { diff --git a/tests/int/campaignUserPhone.int.spec.ts b/tests/int/campaignUserPhone.int.spec.ts index e30b525a..767b1e64 100644 --- a/tests/int/campaignUserPhone.int.spec.ts +++ b/tests/int/campaignUserPhone.int.spec.ts @@ -1,6 +1,12 @@ // @vitest-environment node -import { getPayload, type Field, type Payload, type PayloadRequest } from 'payload' +import { + getPayload, + type Field, + type FieldAccess, + type Payload, + type PayloadRequest, +} from 'payload' import { beforeAll, describe, expect, it } from 'vitest' import { CampaignUser as CampaignUserCollection } from '@/collections/CampaignUser' @@ -8,6 +14,9 @@ import type { CampaignUser } from '@/payload-types' import config from '@/payload.config' import { withCampaignFixtures, type CampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' + +type FieldAccessArgs = Parameters[0] let payload: Payload @@ -233,14 +242,18 @@ describe('campaign user contact phone', () => { user: coordinator, } as unknown as PayloadRequest - await expect(field.access?.update?.({ id: owner.id, req: ownerReq } as never)).resolves.toBe( - true, + await expect( + field.access?.update?.(stub({ id: owner.id, req: ownerReq })), + ).resolves.toBe(true) + await expect( + field.access?.update?.(stub({ id: owner.id, req: coordinatorReq })), + ).resolves.toBe(true) + await expect(field.access?.create?.(stub({ req: ownerReq }))).resolves.toBe( + false, ) await expect( - field.access?.update?.({ id: owner.id, req: coordinatorReq } as never), + field.access?.create?.(stub({ req: coordinatorReq })), ).resolves.toBe(true) - await expect(field.access?.create?.({ req: ownerReq } as never)).resolves.toBe(false) - await expect(field.access?.create?.({ req: coordinatorReq } as never)).resolves.toBe(true) }) }) }) diff --git a/tests/int/collectionAccessLockdown.int.spec.ts b/tests/int/collectionAccessLockdown.int.spec.ts index 2a49f31f..edd49595 100644 --- a/tests/int/collectionAccessLockdown.int.spec.ts +++ b/tests/int/collectionAccessLockdown.int.spec.ts @@ -1,13 +1,15 @@ // @vitest-environment node -import { getPayload, type Payload } from 'payload' +import { getPayload, type Payload, type RequiredDataFromCollectionSlug } from 'payload' import { beforeAll, describe, expect, it } from 'vitest' import { createLeaderSupporterRecord } from '@/app/(campaign)/campanha/actions/leaderSupporter' import config from '@/payload.config' import { SUPPORTER_REGISTRATION_CONSENT_KEY } from '@/utilities/campaignConsent' +import { relationshipId } from '@/utilities/relationship' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' import { SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, withLeasedConsent, @@ -112,10 +114,11 @@ describe('CMS/PII collection access lockdown', () => { await expect( payload.create({ collection, - data: {}, + // Denied before validation: an empty payload stands in for required fields. + data: stub>({}), user: coordinator, overrideAccess: false, - } as never), + }), ).rejects.toThrow(/permissão|not allowed/i) }, ) @@ -159,8 +162,8 @@ describe('leader supporter creation scope', () => { fixtures.own('supporter', supporter.id) expect(supporter.source).toBe('lideranca') - expect(fixtures.id(supporter.createdBy as never)).toBe(leaderAccount.id) - expect(fixtures.id(supporter.municipality as never)).toBe(linked.id) + expect(relationshipId(supporter.createdBy)).toBe(leaderAccount.id) + expect(relationshipId(supporter.municipality)).toBe(linked.id) }) it('rejects a supporter outside the leadership municipalities', async () => { diff --git a/tests/int/contactPhoneNormalization.int.spec.ts b/tests/int/contactPhoneNormalization.int.spec.ts index 0490474e..1e16f807 100644 --- a/tests/int/contactPhoneNormalization.int.spec.ts +++ b/tests/int/contactPhoneNormalization.int.spec.ts @@ -19,15 +19,19 @@ vi.mock('@/utilities/phone', async (importOriginal) => { import { Contact } from '@/collections/Contact' import { leadershipCreateSchema } from '@/lib/schemas/leadership' +import { stub } from '../helpers/stub' + const contactPhoneHook = Contact.hooks?.beforeValidate?.[0] +type ContactPhoneHookArgs = Parameters>[0] + const runContactPhoneHook = async (phone: string) => { if (typeof contactPhoneHook !== 'function') { throw new Error('Expected Contact.beforeValidate phone hook.') } const data = { phone } - await contactPhoneHook({ data } as never) + await contactPhoneHook(stub({ data })) return data.phone } @@ -54,9 +58,7 @@ describe('Contact phone normalization boundary', () => { }) it('rejects an invalid canonical-length direct API phone without re-normalizing it', async () => { - await expect(runContactPhoneHook('00000000000')).rejects.toThrow( - 'Celular brasileiro inválido.', - ) + await expect(runContactPhoneHook('00000000000')).rejects.toThrow('Celular brasileiro inválido.') expect(phoneMocks.normalizeBrazilianPhone).not.toHaveBeenCalled() }) }) diff --git a/tests/int/onda0Provision.int.spec.ts b/tests/int/onda0Provision.int.spec.ts index e1be7277..d0f66a2f 100644 --- a/tests/int/onda0Provision.int.spec.ts +++ b/tests/int/onda0Provision.int.spec.ts @@ -10,6 +10,12 @@ import { provisionOnda0ConsentAndPrivacy } from '@/utilities/onda0Provision' import { assertTestDatabase } from '../helpers/assertTestDatabase' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { + CAMPAIGN_INVITE_CONSENT_LEASE_KEY, + SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY, + withSharedTestDatabaseLease, +} from '../helpers/testDatabaseLease' let payload: Payload @@ -27,20 +33,29 @@ describe('Onda 0 provision (integration)', () => { }) it('upserts consent keys and publishes privacy-policy idempotently', async () => { - await provisionOnda0ConsentAndPrivacy(payload) - await provisionOnda0ConsentAndPrivacy(payload) + // Shared leases on every stable consent key: parallel spec files exercise + // "consent missing" fail-closed paths by deleting these rows under an + // EXCLUSIVE lease — without the shared leases this test races that window. + await withSharedTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY, () => + withSharedTestDatabaseLease(payload, SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, () => + withSharedTestDatabaseLease(payload, SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY, async () => { + await provisionOnda0ConsentAndPrivacy(payload) + await provisionOnda0ConsentAndPrivacy(payload) - for (const { key } of ONDA0_CONSENT_ENTRIES) { - const descriptor = await requireConsentByKey(payload, key) - expect(descriptor.key).toBe(key) - expect(descriptor.contentHash).toMatch(/^[a-f0-9]{64}$/) - } + for (const { key } of ONDA0_CONSENT_ENTRIES) { + const descriptor = await requireConsentByKey(payload, key) + expect(descriptor.key).toBe(key) + expect(descriptor.contentHash).toMatch(/^[a-f0-9]{64}$/) + } - const privacy = await payload.findGlobal({ - slug: 'privacy-policy', - overrideAccess: true, - }) - expect(privacy.published).toBe(true) - expect(privacy.body).toBeTruthy() + const privacy = await payload.findGlobal({ + slug: 'privacy-policy', + overrideAccess: true, + }) + expect(privacy.published).toBe(true) + expect(privacy.body).toBeTruthy() + }), + ), + ) }) }) diff --git a/tests/int/payloadTransaction.int.spec.ts b/tests/int/payloadTransaction.int.spec.ts index 4d5f5c9b..49ee2504 100644 --- a/tests/int/payloadTransaction.int.spec.ts +++ b/tests/int/payloadTransaction.int.spec.ts @@ -1,9 +1,14 @@ // @vitest-environment node +import type { Payload } from 'payload' import { describe, expect, it, vi } from 'vitest' import { withPayloadTransaction } from '@/utilities/payloadTransaction' +import { stub } from '../helpers/stub' + +type DatabaseSession = NonNullable[string] + const createPayload = ({ transactionID = 17, commitError, @@ -27,15 +32,15 @@ const createPayload = ({ return { database, payload: { - db: { + db: stub({ beginTransaction, commitTransaction, rollbackTransaction, sessions: transactionID === null || !hasSession ? {} - : { [String(transactionID)]: { db: database } }, - }, + : { [String(transactionID)]: stub({ db: database }) }, + }), }, beginTransaction, commitTransaction, @@ -51,7 +56,7 @@ describe('withPayloadTransaction', () => { return { req } }) - const result = await withPayloadTransaction(fixture.payload as never, callback) + const result = await withPayloadTransaction(fixture.payload, callback) expect(result.req).toBe(callback.mock.calls[0]![0].req) expect(fixture.commitTransaction).toHaveBeenCalledWith(17) @@ -63,7 +68,7 @@ describe('withPayloadTransaction', () => { const callback = vi.fn() await expect( - withPayloadTransaction(fixture.payload as never, callback, { + withPayloadTransaction(fixture.payload, callback, { beginFailureMessage: 'falha específica', }), ).rejects.toThrow('falha específica') @@ -78,7 +83,7 @@ describe('withPayloadTransaction', () => { const original = new Error('callback falhou') await expect( - withPayloadTransaction(fixture.payload as never, async () => { + withPayloadTransaction(fixture.payload, async () => { throw original }), ).rejects.toBe(original) @@ -91,9 +96,9 @@ describe('withPayloadTransaction', () => { const original = new Error('commit falhou') const fixture = createPayload({ commitError: original }) - await expect( - withPayloadTransaction(fixture.payload as never, async () => 'result'), - ).rejects.toBe(original) + await expect(withPayloadTransaction(fixture.payload, async () => 'result')).rejects.toBe( + original, + ) expect(fixture.commitTransaction).toHaveBeenCalledWith(17) expect(fixture.rollbackTransaction).toHaveBeenCalledWith(17) @@ -104,7 +109,7 @@ describe('withPayloadTransaction', () => { const rollback = new Error('rollback falhou') const fixture = createPayload({ rollbackError: rollback }) - const failure = await withPayloadTransaction(fixture.payload as never, async () => { + const failure = await withPayloadTransaction(fixture.payload, async () => { throw original }).catch((error: unknown) => error) @@ -118,10 +123,9 @@ describe('withPayloadTransaction', () => { const rollback = new Error('rollback falhou') const fixture = createPayload({ commitError: commit, rollbackError: rollback }) - const failure = await withPayloadTransaction( - fixture.payload as never, - async () => 'result', - ).catch((error: unknown) => error) + const failure = await withPayloadTransaction(fixture.payload, async () => 'result').catch( + (error: unknown) => error, + ) expect(failure).toBeInstanceOf(AggregateError) expect((failure as AggregateError).errors).toEqual([commit, rollback]) diff --git a/tests/int/postgresTransactionLocks.int.spec.ts b/tests/int/postgresTransactionLocks.int.spec.ts index 9a7721a8..c9f23cb9 100644 --- a/tests/int/postgresTransactionLocks.int.spec.ts +++ b/tests/int/postgresTransactionLocks.int.spec.ts @@ -17,6 +17,9 @@ import { getTestTransactionBackendPID, waitForAdvisoryLockWaiter, } from '../helpers/testDatabaseLease' +import { stub } from '../helpers/stub' + +type DatabaseSession = NonNullable[string] let payload: Payload @@ -64,30 +67,28 @@ describe('PostgreSQL transaction advisory locks', () => { 'contact-phone:71999990001', 'contact-phone:71999990002', ]) - expect(CONTACT_PHONE_CONFLICT_MESSAGE).toBe( - 'Já existe outro contato com este celular.', - ) + expect(CONTACT_PHONE_CONFLICT_MESSAGE).toBe('Já existe outro contato com este celular.') }) it('uses only the exact Payload transaction session and fails closed without it', async () => { const sessionDatabase = { execute: vi.fn().mockResolvedValue(undefined) } const unrelatedDatabase = { execute: vi.fn().mockResolvedValue(undefined) } const fixture = { - db: { + db: stub({ name: 'postgres', sessions: { - '41': { db: sessionDatabase }, - '42': { db: unrelatedDatabase }, + '41': stub({ db: sessionDatabase }), + '42': stub({ db: unrelatedDatabase }), }, - }, + }), } await expect( - getPostgresTransactionDatabase(fixture as never, { + getPostgresTransactionDatabase(fixture, { transactionID: Promise.resolve(41), }), ).resolves.toBe(sessionDatabase) - await acquireTextAdvisoryLocks(fixture as never, { transactionID: 41 }, [ + await acquireTextAdvisoryLocks(fixture, { transactionID: 41 }, [ 'domain:2', 'domain:1', 'domain:1', @@ -98,31 +99,28 @@ describe('PostgreSQL transaction advisory locks', () => { expect(collectLockParams(query)).toEqual(['domain:1', 'domain:2']) expect(unrelatedDatabase.execute).not.toHaveBeenCalled() - await expect(getPostgresTransactionDatabase(fixture as never, {})).rejects.toThrow( + await expect(getPostgresTransactionDatabase(fixture, {})).rejects.toThrow( 'A transação PostgreSQL não está disponível.', ) - await expect( - acquireTextAdvisoryLocks(fixture as never, {}, ['domain:1']), - ).rejects.toThrow('A transação PostgreSQL não está disponível.') - await expect( - getPostgresTransactionDatabase(fixture as never, { transactionID: 99 }), - ).rejects.toThrow('A sessão PostgreSQL da transação não está disponível.') + await expect(acquireTextAdvisoryLocks(fixture, {}, ['domain:1'])).rejects.toThrow( + 'A transação PostgreSQL não está disponível.', + ) + await expect(getPostgresTransactionDatabase(fixture, { transactionID: 99 })).rejects.toThrow( + 'A sessão PostgreSQL da transação não está disponível.', + ) }) it('rejects invalid text keys before acquiring any lock', async () => { const database = { execute: vi.fn().mockResolvedValue(undefined) } const fixture = { - db: { + db: stub({ name: 'postgres', - sessions: { '51': { db: database } }, - }, + sessions: { '51': stub({ db: database }) }, + }), } await expect( - acquireTextAdvisoryLocks(fixture as never, { transactionID: 51 }, [ - 'valid:key', - ' ', - ]), + acquireTextAdvisoryLocks(fixture, { transactionID: 51 }, ['valid:key', ' ']), ).rejects.toThrow('A chave do bloqueio PostgreSQL deve ser um texto não vazio.') expect(database.execute).not.toHaveBeenCalled() }) diff --git a/tests/unit/campaignActionContext.unit.spec.ts b/tests/unit/campaignActionContext.unit.spec.ts index f0dae092..13acd690 100644 --- a/tests/unit/campaignActionContext.unit.spec.ts +++ b/tests/unit/campaignActionContext.unit.spec.ts @@ -16,9 +16,7 @@ describe('campaign action context', () => { const findByID = vi.fn().mockResolvedValue(currentActor) const req = { transactionID: 21 } - await expect(reloadCampaignActor({ findByID } as never, staleActor, req)).resolves.toBe( - currentActor, - ) + await expect(reloadCampaignActor({ findByID }, staleActor, req)).resolves.toBe(currentActor) expect(findByID).toHaveBeenCalledWith({ collection: 'campaignUser', id: staleActor.id, diff --git a/tests/unit/campaignUserProfile.unit.spec.ts b/tests/unit/campaignUserProfile.unit.spec.ts index e8651fb2..30604fa4 100644 --- a/tests/unit/campaignUserProfile.unit.spec.ts +++ b/tests/unit/campaignUserProfile.unit.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' +import type { Media } from '@/payload-types' import { assertCampaignAvatarFile, campaignUserInitials, @@ -9,6 +10,8 @@ import { mediaDocumentUrl, } from '@/utilities/campaignUserProfile' +import { stub } from '../helpers/stub' + describe('campaignUserProfile helpers', () => { it('builds initials from the user name', () => { expect(campaignUserInitials('Maria Silva')).toBe('MS') @@ -20,7 +23,7 @@ describe('campaignUserProfile helpers', () => { campaignUserShellView({ name: 'Maria', role: 'coordinator', - avatar: { id: 1, url: 'https://example.com/a.jpg' } as never, + avatar: stub({ id: 1, url: 'https://example.com/a.jpg' }), }), ).toEqual({ name: 'Maria', diff --git a/tests/unit/federalCandidateTotalsAggregate.unit.spec.ts b/tests/unit/federalCandidateTotalsAggregate.unit.spec.ts deleted file mode 100644 index 374f353e..00000000 --- a/tests/unit/federalCandidateTotalsAggregate.unit.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -// @vitest-environment node - -import type { Payload } from 'payload' -import { describe, expect, it, vi } from 'vitest' - -import type { CampaignUser, User } from '@/payload-types' -import { assertCanReadElectionData } from '@/utilities/campaignAccess' -import * as drizzleBulk from '@/utilities/drizzleBulk' -import { loadFederalCandidateTotalsAggregated } from '@/utilities/federalCandidateTotalsAggregate' -import type { MunicipalityElectionGeography } from '@/utilities/municipalityElectionGeography' - -describe('assertCanReadElectionData', () => { - it('allows payload admins and staff campaign users to read election data', () => { - expect(() => - assertCanReadElectionData({ - collection: 'users', - id: 1, - } as User), - ).not.toThrow() - - expect(() => - assertCanReadElectionData({ - collection: 'campaignUser', - id: 1, - role: 'advisor', - } as unknown as CampaignUser), - ).not.toThrow() - - expect(() => - assertCanReadElectionData({ - collection: 'campaignUser', - id: 1, - role: 'leader', - } as unknown as CampaignUser), - ).toThrow('Leitura de dados eleitorais negada.') - }) - - it('denies readers outside campaign/admin collections', () => { - expect(() => - assertCanReadElectionData({ - collection: 'contact', - id: 1, - } as never), - ).toThrow('Leitura de dados eleitorais negada.') - }) -}) - -describe('loadFederalCandidateTotalsAggregated', () => { - it('maps drizzle aggregate rows and scopes geography in SQL', async () => { - const geography: MunicipalityElectionGeography = { - cityCode: '38490', - zones: [1, 2], - } - - const execute = vi.fn().mockResolvedValue({ - rows: [ - { - candidate_number: 1313, - candidate_name: 'JORGE SOLLA', - party: 'PT', - votes: 2100, - }, - { - candidate_number: 1234, - candidate_name: 'OUTRO', - party: 'PL', - votes: 900, - }, - ], - }) - vi.spyOn(drizzleBulk, 'requirePostgresDrizzle').mockReturnValue({ execute }) - - const payload = { db: { name: 'postgres' } } as Pick - const totals = await loadFederalCandidateTotalsAggregated( - payload, - { - collection: 'campaignUser', - id: 1, - role: 'coordinator', - } as unknown as CampaignUser, - geography, - ) - - expect(totals).toEqual([ - { candidateNumber: 1313, name: 'JORGE SOLLA', party: 'PT', votes: 2100 }, - { candidateNumber: 1234, name: 'OUTRO', party: 'PL', votes: 900 }, - ]) - expect(execute).toHaveBeenCalledOnce() - const sqlPayload = JSON.stringify(execute.mock.calls[0]?.[0]) - expect(sqlPayload).toContain('city_code') - expect(sqlPayload).not.toContain('city_name') - vi.restoreAllMocks() - }) -}) diff --git a/tsconfig.json b/tsconfig.json index 0c68c372..c0f4b655 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], + "lib": [ + "DOM", + "DOM.Iterable", + "ES2022" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -18,204 +22,24 @@ } ], "paths": { - "@/*": ["./src/*"], - "@payload-config": ["./src/payload.config.ts"] + "@/*": [ + "./src/*" + ], + "@payload-config": [ + "./src/payload.config.ts" + ] }, - "target": "ES2022" + "target": "ES2022", + "noUnusedLocals": true }, "include": [ "**/*.ts", "**/*.tsx", - ".next/build-composition/types/**/*.ts", - ".next/build-vote-estimate-version-final/types/**/*.ts", - ".next/build-vote-estimate-version/types/**/*.ts", - ".next/dev/types/**/*.ts", - ".next/e2e-10044/types/**/*.ts", - ".next/e2e-1006/types/**/*.ts", - ".next/e2e-1085/types/**/*.ts", - ".next/e2e-11201/types/**/*.ts", - ".next/e2e-11506/types/**/*.ts", - ".next/e2e-11663/types/**/*.ts", - ".next/e2e-12280/types/**/*.ts", - ".next/e2e-13120/types/**/*.ts", - ".next/e2e-13521/types/**/*.ts", - ".next/e2e-14136/types/**/*.ts", - ".next/e2e-14928/types/**/*.ts", - ".next/e2e-15205/types/**/*.ts", - ".next/e2e-1536/types/**/*.ts", - ".next/e2e-15855/types/**/*.ts", - ".next/e2e-15927/types/**/*.ts", - ".next/e2e-16483/types/**/*.ts", - ".next/e2e-16874/types/**/*.ts", - ".next/e2e-17058/types/**/*.ts", - ".next/e2e-17359/types/**/*.ts", - ".next/e2e-1764/types/**/*.ts", - ".next/e2e-18285/types/**/*.ts", - ".next/e2e-18508/types/**/*.ts", - ".next/e2e-18580/types/**/*.ts", - ".next/e2e-18785/types/**/*.ts", - ".next/e2e-19365/types/**/*.ts", - ".next/e2e-19899/types/**/*.ts", - ".next/e2e-20552/types/**/*.ts", - ".next/e2e-21365/types/**/*.ts", - ".next/e2e-21464/types/**/*.ts", - ".next/e2e-21632/types/**/*.ts", - ".next/e2e-2180/types/**/*.ts", - ".next/e2e-22000/types/**/*.ts", - ".next/e2e-22628/types/**/*.ts", - ".next/e2e-22922/types/**/*.ts", - ".next/e2e-23174/types/**/*.ts", - ".next/e2e-23318/types/**/*.ts", - ".next/e2e-23628/types/**/*.ts", - ".next/e2e-2427/types/**/*.ts", - ".next/e2e-24574/types/**/*.ts", - ".next/e2e-25365/types/**/*.ts", - ".next/e2e-25431/types/**/*.ts", - ".next/e2e-25587/types/**/*.ts", - ".next/e2e-27635/types/**/*.ts", - ".next/e2e-2815/types/**/*.ts", - ".next/e2e-28280/types/**/*.ts", - ".next/e2e-28367/types/**/*.ts", - ".next/e2e-28783/types/**/*.ts", - ".next/e2e-28804/types/**/*.ts", - ".next/e2e-29103/types/**/*.ts", - ".next/e2e-30214/types/**/*.ts", - ".next/e2e-303/types/**/*.ts", - ".next/e2e-30397/types/**/*.ts", - ".next/e2e-30528/types/**/*.ts", - ".next/e2e-31313/types/**/*.ts", - ".next/e2e-31599/types/**/*.ts", - ".next/e2e-32003/types/**/*.ts", - ".next/e2e-3335/types/**/*.ts", - ".next/e2e-33840/types/**/*.ts", - ".next/e2e-34312/types/**/*.ts", - ".next/e2e-36806/types/**/*.ts", - ".next/e2e-36946/types/**/*.ts", - ".next/e2e-37023/types/**/*.ts", - ".next/e2e-38948/types/**/*.ts", - ".next/e2e-39539/types/**/*.ts", - ".next/e2e-39565/types/**/*.ts", - ".next/e2e-40345/types/**/*.ts", - ".next/e2e-41065/types/**/*.ts", - ".next/e2e-41648/types/**/*.ts", - ".next/e2e-42183/types/**/*.ts", - ".next/e2e-43276/types/**/*.ts", - ".next/e2e-43384/types/**/*.ts", - ".next/e2e-44522/types/**/*.ts", - ".next/e2e-44774/types/**/*.ts", - ".next/e2e-44960/types/**/*.ts", - ".next/e2e-46155/types/**/*.ts", - ".next/e2e-46497/types/**/*.ts", - ".next/e2e-46609/types/**/*.ts", - ".next/e2e-46757/types/**/*.ts", - ".next/e2e-46875/types/**/*.ts", - ".next/e2e-47405/types/**/*.ts", - ".next/e2e-47939/types/**/*.ts", - ".next/e2e-48016/types/**/*.ts", - ".next/e2e-48479/types/**/*.ts", - ".next/e2e-49401/types/**/*.ts", - ".next/e2e-51002/types/**/*.ts", - ".next/e2e-52013/types/**/*.ts", - ".next/e2e-52421/types/**/*.ts", - ".next/e2e-52695/types/**/*.ts", - ".next/e2e-53562/types/**/*.ts", - ".next/e2e-55001/types/**/*.ts", - ".next/e2e-55800/types/**/*.ts", - ".next/e2e-55954/types/**/*.ts", - ".next/e2e-56773/types/**/*.ts", - ".next/e2e-56925/types/**/*.ts", - ".next/e2e-57962/types/**/*.ts", - ".next/e2e-58103/types/**/*.ts", - ".next/e2e-59966/types/**/*.ts", - ".next/e2e-60290/types/**/*.ts", - ".next/e2e-60609/types/**/*.ts", - ".next/e2e-60813/types/**/*.ts", - ".next/e2e-61668/types/**/*.ts", - ".next/e2e-62163/types/**/*.ts", - ".next/e2e-63096/types/**/*.ts", - ".next/e2e-63375/types/**/*.ts", - ".next/e2e-64189/types/**/*.ts", - ".next/e2e-64863/types/**/*.ts", - ".next/e2e-6491/types/**/*.ts", - ".next/e2e-64996/types/**/*.ts", - ".next/e2e-66448/types/**/*.ts", - ".next/e2e-67280/types/**/*.ts", - ".next/e2e-677/types/**/*.ts", - ".next/e2e-67863/types/**/*.ts", - ".next/e2e-6790/types/**/*.ts", - ".next/e2e-68693/types/**/*.ts", - ".next/e2e-69683/types/**/*.ts", - ".next/e2e-70258/types/**/*.ts", - ".next/e2e-70974/types/**/*.ts", - ".next/e2e-72843/types/**/*.ts", - ".next/e2e-73012/types/**/*.ts", - ".next/e2e-7321/types/**/*.ts", - ".next/e2e-74412/types/**/*.ts", - ".next/e2e-75948/types/**/*.ts", - ".next/e2e-76503/types/**/*.ts", - ".next/e2e-76880/types/**/*.ts", - ".next/e2e-78072/types/**/*.ts", - ".next/e2e-78180/types/**/*.ts", - ".next/e2e-78226/types/**/*.ts", - ".next/e2e-78655/types/**/*.ts", - ".next/e2e-79761/types/**/*.ts", - ".next/e2e-8028/types/**/*.ts", - ".next/e2e-80284/types/**/*.ts", - ".next/e2e-80317/types/**/*.ts", - ".next/e2e-81160/types/**/*.ts", - ".next/e2e-81409/types/**/*.ts", - ".next/e2e-8147/types/**/*.ts", - ".next/e2e-81635/types/**/*.ts", - ".next/e2e-81786/types/**/*.ts", - ".next/e2e-81905/types/**/*.ts", - ".next/e2e-83151/types/**/*.ts", - ".next/e2e-83738/types/**/*.ts", - ".next/e2e-84154/types/**/*.ts", - ".next/e2e-84504/types/**/*.ts", - ".next/e2e-84574/types/**/*.ts", - ".next/e2e-85918/types/**/*.ts", - ".next/e2e-8611/types/**/*.ts", - ".next/e2e-86120/types/**/*.ts", - ".next/e2e-86502/types/**/*.ts", - ".next/e2e-87073/types/**/*.ts", - ".next/e2e-87586/types/**/*.ts", - ".next/e2e-87665/types/**/*.ts", - ".next/e2e-88316/types/**/*.ts", - ".next/e2e-88470/types/**/*.ts", - ".next/e2e-89042/types/**/*.ts", - ".next/e2e-89097/types/**/*.ts", - ".next/e2e-89236/types/**/*.ts", - ".next/e2e-89710/types/**/*.ts", - ".next/e2e-90349/types/**/*.ts", - ".next/e2e-90697/types/**/*.ts", - ".next/e2e-91110/types/**/*.ts", - ".next/e2e-91126/types/**/*.ts", - ".next/e2e-92653/types/**/*.ts", - ".next/e2e-92722/types/**/*.ts", - ".next/e2e-93003/types/**/*.ts", - ".next/e2e-93255/types/**/*.ts", - ".next/e2e-93729/types/**/*.ts", - ".next/e2e-9379/types/**/*.ts", - ".next/e2e-94851/types/**/*.ts", - ".next/e2e-94897/types/**/*.ts", - ".next/e2e-95028/types/**/*.ts", - ".next/e2e-95103/types/**/*.ts", - ".next/e2e-95457/types/**/*.ts", - ".next/e2e-96105/types/**/*.ts", - ".next/e2e-96857/types/**/*.ts", - ".next/e2e-96909/types/**/*.ts", - ".next/e2e-97296/types/**/*.ts", - ".next/e2e-97674/types/**/*.ts", - ".next/e2e-9850/types/**/*.ts", - ".next/e2e-99093/types/**/*.ts", - ".next/e2e-99443/types/**/*.ts", - ".next/e2e-99541/types/**/*.ts", - ".next/e2e-9955/types/**/*.ts", - ".next/e2e-99590/types/**/*.ts", - ".next/e2e/types/**/*.ts", - ".next/types/**/*.ts", "next-env.d.ts", - ".next/e2e-12203/types/**/*.ts" + ".next/types/**/*.ts", + ".next/*/types/**/*.ts" ], - "exclude": ["node_modules"] + "exclude": [ + "node_modules" + ] }
( - importModule: () => Promise>>, - exportName: string, - skeletonHeightClassName: string, -) => - dynamic(() => importModule().then((module) => module[exportName]), { - ssr: false, - loading: () => ( - - ), - }) diff --git a/src/components/campaign/LazyDialogModuleFallback.tsx b/src/components/campaign/LazyDialogModuleFallback.tsx deleted file mode 100644 index d9f0c468..00000000 --- a/src/components/campaign/LazyDialogModuleFallback.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client' - -import { AlertCircleIcon } from 'lucide-react' - -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert' -import { Button } from '@/components/ui/button' -import { - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import { Spinner } from '@/components/ui/Spinner' - -export const LazyDialogModuleFallback = ({ - description, - loadingLabel, - onRetry, - status, - title, -}: { - description: string - loadingLabel: string - onRetry: () => void - status: 'idle' | 'loading' | 'error' - title: string -}) => ( - - - {title} - {description} - - {status === 'error' ? ( - <> - - - Conteúdo indisponível - Não foi possível carregar este conteúdo. - - - - - Fechar - - - - Tentar novamente - - - > - ) : ( - - - - )} - -) diff --git a/src/components/campaign/MunicipalityList.tsx b/src/components/campaign/MunicipalityList.tsx index f5dd03eb..6afa5271 100644 --- a/src/components/campaign/MunicipalityList.tsx +++ b/src/components/campaign/MunicipalityList.tsx @@ -156,7 +156,6 @@ export const MunicipalityList = ({ {municipalities.map((municipality) => { - const names = advisorNames(municipality) const hasAdvisor = municipality.advisorIDs.length > 0 return ( diff --git a/src/components/campaign/RecentVisitTracker.tsx b/src/components/campaign/RecentVisitTracker.tsx index 75df52c5..8dfc7b61 100644 --- a/src/components/campaign/RecentVisitTracker.tsx +++ b/src/components/campaign/RecentVisitTracker.tsx @@ -13,15 +13,19 @@ export const RecentVisitTracker = ({ }: { entry: Omit }) => { + // Primitive dependencies: the effect must re-run on visit identity changes, + // not on parent re-renders handing down a fresh `entry` object. + const { href, label, kind } = entry + useEffect(() => { const timer = window.setTimeout(() => { - recordRecentVisit({ ...entry, visitedAt: Date.now() }) + recordRecentVisit({ href, label, kind, visitedAt: Date.now() }) }, RECORD_DWELL_MS) return () => { window.clearTimeout(timer) } - }, [entry.href, entry.label, entry.kind]) + }, [href, label, kind]) return null } diff --git a/src/components/campaign/RecentlyVisited.tsx b/src/components/campaign/RecentlyVisited.tsx deleted file mode 100644 index 4a7dd286..00000000 --- a/src/components/campaign/RecentlyVisited.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client' - -import { useCallback, useState } from 'react' - -import { RecentlyVisitedCard } from '@/components/campaign/RecentlyVisitedCard' -import { UpcomingActionPlansCard } from '@/components/campaign/UpcomingActionPlansCard' -import { cn } from '@/lib/utils' -import type { ActionPlanUpcomingPreviewRecord } from '@/utilities/actionPlanUpcomingPreview' -import { listRecentVisits } from '@/utilities/recentVisits' - -type RecentlyVisitedProps = { - now: Date - upcomingPlans: ActionPlanUpcomingPreviewRecord[] -} - -export const RecentlyVisited = ({ now, upcomingPlans }: RecentlyVisitedProps) => { - const [hasRecent, setHasRecent] = useState(() => listRecentVisits().length > 0) - const handleVisitCountChange = useCallback((count: number) => { - setHasRecent(count > 0) - }, []) - - return ( - - - - - ) -} diff --git a/src/components/campaign/UpcomingActionPlansCard.tsx b/src/components/campaign/UpcomingActionPlansCard.tsx deleted file mode 100644 index 66872062..00000000 --- a/src/components/campaign/UpcomingActionPlansCard.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { ArrowRightIcon } from 'lucide-react' -import Link from 'next/link' - -import { Badge } from '@/components/ui/Badge' -import { Button } from '@/components/ui/button' -import { Card, CardAction, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { actionPlanKindLabels } from '@/lib/schemas/actionPlan' -import { cn } from '@/lib/utils' -import type { ActionPlanUpcomingPreviewRecord } from '@/utilities/actionPlanUpcomingPreview' -import { formatBahiaDateTimeLabel } from '@/utilities/campaignTime' - -export const UpcomingActionPlansCard = ({ - plans, - className, -}: { - plans: ActionPlanUpcomingPreviewRecord[] - className?: string -}) => ( - - - Próximos eventos - - - - Ver todos - - - - - - - {plans.length ? ( - - {plans.map((plan) => ( - - - {actionPlanKindLabels[plan.kind]} - - {formatBahiaDateTimeLabel(plan.startAt)} - {plan.municipalityName ? ` · ${plan.municipalityName}` : ''} - - - - {plan.title} - - - ))} - - ) : ( - Nenhum evento agendado - )} - - -) diff --git a/src/components/campaign/useLazyDialogModule.ts b/src/components/campaign/useLazyDialogModule.ts deleted file mode 100644 index bcb23e03..00000000 --- a/src/components/campaign/useLazyDialogModule.ts +++ /dev/null @@ -1,67 +0,0 @@ -'use client' - -import { useCallback, useEffect, useRef, useState } from 'react' - -type LazyDialogModule = { default: T } - -type LazyDialogModuleState = - | { status: 'idle'; component: null } - | { status: 'loading'; component: null } - | { status: 'ready'; component: T } - | { status: 'error'; component: null } - -export const useLazyDialogModule = ( - loadModule: () => Promise>, -) => { - const [state, setState] = useState>({ - status: 'idle', - component: null, - }) - const statusRef = useRef['status']>('idle') - const attemptRef = useRef(0) - const mountedRef = useRef(true) - - useEffect(() => { - mountedRef.current = true - return () => { - mountedRef.current = false - attemptRef.current += 1 - } - }, []) - - const updateState = useCallback((nextState: LazyDialogModuleState) => { - statusRef.current = nextState.status - setState(nextState) - }, []) - - const load = useCallback(() => { - if (statusRef.current === 'loading' || statusRef.current === 'ready') return - - const attempt = ++attemptRef.current - updateState({ status: 'loading', component: null }) - - void Promise.resolve() - .then(loadModule) - .then(({ default: component }) => { - if (!mountedRef.current || attempt !== attemptRef.current) return - updateState({ status: 'ready', component }) - }) - .catch(() => { - if (!mountedRef.current || attempt !== attemptRef.current) return - updateState({ status: 'error', component: null }) - }) - }, [loadModule, updateState]) - - const resetAfterClose = useCallback(() => { - if (statusRef.current !== 'error') return - attemptRef.current += 1 - updateState({ status: 'idle', component: null }) - }, [updateState]) - - return { - component: state.component, - load, - resetAfterClose, - status: state.status, - } -} diff --git a/src/components/ui/DropdownMenu.tsx b/src/components/ui/DropdownMenu.tsx deleted file mode 100644 index 1d97dc0b..00000000 --- a/src/components/ui/DropdownMenu.tsx +++ /dev/null @@ -1,234 +0,0 @@ -'use client' - -import * as React from 'react' -import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui' - -import { cn } from '@/lib/utils' -import { CheckIcon, ChevronRightIcon } from 'lucide-react' - -const DropdownMenu = ({ ...props }: React.ComponentProps) => ( - -) - -const DropdownMenuPortal = ({ - ...props -}: React.ComponentProps) => ( - -) - -const DropdownMenuTrigger = ({ - ...props -}: React.ComponentProps) => ( - -) - -const DropdownMenuContent = ({ - className, - align = 'start', - sideOffset = 4, - ...props -}: React.ComponentProps) => ( - - - -) - -const DropdownMenuGroup = ({ - ...props -}: React.ComponentProps) => ( - -) - -const DropdownMenuItem = ({ - className, - inset, - variant = 'default', - ...props -}: React.ComponentProps & { - inset?: boolean - variant?: 'default' | 'destructive' -}) => ( - -) - -const DropdownMenuCheckboxItem = ({ - className, - children, - checked, - inset, - ...props -}: React.ComponentProps & { - inset?: boolean -}) => ( - - - - - - - {children} - -) - -const DropdownMenuRadioGroup = ({ - ...props -}: React.ComponentProps) => ( - -) - -const DropdownMenuRadioItem = ({ - className, - children, - inset, - ...props -}: React.ComponentProps & { - inset?: boolean -}) => ( - - - - - - - {children} - -) - -const DropdownMenuLabel = ({ - className, - inset, - ...props -}: React.ComponentProps & { - inset?: boolean -}) => ( - -) - -const DropdownMenuSeparator = ({ - className, - ...props -}: React.ComponentProps) => ( - -) - -const DropdownMenuShortcut = ({ className, ...props }: React.ComponentProps<'span'>) => ( - -) - -const DropdownMenuSub = ({ ...props }: React.ComponentProps) => ( - -) - -const DropdownMenuSubTrigger = ({ - className, - inset, - children, - ...props -}: React.ComponentProps & { - inset?: boolean -}) => ( - - {children} - - -) - -const DropdownMenuSubContent = ({ - className, - ...props -}: React.ComponentProps) => ( - -) - -export { - DropdownMenu, - DropdownMenuPortal, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuLabel, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubTrigger, - DropdownMenuSubContent, -} diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx deleted file mode 100644 index bceb5bea..00000000 --- a/src/components/ui/Tabs.tsx +++ /dev/null @@ -1,78 +0,0 @@ -'use client' - -import * as React from 'react' -import { cva, type VariantProps } from 'class-variance-authority' -import { Tabs as TabsPrimitive } from 'radix-ui' - -import { cn } from '@/lib/utils' - -const Tabs = ({ - className, - orientation = 'horizontal', - ...props -}: React.ComponentProps) => ( - -) - -const tabsListVariants = cva( - 'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:max-w-full group-data-horizontal/tabs:justify-start group-data-horizontal/tabs:overflow-x-auto group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none', - { - variants: { - variant: { - default: 'bg-muted', - line: 'gap-1 bg-transparent', - }, - }, - defaultVariants: { - variant: 'default', - }, - }, -) - -const TabsList = ({ - className, - variant = 'default', - ...props -}: React.ComponentProps & VariantProps) => ( - -) - -const TabsTrigger = ({ - className, - ...props -}: React.ComponentProps) => ( - -) - -const TabsContent = ({ - className, - ...props -}: React.ComponentProps) => ( - -) - -export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx deleted file mode 100644 index 9542be91..00000000 --- a/src/components/ui/select.tsx +++ /dev/null @@ -1,185 +0,0 @@ -'use client' - -import * as React from 'react' -import { Select as SelectPrimitive } from 'radix-ui' - -import { cn } from '@/lib/utils' -import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from 'lucide-react' - -function Select({ ...props }: React.ComponentProps) { - return -} - -function SelectGroup({ className, ...props }: React.ComponentProps) { - return ( - - ) -} - -function SelectValue({ ...props }: React.ComponentProps) { - return -} - -function SelectTrigger({ - className, - size = 'default', - children, - ...props -}: React.ComponentProps & { - size?: 'sm' | 'default' -}) { - return ( - - {children} - - - - - ) -} - -function SelectContent({ - className, - children, - position = 'item-aligned', - align = 'center', - ...props -}: React.ComponentProps) { - return ( - - - - - {children} - - - - - ) -} - -function SelectLabel({ className, ...props }: React.ComponentProps) { - return ( - - ) -} - -function SelectItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ) -} - -function SelectSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function SelectScrollUpButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ) -} - -function SelectScrollDownButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ) -} - -export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -} diff --git a/src/utilities/actionPlanUpcomingPreview.ts b/src/utilities/actionPlanUpcomingPreview.ts deleted file mode 100644 index 84807492..00000000 --- a/src/utilities/actionPlanUpcomingPreview.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Payload, Where } from 'payload' - -import type { ActionPlan, CampaignUser } from '@/payload-types' -import { relationshipId } from '@/utilities/relationship' - -export const actionPlanUpcomingPreviewLimit = 3 - -const upcomingActionPlanSelect = { - title: true, - slug: true, - kind: true, - startAt: true, - municipality: true, -} as const - -export type ActionPlanUpcomingPreviewRecord = { - id: number - slug: string - title: string - kind: ActionPlan['kind'] - startAt: string - municipalityName: string | null -} - -export type ActionPlanUpcomingPreviewFilters = { - municipality?: number -} - -const loadMunicipalityNamesById = async ( - payload: Pick, - municipalityIds: number[], -): Promise> => { - if (municipalityIds.length === 0) return new Map() - - // Display-name lookup: row access on the plans already gated visibility. - const result = await payload.find({ - collection: 'municipality', - where: { id: { in: municipalityIds } }, - depth: 0, - pagination: false, - select: { name: true }, - overrideAccess: true, - }) - - return new Map(result.docs.map((municipality) => [municipality.id, municipality.name])) -} - -export const loadUpcomingActionPlansPreview = async ( - payload: Pick, - user: CampaignUser, - now: Date, - options: { - limit?: number - filters?: ActionPlanUpcomingPreviewFilters - } = {}, -): Promise => { - const limit = options.limit ?? actionPlanUpcomingPreviewLimit - const filters: Where[] = [ - { status: { in: ['planejado', 'confirmado'] } }, - { startAt: { greater_than_equal: now.toISOString() } }, - ] - if (options.filters?.municipality) { - filters.push({ municipality: { equals: options.filters.municipality } }) - } - - const result = await payload.find({ - collection: 'actionPlan', - where: { and: filters }, - depth: 0, - limit, - page: 1, - sort: 'startAt', - select: upcomingActionPlanSelect, - user, - overrideAccess: false, - }) - - const municipalityIds = [ - ...new Set( - result.docs - .map((plan) => relationshipId(plan.municipality)) - .filter((id): id is number => id !== null), - ), - ] - const municipalityNamesById = await loadMunicipalityNamesById(payload, municipalityIds) - - return result.docs.map((plan) => { - const municipalityId = relationshipId(plan.municipality) - return { - id: plan.id, - slug: plan.slug, - title: plan.title, - kind: plan.kind, - startAt: plan.startAt as string, - municipalityName: municipalityId ? (municipalityNamesById.get(municipalityId) ?? null) : null, - } - }) -} diff --git a/src/utilities/electionResultsImport.ts b/src/utilities/electionResultsImport.ts index ea7a6026..af2b93c3 100644 --- a/src/utilities/electionResultsImport.ts +++ b/src/utilities/electionResultsImport.ts @@ -1,4 +1,4 @@ -import { and, eq } from '@payloadcms/db-postgres/drizzle' +import { and, eq, type AnyColumn } from '@payloadcms/db-postgres/drizzle' import type { Payload } from 'payload' import type { @@ -57,11 +57,21 @@ type PayloadDbDrizzle = { } } +/** Payload's generated Drizzle tables type columns as `unknown` — narrow with a runtime check. */ +const scopeColumn = ( + table: Record, + name: 'year' | 'office' | 'turn', +): AnyColumn => { + const column = table[name] + if (column == null) throw new Error(`Election table is missing the "${name}" column.`) + return column as AnyColumn +} + const scopeWhere = (table: Record, scope: ElectionImportScope) => and( - eq(table.year as never, scope.year), - eq(table.office as never, scope.office), - eq(table.turn as never, scope.turn), + eq(scopeColumn(table, 'year'), scope.year), + eq(scopeColumn(table, 'office'), scope.office), + eq(scopeColumn(table, 'turn'), scope.turn), ) const deleteScope = async ( diff --git a/src/utilities/federalCandidateTotalsAggregate.ts b/src/utilities/federalCandidateTotalsAggregate.ts deleted file mode 100644 index 30f3877a..00000000 --- a/src/utilities/federalCandidateTotalsAggregate.ts +++ /dev/null @@ -1,58 +0,0 @@ -import 'server-only' - -import { sql } from '@payloadcms/db-postgres' -import type { Payload } from 'payload' - -import { BASELINE_TICKET_2022, ELECTION_YEAR_2022 } from '@/lib/electionResults' -import { assertCanReadElectionData, type ElectionDataReader } from '@/utilities/campaignAccess' -import { drizzleResultRows, requirePostgresDrizzle } from '@/utilities/drizzleBulk' -import { type MunicipalityElectionGeography, zonesByCityCode } from '@/utilities/municipalityElectionGeography' - -export type FederalCandidateTotal = { - candidateNumber: number - name: string - party: string - votes: number -} - -export const loadFederalCandidateTotalsAggregated = async ( - payload: Pick, - user: ElectionDataReader, - geography: MunicipalityElectionGeography, -): Promise => { - assertCanReadElectionData(user) - - const cityClauses = [...zonesByCityCode(geography).entries()].map( - ([cityCode, zones]) => - sql`("election_candidate_vote"."city_code" = ${cityCode} AND "election_candidate_vote"."zone_number" IN (${sql.join( - zones.map((zone) => sql`${zone}`), - sql`, `, - )}))`, - ) - - const drizzle = requirePostgresDrizzle(payload, 'agregação federal') - const result = await drizzle.execute(sql` - SELECT - "election_candidate_vote"."candidate_number" AS "candidate_number", - MAX("election_candidate_vote"."candidate_name") AS "candidate_name", - COALESCE(MAX("election_candidate_vote"."party"), '') AS "party", - SUM("election_candidate_vote"."votes")::int AS "votes" - FROM "election_candidate_vote" - WHERE - "election_candidate_vote"."year" = ${ELECTION_YEAR_2022} - AND "election_candidate_vote"."state" = 'BA' - AND "election_candidate_vote"."office" = ${BASELINE_TICKET_2022.candidate.office} - AND "election_candidate_vote"."turn" = '1' - AND "election_candidate_vote"."vote_type" = 'nominal' - AND (${sql.join(cityClauses, sql` OR `)}) - GROUP BY "election_candidate_vote"."candidate_number" - ORDER BY "votes" DESC, "election_candidate_vote"."candidate_number" ASC - `) - - return drizzleResultRows(result).map((row) => ({ - candidateNumber: Number(row.candidate_number), - name: String(row.candidate_name ?? ''), - party: String(row.party), - votes: Number(row.votes ?? 0), - })) -} diff --git a/src/utilities/hookFilledData.ts b/src/utilities/hookFilledData.ts new file mode 100644 index 00000000..f5fc5f00 --- /dev/null +++ b/src/utilities/hookFilledData.ts @@ -0,0 +1,13 @@ +import type { CollectionSlug, RequiredDataFromCollectionSlug } from 'payload' + +/** + * Types zod-validated create input as a collection's full create data when the + * remaining required fields (canonical slug, authorship, workflow status) are + * filled by the collection's own beforeValidate/beforeChange hooks. + * + * Unlike an `as never` cast, property-level type checking is preserved for + * every field the caller does provide — only completeness is waived. + */ +export const hookFilledCreateData = ( + data: Partial>, +): RequiredDataFromCollectionSlug => data as RequiredDataFromCollectionSlug diff --git a/src/utilities/tseZone.ts b/src/utilities/tseZone.ts deleted file mode 100644 index a5234a13..00000000 --- a/src/utilities/tseZone.ts +++ /dev/null @@ -1,25 +0,0 @@ -export class TseZoneParseError extends Error {} - -export const sortedUniqueZoneNumbers = (values: readonly number[]): number[] => - [...new Set(values)].sort((left, right) => left - right) - -export const parseTseZoneNumbers = (value: string): number[] => { - if (!/^[\d,\s]*$/.test(value)) { - throw new TseZoneParseError('Use apenas números, vírgulas e espaços nas Zonas TSE.') - } - - return sortedUniqueZoneNumbers( - value - .split(/[,\s]+/) - .filter(Boolean) - .map((token) => { - const zoneNumber = Number(token) - if (!Number.isInteger(zoneNumber) || zoneNumber < 1 || zoneNumber > 999) { - throw new TseZoneParseError( - `Zona TSE inválida: "${token}". Use números de 1 a 999.`, - ) - } - return zoneNumber - }), - ) -} diff --git a/tests/e2e/fixtures/campaignE2EFixtures.ts b/tests/e2e/fixtures/campaignE2EFixtures.ts index 0bb26856..ef392726 100644 --- a/tests/e2e/fixtures/campaignE2EFixtures.ts +++ b/tests/e2e/fixtures/campaignE2EFixtures.ts @@ -60,7 +60,7 @@ class CampaignE2EOwnership { get: (_target, property) => { if (property === 'create') { return async (args: Parameters[0]) => { - const document = await rootPayload.create(args as never) + const document = await rootPayload.create(args) if (this.isOwnedCollection(args.collection) && typeof document.id === 'number') { this.own(args.collection, document.id) } diff --git a/tests/helpers/campaignFixtures.ts b/tests/helpers/campaignFixtures.ts index 77d4d121..ffb60bfe 100644 --- a/tests/helpers/campaignFixtures.ts +++ b/tests/helpers/campaignFixtures.ts @@ -308,7 +308,7 @@ export class CampaignFixtures { builderCounter += 1 this.runID = `${processRunID}-${builderCounter}` const trackedCreate = async (args: Parameters[0]) => { - const document = await rootPayload.create(args as never) + const document = await rootPayload.create(args) const collection = args.collection const isLeasedConsent = collection === 'consent' && @@ -609,7 +609,7 @@ export class CampaignFixtures { contact: relationshipID(input.contact), municipalities: input.municipalities.map(relationshipID), ...(input.organizations - ? { organizations: input.organizations.map((value) => relationshipID(value as never)) } + ? { organizations: input.organizations.map((value) => relationshipID(value)) } : {}), ...(input.user ? { user: relationshipID(input.user) } : {}), ...(input.createdBy ? { createdBy: relationshipID(input.createdBy) } : {}), @@ -917,7 +917,7 @@ export class CampaignFixtures { collection, where: { id: { in: ids } }, depth: 0, - req: req as never, + req, }) const errors = bulkErrors(result) if (errors.length > 0) { @@ -943,7 +943,7 @@ export class CampaignFixtures { lastUpdateAt: null, }, depth: 0, - req: req as never, + req, }) } } diff --git a/tests/helpers/stub.ts b/tests/helpers/stub.ts new file mode 100644 index 00000000..0c98be64 --- /dev/null +++ b/tests/helpers/stub.ts @@ -0,0 +1,7 @@ +/** + * Types a partial test double as the full contract it stands in for. + * + * Unlike casting through `never`, the keys the test DOES provide keep full + * property-level type checking against `T` — only completeness is waived. + */ +export const stub = (partial: Partial): T => partial as T diff --git a/tests/int/campaignActionPlan.int.spec.ts b/tests/int/campaignActionPlan.int.spec.ts index a2216fe3..567a3dc3 100644 --- a/tests/int/campaignActionPlan.int.spec.ts +++ b/tests/int/campaignActionPlan.int.spec.ts @@ -1,6 +1,11 @@ // @vitest-environment node -import { getPayload, type Payload } from 'payload' +import { + getPayload, + type Payload, + type PayloadRequest, + type RequiredDataFromCollectionSlug, +} from 'payload' import { beforeAll, describe, expect, it } from 'vitest' import { actionPlanCreateSchema, actionPlanUpdateSchema } from '@/lib/schemas/actionPlan' @@ -13,6 +18,10 @@ import { } from '@/utilities/campaignAccess' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' + +/** Create data with server-derived fields (e.g. `slug`) intentionally omitted. */ +type ActionPlanCreateData = RequiredDataFromCollectionSlug<'actionPlan'> let payload: Payload const campaignFixtures = installCampaignFixtures({ @@ -88,13 +97,13 @@ describe('action plan domain', () => { }) const createAsCoordinator = await canCreateActionPlan({ - req: { user: coordinator, payload, context: {} } as never, + req: stub({ user: coordinator, payload, context: {} }), }) const createAsAdvisor = await canCreateActionPlan({ - req: { user: advisor, payload, context: {} } as never, + req: stub({ user: advisor, payload, context: {} }), }) const createAsLeader = await canCreateActionPlan({ - req: { user: leaderAccount, payload, context: {} } as never, + req: stub({ user: leaderAccount, payload, context: {} }), }) expect(createAsCoordinator).toBe(true) expect(createAsAdvisor).toBe(true) @@ -102,49 +111,49 @@ describe('action plan domain', () => { const plan = await payload.create({ collection: 'actionPlan', - data: { + data: stub({ ...validPlanInput(municipality.id), advisors: [advisor.id], leadership: leadership.id, responsible: contact.id, createdBy: coordinator.id, - } as never, + }), overrideAccess: true, }) fixtures.own('actionPlan', plan.id) const coordinatorRead = await canReadActionPlan({ - req: { user: coordinator, payload, context: {} } as never, + req: stub({ user: coordinator, payload, context: {} }), }) expect(coordinatorRead).toBe(true) const advisorRead = await canReadActionPlan({ - req: { user: advisor, payload, context: {} } as never, + req: stub({ user: advisor, payload, context: {} }), }) expect(advisorRead).toEqual({ or: [{ advisors: { contains: advisor.id } }, { municipality: { in: [municipality.id] } }], }) const otherAdvisorRead = await canReadActionPlan({ - req: { user: otherAdvisor, payload, context: {} } as never, + req: stub({ user: otherAdvisor, payload, context: {} }), }) expect(otherAdvisorRead).toEqual({ or: [{ advisors: { contains: otherAdvisor.id } }, { municipality: { in: [] } }], }) const leaderRead = await canReadActionPlan({ - req: { user: leaderAccount, payload, context: {} } as never, + req: stub({ user: leaderAccount, payload, context: {} }), }) expect(leaderRead).toBe(false) const leadershipIds = await getAccessibleLeadershipIds( - { user: leaderAccount, payload, context: {} } as never, + stub({ user: leaderAccount, payload, context: {} }), leaderAccount, ) expect(leadershipIds).toEqual([]) const advisorUpdate = await canUpdateActionPlan({ - req: { user: advisor, payload, context: {} } as never, + req: stub({ user: advisor, payload, context: {} }), }) expect(advisorUpdate).toEqual({ or: [{ advisors: { contains: advisor.id } }, { municipality: { in: [municipality.id] } }], @@ -187,10 +196,10 @@ describe('action plan domain', () => { const plan = await payload.create({ collection: 'actionPlan', - data: { + data: stub({ ...validPlanInput(municipality.id), title: fixtures.value('Plano do assessor'), - } as never, + }), user: advisor, overrideAccess: false, }) @@ -213,12 +222,12 @@ describe('action plan domain', () => { await expect( payload.create({ collection: 'actionPlan', - data: { + data: stub({ ...validPlanInput(municipality.id), title: fixtures.value('Plano sem data'), startAt: null, createdBy: coordinator.id, - } as never, + }), overrideAccess: true, }), ).rejects.toThrow('Informe a data e horário de início ao planejar ou confirmar o plano.') @@ -227,13 +236,13 @@ describe('action plan domain', () => { await expect( payload.create({ collection: 'actionPlan', - data: { + data: stub({ ...validPlanInput(municipality.id), title: fixtures.value('Plano com término invertido'), startAt: start.toISOString(), endAt: new Date(start.getTime() - 3_600_000).toISOString(), createdBy: coordinator.id, - } as never, + }), overrideAccess: true, }), ).rejects.toThrow('O horário de término deve ser posterior ao de início.') @@ -255,13 +264,13 @@ describe('action plan domain', () => { const plan = await payload.create({ collection: 'actionPlan', - data: { + data: stub({ ...validPlanInput(municipality.id), title: fixtures.value('Plano Tarefas'), advisors: [advisor.id], leadership: leadership.id, tasks: [{ title: 'Levar faixas', done: false }], - } as never, + }), overrideAccess: true, }) fixtures.own('actionPlan', plan.id) @@ -272,7 +281,7 @@ describe('action plan domain', () => { id: plan.id, data: { tasks: [{ title: 'Levar faixas', done: true }], - } as never, + }, user: leaderAccount, overrideAccess: false, }), diff --git a/tests/int/campaignAuth.int.spec.ts b/tests/int/campaignAuth.int.spec.ts index 54a1c606..74213604 100644 --- a/tests/int/campaignAuth.int.spec.ts +++ b/tests/int/campaignAuth.int.spec.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs' -import type { Payload, PayloadRequest } from 'payload' +import type { Payload, PayloadRequest, RequiredDataFromCollectionSlug } from 'payload' import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { assignMunicipalityAdvisorsRecord } from '@/app/(campaign)/campanha/actions/municipality' @@ -20,6 +20,10 @@ import { import { getPayload } from 'payload' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' + +/** Create data with `role` intentionally omitted so the collection default applies. */ +type CampaignUserCreateData = RequiredDataFromCollectionSlug<'campaignUser'> let payload: Payload const campaignFixtures = installCampaignFixtures({ @@ -52,11 +56,11 @@ describe('campaign authentication foundation', () => { const username = campaignFixtures().phone() const user = await payload.create({ collection: 'campaignUser', - data: { + data: stub({ name: 'Liderança padrão', username, password: campaignFixtures().value('password'), - } as never, + }), }) expect(user.role).toBe('leader') @@ -267,11 +271,11 @@ describe('campaign authentication foundation', () => { const username = campaignFixtures().phone() const target = await payload.create({ collection: 'campaignUser', - data: { + data: stub({ name: 'Liderança administrada', username, password: campaignFixtures().value('password'), - } as never, + }), }) const visibleTarget = await payload.findByID({ @@ -383,11 +387,11 @@ describe('campaign authentication foundation', () => { await expect( payload.create({ collection: 'campaignUser', - data: { + data: stub({ name: 'Criação indevida', email: `${campaignFixtures().value('denied')}@example.com`, password: campaignFixtures().value('password'), - } as never, + }), user: actor, overrideAccess: false, }), diff --git a/tests/int/campaignFixturesRowCounts.int.spec.ts b/tests/int/campaignFixturesRowCounts.int.spec.ts index c4d45bc4..f08f82ea 100644 --- a/tests/int/campaignFixturesRowCounts.int.spec.ts +++ b/tests/int/campaignFixturesRowCounts.int.spec.ts @@ -2,20 +2,19 @@ import { createRequire } from 'node:module' -import { sql } from '@payloadcms/db-postgres' +import { sql, type MigrateUpArgs } from '@payloadcms/db-postgres' import { drizzle } from '@payloadcms/db-postgres/drizzle/node-postgres' import { getPayload, type Payload } from 'payload' +import type { Pool as PgPool } from 'pg' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { migrations } from '@/migrations' import { withCampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' const require = createRequire(import.meta.resolve('@payloadcms/db-postgres')) const { Pool } = require('pg') as { - Pool: new (options: { connectionString: string }) => { - end: () => Promise - query: (statement: string) => Promise - } + Pool: new (options: { connectionString: string }) => PgPool } const testDatabaseUrl = @@ -113,9 +112,9 @@ beforeAll(async () => { await adminPool.end() const migrationPool = new Pool({ connectionString: databaseUrl }) - const database = drizzle(migrationPool as never) + const database = drizzle>(migrationPool) for (const migration of migrations) { - await migration.up({ db: database } as never) + await migration.up(stub({ db: database })) } await migrationPool.end() diff --git a/tests/int/campaignInvite.int.spec.ts b/tests/int/campaignInvite.int.spec.ts index bf16a812..3a75203b 100644 --- a/tests/int/campaignInvite.int.spec.ts +++ b/tests/int/campaignInvite.int.spec.ts @@ -922,7 +922,10 @@ describe('campaign invite domain', () => { const actions = await import('@/app/(campaign)/campanha/actions/invite') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const invite = await createInviteThroughAction(coordinator, { leadership: leadership.id, kind: 'autopreenchimento', @@ -983,7 +986,10 @@ describe('campaign invite domain', () => { const actions = await import('@/app/(campaign)/campanha/actions/invite') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const invite = await createInviteThroughAction(coordinator, { leadership: leadership.id, kind: 'autopreenchimento', @@ -1318,7 +1324,10 @@ describe('campaign invite domain', () => { const actions = await import('@/app/(campaign)/campanha/actions/invite') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const invite = await createInviteThroughAction(coordinator, { leadership: leadership.id, kind: 'login', @@ -1370,7 +1379,10 @@ describe('campaign invite domain', () => { }) const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const account = await payload.create({ collection: 'campaignUser', data: { @@ -1425,7 +1437,10 @@ describe('campaign invite domain', () => { const { getCampaignInvitePageData } = await import('@/utilities/campaignInvitePageData') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const firstInvite = await createInviteThroughAction(coordinator, { leadership: leadership.id, kind: 'autopreenchimento', @@ -1471,7 +1486,7 @@ describe('campaign invite domain', () => { version: 1, }, }, - } as never, + }, }) const secondInvite = await createInviteThroughAction(coordinator, { leadership: leadership.id, @@ -1514,7 +1529,10 @@ describe('campaign invite domain', () => { const records = await import('@/app/(campaign)/campanha/actions/invite') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const victim = await payload.create({ collection: 'campaignUser', data: { @@ -1602,7 +1620,10 @@ describe('campaign invite domain', () => { const records = await import('@/app/(campaign)/campanha/actions/invite') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const invite = await createInviteThroughAction(coordinator, { leadership: leadership.id, kind: 'autopreenchimento', @@ -1654,7 +1675,10 @@ describe('campaign invite domain', () => { const actions = await import('@/app/(campaign)/campanha/actions/invite') const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const { contact, leadership } = await createInviteLeadershipGraph(coordinator, municipality.id) + const { contact, leadership } = await createInviteLeadershipGraph( + coordinator, + municipality.id, + ) const account = await payload.create({ collection: 'campaignUser', data: { diff --git a/tests/int/campaignLeadership.int.spec.ts b/tests/int/campaignLeadership.int.spec.ts index 6b0ec1bd..f755d353 100644 --- a/tests/int/campaignLeadership.int.spec.ts +++ b/tests/int/campaignLeadership.int.spec.ts @@ -66,7 +66,7 @@ describe('campaign leadership domain', () => { createdBy: 999, user: 999, consent: 999, - } as never) + }) expect(parsed.phone).toBe('71999990000') expect(parsed.municipalities).toEqual([1, 2]) @@ -81,7 +81,7 @@ describe('campaign leadership domain', () => { contact: 999, user: 999, createdBy: 999, - } as never) + }) expect(update).toEqual({ id: 1 }) }) @@ -188,7 +188,7 @@ describe('campaign leadership domain', () => { markFirstRead = resolve }) const findSpy = vi.spyOn(payload, 'find').mockImplementation(async (args) => { - const result = await originalFind(args as never) + const result = await originalFind(args) if ( args.collection === 'contact' && 'where' in args && @@ -200,7 +200,7 @@ describe('campaign leadership domain', () => { await firstReadGate } } - return result as never + return result }) const first = createLeadershipRecord(payload, coordinator, { @@ -461,7 +461,7 @@ describe('campaign leadership domain', () => { if (args.collection === 'leadership') { throw new Error('falha forçada após contato') } - return originalCreate(args as never) + return originalCreate(args) }) await expect( diff --git a/tests/int/campaignMigrationReconciliation.int.spec.ts b/tests/int/campaignMigrationReconciliation.int.spec.ts index 7aa94ead..da9aa79c 100644 --- a/tests/int/campaignMigrationReconciliation.int.spec.ts +++ b/tests/int/campaignMigrationReconciliation.int.spec.ts @@ -2,29 +2,28 @@ import { createRequire } from 'node:module' -import { sql } from '@payloadcms/db-postgres' +import { sql, type MigrateDownArgs, type MigrateUpArgs } from '@payloadcms/db-postgres' import { drizzle } from '@payloadcms/db-postgres/drizzle/node-postgres' +import type { Pool as PgPool } from 'pg' import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { migrations } from '@/migrations' +import { stub } from '../helpers/stub' const require = createRequire(import.meta.resolve('@payloadcms/db-postgres')) const { Pool } = require('pg') as { - Pool: new (options: { connectionString: string }) => { - connect: () => Promise<{ query: (statement: string) => Promise; release: () => void }> - end: () => Promise - query: (statement: string) => Promise<{ rows: Array> }> - } + Pool: new (options: { connectionString: string }) => PgPool } const testDatabaseUrl = process.env.DATABASE_URL ?? 'postgresql://teqo:teqo@localhost:5432/teqo_test' const adminUrl = testDatabaseUrl.replace(/\/[^/]+$/, '/postgres') const consolidatedName = '20260718_010733_consolidate_campaign_schema' +type MigrationDatabase = MigrateUpArgs['db'] let databaseSequence = 0 let databaseName = '' let databasePool: InstanceType | undefined -let database: ReturnType | undefined +let database: MigrationDatabase | undefined const quoteIdentifier = (identifier: string): string => { if (!/^[a-z0-9_]+$/.test(identifier)) throw new Error(`Unsafe database identifier: ${identifier}`) @@ -77,10 +76,10 @@ beforeEach(async () => { databasePool = new Pool({ connectionString: testDatabaseUrl.replace(/\/[^/]+$/, `/${databaseName}`), }) - database = drizzle(databasePool as never) + database = drizzle>(databasePool) for (const migration of migrations) { - await migration.up({ db: database } as never) + await migration.up(stub({ db: database })) } }) @@ -139,7 +138,7 @@ describe('campaign migration existing-schema reconciliation', () => { await expect( database.transaction(async (transaction) => { - await consolidated.up({ db: transaction } as never) + await consolidated.up(stub({ db: transaction })) await transaction.execute(sql` INSERT INTO "payload_migrations" ("name", "batch", "updated_at", "created_at") VALUES (${consolidatedName}, 999, now(), now()) @@ -176,7 +175,7 @@ describe('campaign migration existing-schema reconciliation', () => { `) const checksumBefore = await schemaChecksum() - await expect(consolidated.down({ db: database } as never)).rejects.toThrow( + await expect(consolidated.down(stub({ db: database }))).rejects.toThrow( 'Refusing to roll back consolidated campaign schema', ) diff --git a/tests/int/campaignMunicipalityAccess.int.spec.ts b/tests/int/campaignMunicipalityAccess.int.spec.ts index 41dd52f5..e905a0b9 100644 --- a/tests/int/campaignMunicipalityAccess.int.spec.ts +++ b/tests/int/campaignMunicipalityAccess.int.spec.ts @@ -1,6 +1,6 @@ // @vitest-environment node -import type { Payload } from 'payload' +import type { Payload, PayloadRequest } from 'payload' import { getPayload } from 'payload' import { beforeAll, describe, expect, it } from 'vitest' @@ -15,6 +15,7 @@ import config from '@/payload.config' import { getAccessibleMunicipalityIds } from '@/utilities/campaignAccess' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' let payload: Payload const campaignFixtures = installCampaignFixtures({ @@ -117,7 +118,7 @@ describe('municipality catalog seed and per-role access', () => { }) const visible = await getAccessibleMunicipalityIds( - { payload, user: account, context: {} } as never, + stub({ payload, user: account, context: {} }), account, ) expect(visible?.sort((a, b) => a - b)).toEqual( @@ -145,7 +146,7 @@ describe('municipality catalog seed and per-role access', () => { }) const scope = await getAccessibleMunicipalityIds( - { payload, user: account, context: {} } as never, + stub({ payload, user: account, context: {} }), account, ) expect(scope).toEqual([]) @@ -259,7 +260,7 @@ describe('municipality catalog seed and per-role access', () => { ).toEqual([advisor.id]) const scope = await getAccessibleMunicipalityIds( - { payload, user: advisor, context: {} } as never, + stub({ payload, user: advisor, context: {} }), advisor, ) expect(scope).toEqual([municipality.id]) diff --git a/tests/int/campaignMunicipalityUpdate.int.spec.ts b/tests/int/campaignMunicipalityUpdate.int.spec.ts index 7d832c07..7f816c7f 100644 --- a/tests/int/campaignMunicipalityUpdate.int.spec.ts +++ b/tests/int/campaignMunicipalityUpdate.int.spec.ts @@ -1,6 +1,11 @@ // @vitest-environment node -import { getPayload, type Payload } from 'payload' +import { + getPayload, + type Payload, + type PayloadRequest, + type RequiredDataFromCollectionSlug, +} from 'payload' import { beforeAll, describe, expect, it, vi } from 'vitest' import { createMunicipalityUpdateRecord } from '@/app/(campaign)/campanha/actions/municipalityUpdate' @@ -15,6 +20,10 @@ import { } from '../helpers/testDatabaseLease' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' + +/** Create data with server-managed fields (e.g. `author`) intentionally omitted. */ +type MunicipalityUpdateCreateData = RequiredDataFromCollectionSlug<'municipalityUpdate'> let payload: Payload const campaignFixtures = installCampaignFixtures({ @@ -100,7 +109,7 @@ describe('campaign municipality update domain', () => { author: 999, createdAt: '2000-01-01T00:00:00.000Z', updatedAt: '2000-01-01T00:00:00.000Z', - } as never) + }) expect(parsed).toEqual({ municipality: 1, @@ -116,11 +125,11 @@ describe('campaign municipality update domain', () => { await expect( payload.create({ collection: 'municipalityUpdate', - data: { + data: stub({ municipality: municipality.id, kind: 'semanal', worked: 'Somente uma resposta', - } as never, + }), user: coordinator, overrideAccess: false, }), @@ -128,11 +137,11 @@ describe('campaign municipality update domain', () => { await expect( payload.create({ collection: 'municipalityUpdate', - data: { + data: stub({ municipality: municipality.id, kind: 'urgente', body: ' ', - } as never, + }), user: coordinator, overrideAccess: false, }), @@ -146,7 +155,7 @@ describe('campaign municipality update domain', () => { ? await MunicipalityUpdate.access.update({ id: 1, data: undefined, - req: { user: coordinator } as never, + req: stub({ user: coordinator }), }) : MunicipalityUpdate.access?.update const deleteAccess = @@ -154,7 +163,7 @@ describe('campaign municipality update domain', () => { ? await MunicipalityUpdate.access.delete({ id: 1, data: undefined, - req: { user: coordinator } as never, + req: stub({ user: coordinator }), }) : MunicipalityUpdate.access?.delete @@ -170,16 +179,18 @@ describe('campaign municipality update domain', () => { it('lets the coordinator create and read every update', async () => { const coordinator = await campaignFixtures().createCampaignUser('coordinator') const municipality = await campaignFixtures().getMunicipality() - const created = await createMunicipalityUpdateRecord(payload, coordinator, { + // Forged `author` goes through a widened variable: the schema must strip it. + const forgedInput = { municipality: municipality.id, - kind: 'semanal', + kind: 'semanal' as const, worked: 'Visitas concluídas', failed: 'Dois endereços fechados', needs: 'Mais panfletos', activeVolunteers: 7, newSupports: 12, author: 999, - } as never) + } + const created = await createMunicipalityUpdateRecord(payload, coordinator, forgedInput) expect(created.author).toBe(coordinator.id) const visible = await listMunicipalityUpdates(coordinator, municipality.id) @@ -278,11 +289,11 @@ describe('campaign municipality update domain', () => { await expect( payload.create({ collection: 'municipalityUpdate', - data: { + data: stub({ municipality: municipality.id, kind: 'nota', body: 'Tentativa anônima', - } as never, + }), overrideAccess: false, }), ).rejects.toThrow() @@ -537,7 +548,7 @@ describe('campaign municipality update domain', () => { ) { throw new Error('falha forçada no campo derivado') } - return originalUpdate(args as never) as never + return originalUpdate(args) }) try { diff --git a/tests/int/campaignUserPhone.int.spec.ts b/tests/int/campaignUserPhone.int.spec.ts index e30b525a..767b1e64 100644 --- a/tests/int/campaignUserPhone.int.spec.ts +++ b/tests/int/campaignUserPhone.int.spec.ts @@ -1,6 +1,12 @@ // @vitest-environment node -import { getPayload, type Field, type Payload, type PayloadRequest } from 'payload' +import { + getPayload, + type Field, + type FieldAccess, + type Payload, + type PayloadRequest, +} from 'payload' import { beforeAll, describe, expect, it } from 'vitest' import { CampaignUser as CampaignUserCollection } from '@/collections/CampaignUser' @@ -8,6 +14,9 @@ import type { CampaignUser } from '@/payload-types' import config from '@/payload.config' import { withCampaignFixtures, type CampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' + +type FieldAccessArgs = Parameters[0] let payload: Payload @@ -233,14 +242,18 @@ describe('campaign user contact phone', () => { user: coordinator, } as unknown as PayloadRequest - await expect(field.access?.update?.({ id: owner.id, req: ownerReq } as never)).resolves.toBe( - true, + await expect( + field.access?.update?.(stub({ id: owner.id, req: ownerReq })), + ).resolves.toBe(true) + await expect( + field.access?.update?.(stub({ id: owner.id, req: coordinatorReq })), + ).resolves.toBe(true) + await expect(field.access?.create?.(stub({ req: ownerReq }))).resolves.toBe( + false, ) await expect( - field.access?.update?.({ id: owner.id, req: coordinatorReq } as never), + field.access?.create?.(stub({ req: coordinatorReq })), ).resolves.toBe(true) - await expect(field.access?.create?.({ req: ownerReq } as never)).resolves.toBe(false) - await expect(field.access?.create?.({ req: coordinatorReq } as never)).resolves.toBe(true) }) }) }) diff --git a/tests/int/collectionAccessLockdown.int.spec.ts b/tests/int/collectionAccessLockdown.int.spec.ts index 2a49f31f..edd49595 100644 --- a/tests/int/collectionAccessLockdown.int.spec.ts +++ b/tests/int/collectionAccessLockdown.int.spec.ts @@ -1,13 +1,15 @@ // @vitest-environment node -import { getPayload, type Payload } from 'payload' +import { getPayload, type Payload, type RequiredDataFromCollectionSlug } from 'payload' import { beforeAll, describe, expect, it } from 'vitest' import { createLeaderSupporterRecord } from '@/app/(campaign)/campanha/actions/leaderSupporter' import config from '@/payload.config' import { SUPPORTER_REGISTRATION_CONSENT_KEY } from '@/utilities/campaignConsent' +import { relationshipId } from '@/utilities/relationship' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { stub } from '../helpers/stub' import { SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, withLeasedConsent, @@ -112,10 +114,11 @@ describe('CMS/PII collection access lockdown', () => { await expect( payload.create({ collection, - data: {}, + // Denied before validation: an empty payload stands in for required fields. + data: stub>({}), user: coordinator, overrideAccess: false, - } as never), + }), ).rejects.toThrow(/permissão|not allowed/i) }, ) @@ -159,8 +162,8 @@ describe('leader supporter creation scope', () => { fixtures.own('supporter', supporter.id) expect(supporter.source).toBe('lideranca') - expect(fixtures.id(supporter.createdBy as never)).toBe(leaderAccount.id) - expect(fixtures.id(supporter.municipality as never)).toBe(linked.id) + expect(relationshipId(supporter.createdBy)).toBe(leaderAccount.id) + expect(relationshipId(supporter.municipality)).toBe(linked.id) }) it('rejects a supporter outside the leadership municipalities', async () => { diff --git a/tests/int/contactPhoneNormalization.int.spec.ts b/tests/int/contactPhoneNormalization.int.spec.ts index 0490474e..1e16f807 100644 --- a/tests/int/contactPhoneNormalization.int.spec.ts +++ b/tests/int/contactPhoneNormalization.int.spec.ts @@ -19,15 +19,19 @@ vi.mock('@/utilities/phone', async (importOriginal) => { import { Contact } from '@/collections/Contact' import { leadershipCreateSchema } from '@/lib/schemas/leadership' +import { stub } from '../helpers/stub' + const contactPhoneHook = Contact.hooks?.beforeValidate?.[0] +type ContactPhoneHookArgs = Parameters>[0] + const runContactPhoneHook = async (phone: string) => { if (typeof contactPhoneHook !== 'function') { throw new Error('Expected Contact.beforeValidate phone hook.') } const data = { phone } - await contactPhoneHook({ data } as never) + await contactPhoneHook(stub({ data })) return data.phone } @@ -54,9 +58,7 @@ describe('Contact phone normalization boundary', () => { }) it('rejects an invalid canonical-length direct API phone without re-normalizing it', async () => { - await expect(runContactPhoneHook('00000000000')).rejects.toThrow( - 'Celular brasileiro inválido.', - ) + await expect(runContactPhoneHook('00000000000')).rejects.toThrow('Celular brasileiro inválido.') expect(phoneMocks.normalizeBrazilianPhone).not.toHaveBeenCalled() }) }) diff --git a/tests/int/onda0Provision.int.spec.ts b/tests/int/onda0Provision.int.spec.ts index e1be7277..d0f66a2f 100644 --- a/tests/int/onda0Provision.int.spec.ts +++ b/tests/int/onda0Provision.int.spec.ts @@ -10,6 +10,12 @@ import { provisionOnda0ConsentAndPrivacy } from '@/utilities/onda0Provision' import { assertTestDatabase } from '../helpers/assertTestDatabase' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { + CAMPAIGN_INVITE_CONSENT_LEASE_KEY, + SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY, + withSharedTestDatabaseLease, +} from '../helpers/testDatabaseLease' let payload: Payload @@ -27,20 +33,29 @@ describe('Onda 0 provision (integration)', () => { }) it('upserts consent keys and publishes privacy-policy idempotently', async () => { - await provisionOnda0ConsentAndPrivacy(payload) - await provisionOnda0ConsentAndPrivacy(payload) + // Shared leases on every stable consent key: parallel spec files exercise + // "consent missing" fail-closed paths by deleting these rows under an + // EXCLUSIVE lease — without the shared leases this test races that window. + await withSharedTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY, () => + withSharedTestDatabaseLease(payload, SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, () => + withSharedTestDatabaseLease(payload, SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY, async () => { + await provisionOnda0ConsentAndPrivacy(payload) + await provisionOnda0ConsentAndPrivacy(payload) - for (const { key } of ONDA0_CONSENT_ENTRIES) { - const descriptor = await requireConsentByKey(payload, key) - expect(descriptor.key).toBe(key) - expect(descriptor.contentHash).toMatch(/^[a-f0-9]{64}$/) - } + for (const { key } of ONDA0_CONSENT_ENTRIES) { + const descriptor = await requireConsentByKey(payload, key) + expect(descriptor.key).toBe(key) + expect(descriptor.contentHash).toMatch(/^[a-f0-9]{64}$/) + } - const privacy = await payload.findGlobal({ - slug: 'privacy-policy', - overrideAccess: true, - }) - expect(privacy.published).toBe(true) - expect(privacy.body).toBeTruthy() + const privacy = await payload.findGlobal({ + slug: 'privacy-policy', + overrideAccess: true, + }) + expect(privacy.published).toBe(true) + expect(privacy.body).toBeTruthy() + }), + ), + ) }) }) diff --git a/tests/int/payloadTransaction.int.spec.ts b/tests/int/payloadTransaction.int.spec.ts index 4d5f5c9b..49ee2504 100644 --- a/tests/int/payloadTransaction.int.spec.ts +++ b/tests/int/payloadTransaction.int.spec.ts @@ -1,9 +1,14 @@ // @vitest-environment node +import type { Payload } from 'payload' import { describe, expect, it, vi } from 'vitest' import { withPayloadTransaction } from '@/utilities/payloadTransaction' +import { stub } from '../helpers/stub' + +type DatabaseSession = NonNullable[string] + const createPayload = ({ transactionID = 17, commitError, @@ -27,15 +32,15 @@ const createPayload = ({ return { database, payload: { - db: { + db: stub({ beginTransaction, commitTransaction, rollbackTransaction, sessions: transactionID === null || !hasSession ? {} - : { [String(transactionID)]: { db: database } }, - }, + : { [String(transactionID)]: stub({ db: database }) }, + }), }, beginTransaction, commitTransaction, @@ -51,7 +56,7 @@ describe('withPayloadTransaction', () => { return { req } }) - const result = await withPayloadTransaction(fixture.payload as never, callback) + const result = await withPayloadTransaction(fixture.payload, callback) expect(result.req).toBe(callback.mock.calls[0]![0].req) expect(fixture.commitTransaction).toHaveBeenCalledWith(17) @@ -63,7 +68,7 @@ describe('withPayloadTransaction', () => { const callback = vi.fn() await expect( - withPayloadTransaction(fixture.payload as never, callback, { + withPayloadTransaction(fixture.payload, callback, { beginFailureMessage: 'falha específica', }), ).rejects.toThrow('falha específica') @@ -78,7 +83,7 @@ describe('withPayloadTransaction', () => { const original = new Error('callback falhou') await expect( - withPayloadTransaction(fixture.payload as never, async () => { + withPayloadTransaction(fixture.payload, async () => { throw original }), ).rejects.toBe(original) @@ -91,9 +96,9 @@ describe('withPayloadTransaction', () => { const original = new Error('commit falhou') const fixture = createPayload({ commitError: original }) - await expect( - withPayloadTransaction(fixture.payload as never, async () => 'result'), - ).rejects.toBe(original) + await expect(withPayloadTransaction(fixture.payload, async () => 'result')).rejects.toBe( + original, + ) expect(fixture.commitTransaction).toHaveBeenCalledWith(17) expect(fixture.rollbackTransaction).toHaveBeenCalledWith(17) @@ -104,7 +109,7 @@ describe('withPayloadTransaction', () => { const rollback = new Error('rollback falhou') const fixture = createPayload({ rollbackError: rollback }) - const failure = await withPayloadTransaction(fixture.payload as never, async () => { + const failure = await withPayloadTransaction(fixture.payload, async () => { throw original }).catch((error: unknown) => error) @@ -118,10 +123,9 @@ describe('withPayloadTransaction', () => { const rollback = new Error('rollback falhou') const fixture = createPayload({ commitError: commit, rollbackError: rollback }) - const failure = await withPayloadTransaction( - fixture.payload as never, - async () => 'result', - ).catch((error: unknown) => error) + const failure = await withPayloadTransaction(fixture.payload, async () => 'result').catch( + (error: unknown) => error, + ) expect(failure).toBeInstanceOf(AggregateError) expect((failure as AggregateError).errors).toEqual([commit, rollback]) diff --git a/tests/int/postgresTransactionLocks.int.spec.ts b/tests/int/postgresTransactionLocks.int.spec.ts index 9a7721a8..c9f23cb9 100644 --- a/tests/int/postgresTransactionLocks.int.spec.ts +++ b/tests/int/postgresTransactionLocks.int.spec.ts @@ -17,6 +17,9 @@ import { getTestTransactionBackendPID, waitForAdvisoryLockWaiter, } from '../helpers/testDatabaseLease' +import { stub } from '../helpers/stub' + +type DatabaseSession = NonNullable[string] let payload: Payload @@ -64,30 +67,28 @@ describe('PostgreSQL transaction advisory locks', () => { 'contact-phone:71999990001', 'contact-phone:71999990002', ]) - expect(CONTACT_PHONE_CONFLICT_MESSAGE).toBe( - 'Já existe outro contato com este celular.', - ) + expect(CONTACT_PHONE_CONFLICT_MESSAGE).toBe('Já existe outro contato com este celular.') }) it('uses only the exact Payload transaction session and fails closed without it', async () => { const sessionDatabase = { execute: vi.fn().mockResolvedValue(undefined) } const unrelatedDatabase = { execute: vi.fn().mockResolvedValue(undefined) } const fixture = { - db: { + db: stub({ name: 'postgres', sessions: { - '41': { db: sessionDatabase }, - '42': { db: unrelatedDatabase }, + '41': stub({ db: sessionDatabase }), + '42': stub({ db: unrelatedDatabase }), }, - }, + }), } await expect( - getPostgresTransactionDatabase(fixture as never, { + getPostgresTransactionDatabase(fixture, { transactionID: Promise.resolve(41), }), ).resolves.toBe(sessionDatabase) - await acquireTextAdvisoryLocks(fixture as never, { transactionID: 41 }, [ + await acquireTextAdvisoryLocks(fixture, { transactionID: 41 }, [ 'domain:2', 'domain:1', 'domain:1', @@ -98,31 +99,28 @@ describe('PostgreSQL transaction advisory locks', () => { expect(collectLockParams(query)).toEqual(['domain:1', 'domain:2']) expect(unrelatedDatabase.execute).not.toHaveBeenCalled() - await expect(getPostgresTransactionDatabase(fixture as never, {})).rejects.toThrow( + await expect(getPostgresTransactionDatabase(fixture, {})).rejects.toThrow( 'A transação PostgreSQL não está disponível.', ) - await expect( - acquireTextAdvisoryLocks(fixture as never, {}, ['domain:1']), - ).rejects.toThrow('A transação PostgreSQL não está disponível.') - await expect( - getPostgresTransactionDatabase(fixture as never, { transactionID: 99 }), - ).rejects.toThrow('A sessão PostgreSQL da transação não está disponível.') + await expect(acquireTextAdvisoryLocks(fixture, {}, ['domain:1'])).rejects.toThrow( + 'A transação PostgreSQL não está disponível.', + ) + await expect(getPostgresTransactionDatabase(fixture, { transactionID: 99 })).rejects.toThrow( + 'A sessão PostgreSQL da transação não está disponível.', + ) }) it('rejects invalid text keys before acquiring any lock', async () => { const database = { execute: vi.fn().mockResolvedValue(undefined) } const fixture = { - db: { + db: stub({ name: 'postgres', - sessions: { '51': { db: database } }, - }, + sessions: { '51': stub({ db: database }) }, + }), } await expect( - acquireTextAdvisoryLocks(fixture as never, { transactionID: 51 }, [ - 'valid:key', - ' ', - ]), + acquireTextAdvisoryLocks(fixture, { transactionID: 51 }, ['valid:key', ' ']), ).rejects.toThrow('A chave do bloqueio PostgreSQL deve ser um texto não vazio.') expect(database.execute).not.toHaveBeenCalled() }) diff --git a/tests/unit/campaignActionContext.unit.spec.ts b/tests/unit/campaignActionContext.unit.spec.ts index f0dae092..13acd690 100644 --- a/tests/unit/campaignActionContext.unit.spec.ts +++ b/tests/unit/campaignActionContext.unit.spec.ts @@ -16,9 +16,7 @@ describe('campaign action context', () => { const findByID = vi.fn().mockResolvedValue(currentActor) const req = { transactionID: 21 } - await expect(reloadCampaignActor({ findByID } as never, staleActor, req)).resolves.toBe( - currentActor, - ) + await expect(reloadCampaignActor({ findByID }, staleActor, req)).resolves.toBe(currentActor) expect(findByID).toHaveBeenCalledWith({ collection: 'campaignUser', id: staleActor.id, diff --git a/tests/unit/campaignUserProfile.unit.spec.ts b/tests/unit/campaignUserProfile.unit.spec.ts index e8651fb2..30604fa4 100644 --- a/tests/unit/campaignUserProfile.unit.spec.ts +++ b/tests/unit/campaignUserProfile.unit.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' +import type { Media } from '@/payload-types' import { assertCampaignAvatarFile, campaignUserInitials, @@ -9,6 +10,8 @@ import { mediaDocumentUrl, } from '@/utilities/campaignUserProfile' +import { stub } from '../helpers/stub' + describe('campaignUserProfile helpers', () => { it('builds initials from the user name', () => { expect(campaignUserInitials('Maria Silva')).toBe('MS') @@ -20,7 +23,7 @@ describe('campaignUserProfile helpers', () => { campaignUserShellView({ name: 'Maria', role: 'coordinator', - avatar: { id: 1, url: 'https://example.com/a.jpg' } as never, + avatar: stub({ id: 1, url: 'https://example.com/a.jpg' }), }), ).toEqual({ name: 'Maria', diff --git a/tests/unit/federalCandidateTotalsAggregate.unit.spec.ts b/tests/unit/federalCandidateTotalsAggregate.unit.spec.ts deleted file mode 100644 index 374f353e..00000000 --- a/tests/unit/federalCandidateTotalsAggregate.unit.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -// @vitest-environment node - -import type { Payload } from 'payload' -import { describe, expect, it, vi } from 'vitest' - -import type { CampaignUser, User } from '@/payload-types' -import { assertCanReadElectionData } from '@/utilities/campaignAccess' -import * as drizzleBulk from '@/utilities/drizzleBulk' -import { loadFederalCandidateTotalsAggregated } from '@/utilities/federalCandidateTotalsAggregate' -import type { MunicipalityElectionGeography } from '@/utilities/municipalityElectionGeography' - -describe('assertCanReadElectionData', () => { - it('allows payload admins and staff campaign users to read election data', () => { - expect(() => - assertCanReadElectionData({ - collection: 'users', - id: 1, - } as User), - ).not.toThrow() - - expect(() => - assertCanReadElectionData({ - collection: 'campaignUser', - id: 1, - role: 'advisor', - } as unknown as CampaignUser), - ).not.toThrow() - - expect(() => - assertCanReadElectionData({ - collection: 'campaignUser', - id: 1, - role: 'leader', - } as unknown as CampaignUser), - ).toThrow('Leitura de dados eleitorais negada.') - }) - - it('denies readers outside campaign/admin collections', () => { - expect(() => - assertCanReadElectionData({ - collection: 'contact', - id: 1, - } as never), - ).toThrow('Leitura de dados eleitorais negada.') - }) -}) - -describe('loadFederalCandidateTotalsAggregated', () => { - it('maps drizzle aggregate rows and scopes geography in SQL', async () => { - const geography: MunicipalityElectionGeography = { - cityCode: '38490', - zones: [1, 2], - } - - const execute = vi.fn().mockResolvedValue({ - rows: [ - { - candidate_number: 1313, - candidate_name: 'JORGE SOLLA', - party: 'PT', - votes: 2100, - }, - { - candidate_number: 1234, - candidate_name: 'OUTRO', - party: 'PL', - votes: 900, - }, - ], - }) - vi.spyOn(drizzleBulk, 'requirePostgresDrizzle').mockReturnValue({ execute }) - - const payload = { db: { name: 'postgres' } } as Pick - const totals = await loadFederalCandidateTotalsAggregated( - payload, - { - collection: 'campaignUser', - id: 1, - role: 'coordinator', - } as unknown as CampaignUser, - geography, - ) - - expect(totals).toEqual([ - { candidateNumber: 1313, name: 'JORGE SOLLA', party: 'PT', votes: 2100 }, - { candidateNumber: 1234, name: 'OUTRO', party: 'PL', votes: 900 }, - ]) - expect(execute).toHaveBeenCalledOnce() - const sqlPayload = JSON.stringify(execute.mock.calls[0]?.[0]) - expect(sqlPayload).toContain('city_code') - expect(sqlPayload).not.toContain('city_name') - vi.restoreAllMocks() - }) -}) diff --git a/tsconfig.json b/tsconfig.json index 0c68c372..c0f4b655 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], + "lib": [ + "DOM", + "DOM.Iterable", + "ES2022" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -18,204 +22,24 @@ } ], "paths": { - "@/*": ["./src/*"], - "@payload-config": ["./src/payload.config.ts"] + "@/*": [ + "./src/*" + ], + "@payload-config": [ + "./src/payload.config.ts" + ] }, - "target": "ES2022" + "target": "ES2022", + "noUnusedLocals": true }, "include": [ "**/*.ts", "**/*.tsx", - ".next/build-composition/types/**/*.ts", - ".next/build-vote-estimate-version-final/types/**/*.ts", - ".next/build-vote-estimate-version/types/**/*.ts", - ".next/dev/types/**/*.ts", - ".next/e2e-10044/types/**/*.ts", - ".next/e2e-1006/types/**/*.ts", - ".next/e2e-1085/types/**/*.ts", - ".next/e2e-11201/types/**/*.ts", - ".next/e2e-11506/types/**/*.ts", - ".next/e2e-11663/types/**/*.ts", - ".next/e2e-12280/types/**/*.ts", - ".next/e2e-13120/types/**/*.ts", - ".next/e2e-13521/types/**/*.ts", - ".next/e2e-14136/types/**/*.ts", - ".next/e2e-14928/types/**/*.ts", - ".next/e2e-15205/types/**/*.ts", - ".next/e2e-1536/types/**/*.ts", - ".next/e2e-15855/types/**/*.ts", - ".next/e2e-15927/types/**/*.ts", - ".next/e2e-16483/types/**/*.ts", - ".next/e2e-16874/types/**/*.ts", - ".next/e2e-17058/types/**/*.ts", - ".next/e2e-17359/types/**/*.ts", - ".next/e2e-1764/types/**/*.ts", - ".next/e2e-18285/types/**/*.ts", - ".next/e2e-18508/types/**/*.ts", - ".next/e2e-18580/types/**/*.ts", - ".next/e2e-18785/types/**/*.ts", - ".next/e2e-19365/types/**/*.ts", - ".next/e2e-19899/types/**/*.ts", - ".next/e2e-20552/types/**/*.ts", - ".next/e2e-21365/types/**/*.ts", - ".next/e2e-21464/types/**/*.ts", - ".next/e2e-21632/types/**/*.ts", - ".next/e2e-2180/types/**/*.ts", - ".next/e2e-22000/types/**/*.ts", - ".next/e2e-22628/types/**/*.ts", - ".next/e2e-22922/types/**/*.ts", - ".next/e2e-23174/types/**/*.ts", - ".next/e2e-23318/types/**/*.ts", - ".next/e2e-23628/types/**/*.ts", - ".next/e2e-2427/types/**/*.ts", - ".next/e2e-24574/types/**/*.ts", - ".next/e2e-25365/types/**/*.ts", - ".next/e2e-25431/types/**/*.ts", - ".next/e2e-25587/types/**/*.ts", - ".next/e2e-27635/types/**/*.ts", - ".next/e2e-2815/types/**/*.ts", - ".next/e2e-28280/types/**/*.ts", - ".next/e2e-28367/types/**/*.ts", - ".next/e2e-28783/types/**/*.ts", - ".next/e2e-28804/types/**/*.ts", - ".next/e2e-29103/types/**/*.ts", - ".next/e2e-30214/types/**/*.ts", - ".next/e2e-303/types/**/*.ts", - ".next/e2e-30397/types/**/*.ts", - ".next/e2e-30528/types/**/*.ts", - ".next/e2e-31313/types/**/*.ts", - ".next/e2e-31599/types/**/*.ts", - ".next/e2e-32003/types/**/*.ts", - ".next/e2e-3335/types/**/*.ts", - ".next/e2e-33840/types/**/*.ts", - ".next/e2e-34312/types/**/*.ts", - ".next/e2e-36806/types/**/*.ts", - ".next/e2e-36946/types/**/*.ts", - ".next/e2e-37023/types/**/*.ts", - ".next/e2e-38948/types/**/*.ts", - ".next/e2e-39539/types/**/*.ts", - ".next/e2e-39565/types/**/*.ts", - ".next/e2e-40345/types/**/*.ts", - ".next/e2e-41065/types/**/*.ts", - ".next/e2e-41648/types/**/*.ts", - ".next/e2e-42183/types/**/*.ts", - ".next/e2e-43276/types/**/*.ts", - ".next/e2e-43384/types/**/*.ts", - ".next/e2e-44522/types/**/*.ts", - ".next/e2e-44774/types/**/*.ts", - ".next/e2e-44960/types/**/*.ts", - ".next/e2e-46155/types/**/*.ts", - ".next/e2e-46497/types/**/*.ts", - ".next/e2e-46609/types/**/*.ts", - ".next/e2e-46757/types/**/*.ts", - ".next/e2e-46875/types/**/*.ts", - ".next/e2e-47405/types/**/*.ts", - ".next/e2e-47939/types/**/*.ts", - ".next/e2e-48016/types/**/*.ts", - ".next/e2e-48479/types/**/*.ts", - ".next/e2e-49401/types/**/*.ts", - ".next/e2e-51002/types/**/*.ts", - ".next/e2e-52013/types/**/*.ts", - ".next/e2e-52421/types/**/*.ts", - ".next/e2e-52695/types/**/*.ts", - ".next/e2e-53562/types/**/*.ts", - ".next/e2e-55001/types/**/*.ts", - ".next/e2e-55800/types/**/*.ts", - ".next/e2e-55954/types/**/*.ts", - ".next/e2e-56773/types/**/*.ts", - ".next/e2e-56925/types/**/*.ts", - ".next/e2e-57962/types/**/*.ts", - ".next/e2e-58103/types/**/*.ts", - ".next/e2e-59966/types/**/*.ts", - ".next/e2e-60290/types/**/*.ts", - ".next/e2e-60609/types/**/*.ts", - ".next/e2e-60813/types/**/*.ts", - ".next/e2e-61668/types/**/*.ts", - ".next/e2e-62163/types/**/*.ts", - ".next/e2e-63096/types/**/*.ts", - ".next/e2e-63375/types/**/*.ts", - ".next/e2e-64189/types/**/*.ts", - ".next/e2e-64863/types/**/*.ts", - ".next/e2e-6491/types/**/*.ts", - ".next/e2e-64996/types/**/*.ts", - ".next/e2e-66448/types/**/*.ts", - ".next/e2e-67280/types/**/*.ts", - ".next/e2e-677/types/**/*.ts", - ".next/e2e-67863/types/**/*.ts", - ".next/e2e-6790/types/**/*.ts", - ".next/e2e-68693/types/**/*.ts", - ".next/e2e-69683/types/**/*.ts", - ".next/e2e-70258/types/**/*.ts", - ".next/e2e-70974/types/**/*.ts", - ".next/e2e-72843/types/**/*.ts", - ".next/e2e-73012/types/**/*.ts", - ".next/e2e-7321/types/**/*.ts", - ".next/e2e-74412/types/**/*.ts", - ".next/e2e-75948/types/**/*.ts", - ".next/e2e-76503/types/**/*.ts", - ".next/e2e-76880/types/**/*.ts", - ".next/e2e-78072/types/**/*.ts", - ".next/e2e-78180/types/**/*.ts", - ".next/e2e-78226/types/**/*.ts", - ".next/e2e-78655/types/**/*.ts", - ".next/e2e-79761/types/**/*.ts", - ".next/e2e-8028/types/**/*.ts", - ".next/e2e-80284/types/**/*.ts", - ".next/e2e-80317/types/**/*.ts", - ".next/e2e-81160/types/**/*.ts", - ".next/e2e-81409/types/**/*.ts", - ".next/e2e-8147/types/**/*.ts", - ".next/e2e-81635/types/**/*.ts", - ".next/e2e-81786/types/**/*.ts", - ".next/e2e-81905/types/**/*.ts", - ".next/e2e-83151/types/**/*.ts", - ".next/e2e-83738/types/**/*.ts", - ".next/e2e-84154/types/**/*.ts", - ".next/e2e-84504/types/**/*.ts", - ".next/e2e-84574/types/**/*.ts", - ".next/e2e-85918/types/**/*.ts", - ".next/e2e-8611/types/**/*.ts", - ".next/e2e-86120/types/**/*.ts", - ".next/e2e-86502/types/**/*.ts", - ".next/e2e-87073/types/**/*.ts", - ".next/e2e-87586/types/**/*.ts", - ".next/e2e-87665/types/**/*.ts", - ".next/e2e-88316/types/**/*.ts", - ".next/e2e-88470/types/**/*.ts", - ".next/e2e-89042/types/**/*.ts", - ".next/e2e-89097/types/**/*.ts", - ".next/e2e-89236/types/**/*.ts", - ".next/e2e-89710/types/**/*.ts", - ".next/e2e-90349/types/**/*.ts", - ".next/e2e-90697/types/**/*.ts", - ".next/e2e-91110/types/**/*.ts", - ".next/e2e-91126/types/**/*.ts", - ".next/e2e-92653/types/**/*.ts", - ".next/e2e-92722/types/**/*.ts", - ".next/e2e-93003/types/**/*.ts", - ".next/e2e-93255/types/**/*.ts", - ".next/e2e-93729/types/**/*.ts", - ".next/e2e-9379/types/**/*.ts", - ".next/e2e-94851/types/**/*.ts", - ".next/e2e-94897/types/**/*.ts", - ".next/e2e-95028/types/**/*.ts", - ".next/e2e-95103/types/**/*.ts", - ".next/e2e-95457/types/**/*.ts", - ".next/e2e-96105/types/**/*.ts", - ".next/e2e-96857/types/**/*.ts", - ".next/e2e-96909/types/**/*.ts", - ".next/e2e-97296/types/**/*.ts", - ".next/e2e-97674/types/**/*.ts", - ".next/e2e-9850/types/**/*.ts", - ".next/e2e-99093/types/**/*.ts", - ".next/e2e-99443/types/**/*.ts", - ".next/e2e-99541/types/**/*.ts", - ".next/e2e-9955/types/**/*.ts", - ".next/e2e-99590/types/**/*.ts", - ".next/e2e/types/**/*.ts", - ".next/types/**/*.ts", "next-env.d.ts", - ".next/e2e-12203/types/**/*.ts" + ".next/types/**/*.ts", + ".next/*/types/**/*.ts" ], - "exclude": ["node_modules"] + "exclude": [ + "node_modules" + ] }
Nenhum evento agendado