From 4f0f997c329db8f3e7f9f9e0ac91c9dc259f9f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Wed, 15 Jul 2026 13:52:30 -0300 Subject: [PATCH] Isolate local/dev and tests from the production database Local dev and the test suite previously defaulted to the live production Neon database, and running the tests mutated prod data. Add guardrails plus a proper local Postgres + migrations workflow so production can't be hit by mistake: - docker-compose: local Postgres 17 (matches prod) + auto-created teqo_test - dev guard refuses a non-local DATABASE_URL; tests refuse non-_test databases - .env.test isolates the suite; vitest/playwright load it and fail closed - Payload migrations set up (push disabled); build runs `payload migrate`, so deploys apply pending migrations to prod automatically - baseline `initial` migration + idempotent consent.text varchar->jsonb fix - `pnpm db:pull` copies prod content locally, excluding supporter PII - docs (README, AGENTS.md) + agent skills for the local-db/migration workflow --- .cursor/skills/local-database/SKILL.md | 63 + .cursor/skills/payload-migrations/SKILL.md | 74 + .env.example | 14 +- .env.test | 13 + AGENTS.md | 67 +- README.md | 47 +- docker-compose.yml | 61 +- docker/postgres/init/01-create-test-db.sql | 5 + package.json | 12 +- playwright.config.ts | 23 +- scripts/db-pull.mjs | 145 + scripts/guard-dev-db.mjs | 57 + src/migrations/20260715_163458_initial.json | 3169 +++++++++++++++++ src/migrations/20260715_163458_initial.ts | 415 +++ .../20260715_163500_consent_text_to_jsonb.ts | 43 + src/migrations/index.ts | 15 + src/payload.config.ts | 25 +- tests/helpers/assertTestDatabase.ts | 32 + tests/helpers/seedUser.ts | 4 + vitest.setup.ts | 13 +- 20 files changed, 4210 insertions(+), 87 deletions(-) create mode 100644 .cursor/skills/local-database/SKILL.md create mode 100644 .cursor/skills/payload-migrations/SKILL.md create mode 100644 .env.test create mode 100644 docker/postgres/init/01-create-test-db.sql create mode 100644 scripts/db-pull.mjs create mode 100644 scripts/guard-dev-db.mjs create mode 100644 src/migrations/20260715_163458_initial.json create mode 100644 src/migrations/20260715_163458_initial.ts create mode 100644 src/migrations/20260715_163500_consent_text_to_jsonb.ts create mode 100644 src/migrations/index.ts create mode 100644 tests/helpers/assertTestDatabase.ts diff --git a/.cursor/skills/local-database/SKILL.md b/.cursor/skills/local-database/SKILL.md new file mode 100644 index 00000000..4c022e2f --- /dev/null +++ b/.cursor/skills/local-database/SKILL.md @@ -0,0 +1,63 @@ +--- +name: local-database +description: Set up, refresh, and safely use the Teqo local Postgres database, and copy production content locally with `pnpm db:pull`. Use when starting local development, when the local database is empty or stale, when preparing the test database, or when the user mentions the local DB, docker-compose, db:pull, or seeding local data. Enforces that local development and tests never touch the production Neon database. +--- + +# Teqo Local Database + +Production is a live **Neon Postgres** with real citizens' PII. Local development and tests run against a **local Docker Postgres** and must never connect to production. + +## Guardrails already in place (do not weaken them) + +- `pnpm dev` runs `scripts/guard-dev-db.mjs` and **refuses to start** if `DATABASE_URL` is a non-local host. Escape hatch: `ALLOW_REMOTE_DB=true` (use only with a very good reason). +- Tests load `.env.test` (database `teqo_test`) and call `assertTestDatabase()` (`tests/helpers/assertTestDatabase.ts`), which **throws** unless the database name ends in `_test`. +- Never repoint `DATABASE_URL` in `.env`, `.env.local`, or `.env.test` at Neon. `.env.test` is committed and local-only on purpose. + +## Local connection values + +- Dev database: `postgresql://teqo:teqo@localhost:5432/teqo` +- Test database: `postgresql://teqo:teqo@localhost:5432/teqo_test` +- Local Postgres major version is pinned to **17** in `docker-compose.yml` to match production Neon. + +## First-time setup + +1. Ensure Docker is running (`open -a Docker` on macOS if the daemon is down). +2. `pnpm db:start` — boots local Postgres and auto-creates `teqo_test`. +3. Set `DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo` in `.env.local`. +4. Load schema + data (pick one): + - Mirror prod content (recommended): see "Refresh local content from prod" below. + - Empty schema from migrations: `pnpm migrate`. +5. `pnpm dev`. + +Stop the database with `pnpm db:stop`. + +## Refresh local content from prod (`pnpm db:pull`) + +```bash +PROD_DATABASE_URL="" pnpm db:pull +``` + +- The unpooled URL is `DATABASE_URL_UNPOOLED` in `.env.local`. +- `scripts/db-pull.mjs` only **reads** prod (via `pg_dump`) and only **writes** local; it refuses any non-local target. +- It **excludes PII**: row data of `contact`, `signature`, `subscription` (and their `_rels`) is not copied; the table structure is kept. +- It resets and rebuilds the local `public` schema, so it overwrites local data. + +## Prepare / reset the test database + +The test DB needs the schema applied via migrations: + +```bash +DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test pnpm migrate +``` + +Then run `pnpm test` (or `pnpm test:int` / `pnpm test:e2e`). Playwright injects the test DB into its own dev server and never reuses a running one. + +## Never do + +- Point any `DATABASE_URL` at Neon for dev or tests. +- Run the test suite against a database not ending in `_test`. +- Try to use `db:pull` to push local → prod (it cannot; it is read-only on the source). + +## Related + +- Schema changes and migrations: use the `payload-migrations` skill. diff --git a/.cursor/skills/payload-migrations/SKILL.md b/.cursor/skills/payload-migrations/SKILL.md new file mode 100644 index 00000000..49cdbc3a --- /dev/null +++ b/.cursor/skills/payload-migrations/SKILL.md @@ -0,0 +1,74 @@ +--- +name: payload-migrations +description: Create, apply, and deploy Payload schema migrations for the Teqo project (Postgres, push disabled). Use when changing any collection/global/field config, adding indexes or enums, reconciling schema drift, writing a data migration, or when the user mentions migrations, migrate:create, or applying schema changes to production. Explains how migrations reach the production Neon database on deploy. +--- + +# Teqo Payload Migrations + +Schema is managed exclusively by committed migrations in `src/migrations/`. `push: false` is set in `payload.config.ts` for every environment — never enable it against a remote database. + +## How it deploys + +`pnpm build` runs `payload migrate` before `next build`. On Vercel, the build runs with the production `DATABASE_URL`, so **every deploy applies pending migrations to production automatically**. Do not run `pnpm migrate` against production by hand. + +Production is already baselined at migration `20260715_163458_initial` (marked applied in prod's `payload_migrations`). **Never regenerate, rename, or replace the initial migration** — doing so would make prod try to recreate existing tables. + +## Making a schema change + +1. Make sure the local DB is running and current: `pnpm db:start`, `DATABASE_URL` local, `pnpm migrate`. +2. Edit the collection / global / field config. +3. Generate the migration: + ```bash + pnpm migrate:create + ``` +4. **Review** the generated SQL in `src/migrations/_.ts`. Confirm it does only what you intend (watch for unintended drops/renames). +5. Commit all generated files: the `.ts`, the `.json` snapshot, and the updated `src/migrations/index.ts`. +6. Apply locally and verify: `pnpm migrate` (inspect with `pnpm migrate:status`). +7. If types or the import map are affected: `pnpm generate:types` / `pnpm generate:importmap`. +8. Deploying (merge/deploy) applies it to prod via the build step. + +## Hand-written data / reconciliation migrations + +For changes Payload's diff can't generate (data backfills, type reconciliations), add a file manually and register it in `index.ts`. Make it **idempotent** so it's safe on databases already in the target state. + +Follow the existing example `src/migrations/20260715_163500_consent_text_to_jsonb.ts`: + +```ts +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db }: MigrateUpArgs): Promise { + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( /* guard: only run when not already in target state */ ) THEN + /* ALTER / UPDATE ... */ + END IF; + END $$; + `) +} + +export async function down({ db }: MigrateDownArgs): Promise { + /* reverse, also guarded */ +} +``` + +Then add both `up`/`down` and a `name` entry to `src/migrations/index.ts` (order matters — after the previous migration). + +## Verifying against production (read-only, safe) + +To confirm what a deploy will apply, run status pointed at prod's unpooled URL — this only reads: + +```bash +DATABASE_URL="" pnpm migrate:status +``` + +## Never do + +- Set `push: true`, especially against a remote DB. +- Run `pnpm migrate` manually against production (deploys handle it). +- Edit production schema by hand — make it a migration instead. +- Modify or delete the baseline `20260715_163458_initial` migration. + +## Related + +- Local database setup, `db:pull`, and test DB: use the `local-database` skill. diff --git a/.env.example b/.env.example index 4a8dbaf4..6e780924 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,14 @@ -DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DATABASE +# Copy this file to `.env.local` for local development. +# +# For LOCAL DEVELOPMENT, DATABASE_URL must point at the local Postgres started by +# docker-compose (`pnpm db:start`). `pnpm dev` will REFUSE to start against a +# non-local host (e.g. the production Neon database) unless you explicitly set +# ALLOW_REMOTE_DB=true — this is intentional, to protect production data. +DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo + PAYLOAD_SECRET=YOUR_SECRET_HERE BLOB_READ_WRITE_TOKEN=YOUR_BLOB_TOKEN_HERE -NEXT_PUBLIC_SITE_URL=https://jorgesolla.com.br +NEXT_PUBLIC_SITE_URL=http://localhost:3000 + +# Note: the automated test suite ignores this file and reads .env.test instead, +# and only ever runs against a database whose name ends with "_test". diff --git a/.env.test b/.env.test new file mode 100644 index 00000000..e3414b79 --- /dev/null +++ b/.env.test @@ -0,0 +1,13 @@ +# Environment for the automated test suite (vitest + playwright). +# +# This file is committed ON PURPOSE: it contains no secrets, only local values. +# The test suite deletes and recreates records, so it MUST use an isolated, +# throwaway database. Tests load THIS file (not .env / .env.local) and refuse to +# run unless the database name ends with "_test" (see assertTestDatabase.ts). +# +# Requires the local Postgres from docker-compose (`pnpm db:start`), which +# auto-creates the teqo_test database on first run. +DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test +PAYLOAD_SECRET=test-only-secret-not-used-in-production +BLOB_READ_WRITE_TOKEN= +NEXT_PUBLIC_SITE_URL=http://localhost:3000 diff --git a/AGENTS.md b/AGENTS.md index ac853cf3..9e7fefc2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,40 @@ You are an expert Payload CMS developer. When working with Payload projects, follow these rules: +## Teqo Project Context (read this first) + +Teqo is the digital platform for deputado federal Jorge Solla (PT-BA): public site + editorial CMS + (soon) an internal `/campanha` campaign-management area, all in this one Next.js + Payload app. Full architecture/decision doc lives outside this repo at `plano-arquitetura-campanha-2026.md` (Cowork workspace) — check it for the "why" behind decisions below. + +**Locked decisions — do not relitigate without a new concrete reason:** + +- Single Next.js app, three route groups: `(frontend)` public site, `(payload)` admin, `(campanha)` internal campaign tool (roles: assessores). No separate Rust service, no separate frontend app for `/campanha` — model it as Payload collections (it already has native `point` fields + `near` queries for the political map). +- Hosting stays on Vercel for now. No self-host/Coolify migration in progress. +- Donations are NOT handled in this app — they run through QueroApoiar (`apoiar.me/jorgesolla`, TSE-homologated). The site only needs a "Doar" link/CTA out to that URL. + +**Real conventions already established in this codebase (follow these, not just the generic patterns below):** + +- `Contact` is the normalized "person" record (name, email, phone, state/city via `src/lib/cities.ts`, CEP). Don't create a parallel "person" collection for new features (e.g. líderanças/apoiadores) — add a join collection that relates to `Contact`, the same way `Signature` and `Subscription` do. +- Every action that writes to more than one collection (see `src/app/(frontend)/actions/*.ts`) wraps the writes in a Payload transaction (`payload.db.beginTransaction/commitTransaction/rollbackTransaction`) and passes `req: { transactionID }` to every `payload.create`/`payload.update` call. Follow this pattern for any new multi-collection write. +- LGPD consent is tracked via a `Consent` collection (richText, versioned) referenced by relationship from `Signature`/`Subscription`. Any new form/opt-in flow should link to a `Consent` document the same way — don't invent a new consent mechanism. +- Cache invalidation: collections/globals call `revalidateDocumentById` (`src/utilities/documents.ts`) / `revalidateGlobal` (`src/utilities/globals.ts`) in an `afterChange` hook. Add the same hook to any new collection/global that backs a public page. +- Admin UI is organized via `admin.group` (e.g. `'Abaixo-assinados'`, `'Contatos'`, `'Configurações'`, `'Paginas'`) — group new collections consistently instead of leaving them ungrouped. +- i18n: Payload admin defaults to `pt` (not `pt-BR` — see `payload.config.ts`, `payload/i18n/pt`). + +**Database & local development (safety-critical — production is a live Neon Postgres with real citizens' PII):** + +- Local dev and tests must NEVER touch production. Local dev runs against a local Docker Postgres (`pnpm db:start`); `pnpm dev` refuses a non-local `DATABASE_URL` (guard: `scripts/guard-dev-db.mjs`) unless `ALLOW_REMOTE_DB=true`. Tests load `.env.test` (`teqo_test`) and refuse any database whose name doesn't end in `_test` (`tests/helpers/assertTestDatabase.ts`). Never repoint `.env`/`.env.local`/`.env.test` `DATABASE_URL` at Neon. Full workflow lives in the `local-database` skill (`.cursor/skills/local-database`). +- Schema changes go through committed Payload migrations — `push: false` everywhere, never flip it on against a remote DB. Edit configs → `pnpm migrate:create ` → commit `src/migrations/*` (both `.ts` and `.json`, plus `index.ts`) → `pnpm migrate` locally. `pnpm build` runs `payload migrate`, so **every Vercel deploy applies pending migrations to prod**. Prod is baselined at migration `20260715_163458_initial` — never regenerate or replace the initial migration. Full workflow (incl. hand-written data/reconciliation migrations) lives in the `payload-migrations` skill (`.cursor/skills/payload-migrations`). +- `Consent.text` was reconciled from `varchar` to `jsonb` via `20260715_163500_consent_text_to_jsonb` (matching the `richText` field). Any future field-type change is a migration, not a manual DB edit. +- To copy production content locally, use `pnpm db:pull` (`scripts/db-pull.mjs`): it only reads prod, only writes local, and excludes supporter PII (`contact`/`signature`/`subscription` row data). + +## Known Gaps (as of 2026-07-15 — resolve before relying on this file for onboarding new devs) + +1. **`Users` has no `roles` field yet.** The RBAC examples below are generic Payload patterns, not yet implemented here — every admin user today has full access. Add `roles` (e.g. `admin`/`editor`/`assessor`) before opening the admin to more people or building `/campanha`. +2. **`Consent` document IDs are hardcoded** in code (e.g. `consent: 2` in `submitWhatsapp.ts`). Fragile — breaks silently if that document is ever recreated with a different ID, and blocks having a reproducible local seed. Consider referencing by a stable slug/key instead. (The column-type drift is fixed; the hardcoded-ID fragility remains.) +3. **No `Pages`/`Posts` collections yet** for institutional content (bio, propostas, notícias/imprensa). `HomePage` global today only has a single image field — most homepage content is still hardcoded in `src/app/(frontend)/(home)/page.tsx`. This is the biggest gap for reproducing jorgesolla.com.br content in Payload. + +**Recently resolved (2026-07-15):** local Postgres now runs via `docker-compose.yml` (Postgres 17, matching prod, auto-creating `teqo_test`); Payload migrations are set up with prod baselined; dev/test database guards prevent accidental production access. See the two database skills. + ## Core Principles 1. **TypeScript-First**: Always use TypeScript with proper types from Payload @@ -15,19 +49,23 @@ You are an expert Payload CMS developer. When working with Payload projects, fol - To validate typescript correctness after modifying code run `tsc --noEmit` - Generate import maps after creating or modifying components. +- Any change to a collection/global/field schema requires a migration: `pnpm migrate:create `, then `pnpm migrate` locally. Never rely on `push` (it is `false`). See the `payload-migrations` skill. ## Project Structure +Generic Payload layout (aspirational — `access/` and `hooks/` as separate dirs don't exist yet in this repo; access control and hooks are currently written inline inside each collection/global file): + ``` src/ ├── app/ -│ ├── (frontend)/ # Frontend routes -│ └── (payload)/ # Payload admin routes -├── collections/ # Collection configs -├── globals/ # Global configs -├── components/ # Custom React components -├── hooks/ # Hook functions -├── access/ # Access control functions +│ ├── (frontend)/ # Frontend routes + server actions (src/app/(frontend)/actions/*.ts) +│ ├── (payload)/ # Payload admin routes +│ └── (campanha)/ # NOT YET CREATED — planned internal campaign-management area +├── collections/ # Collection configs (Users, Media, Petition, Signature, Consent, Contact, Subscription) +├── globals/ # Global configs (SiteSettings, HomePage, Metadata) +├── components/ # Custom React components (incl. components/ui from shadcn) +├── lib/ # cities.ts, zod schemas (lib/schemas/*) +├── utilities/ # documents.ts / globals.ts (revalidation helpers) └── payload.config.ts # Main config ``` @@ -725,7 +763,6 @@ export const Posts: CollectionConfig = { ### Performance Best Practices 1. **Import correctly:** - - Admin Panel: `import { Button } from '@payloadcms/ui'` - Frontend: `import { Button } from '@payloadcms/ui/elements/Button'` @@ -740,7 +777,6 @@ export const Posts: CollectionConfig = { ``` 3. **Prefer Server Components** - Only use Client Components when you need: - - State (useState, useReducer) - Effects (useEffect) - Event handlers (onClick, onChange) @@ -1040,19 +1076,16 @@ For deeper exploration of specific topics, refer to the context files located in ### Available Context Files 1. **`payload-overview.md`** - High-level architecture and core concepts - - Payload structure and initialization - Configuration fundamentals - Database adapters overview 2. **`security-critical.md`** - Critical security patterns (⚠️ IMPORTANT) - - Local API access control - Transaction safety in hooks - Preventing infinite hook loops 3. **`collections.md`** - Collection configurations - - Basic collection patterns - Auth collections with RBAC - Upload collections @@ -1060,7 +1093,6 @@ For deeper exploration of specific topics, refer to the context files located in - Globals 4. **`fields.md`** - Field types and patterns - - All field types with examples - Conditional fields - Virtual fields @@ -1068,13 +1100,11 @@ For deeper exploration of specific topics, refer to the context files located in - Common field patterns 5. **`field-type-guards.md`** - TypeScript field type utilities - - Field type checking utilities - Safe type narrowing - Runtime field validation 6. **`access-control.md`** - Permission patterns - - Collection-level access - Field-level access - Row-level security @@ -1082,48 +1112,41 @@ For deeper exploration of specific topics, refer to the context files located in - Multi-tenant access control 7. **`access-control-advanced.md`** - Complex access patterns - - Nested document access - Cross-collection permissions - Dynamic role hierarchies - Performance optimization 8. **`hooks.md`** - Lifecycle hooks - - Collection hooks - Field hooks - Hook context patterns - Common hook recipes 9. **`queries.md`** - Database operations - - Local API usage - Query operators - Complex queries with AND/OR - Performance optimization 10. **`endpoints.md`** - Custom API endpoints - - REST endpoint patterns - Authentication in endpoints - Error handling - Route parameters 11. **`adapters.md`** - Database and storage adapters - - MongoDB, PostgreSQL, SQLite patterns - Storage adapter usage (S3, Azure, GCS, etc.) - Custom adapter development 12. **`plugin-development.md`** - Creating plugins - - Plugin architecture - Modifying configuration - Plugin hooks - Best practices 13. **`components.md`** - Custom Components - - Component types (Root, Collection, Global, Field) - Server vs Client Components - Component paths and definition diff --git a/README.md b/README.md index 7c133e25..31a5cfe5 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,49 @@ Teqo then becomes a configurable base product for other politicians in Brazil: ## Local Development -1. Copy environment variables: `cp .env.example .env` -2. Install dependencies: `pnpm install` -3. Start development server: `pnpm dev` -4. Open: `http://localhost:3000` +> **Production safety:** the production database is a live Neon Postgres holding real citizens' data. Local development and tests run against a **local** Postgres and must never point at production. `pnpm dev` refuses to start against a non-local database, and the test suite refuses any database whose name doesn't end in `_test`. + +### First-time setup + +1. Install dependencies: `pnpm install` +2. Create your env file: `cp .env.example .env.local` +3. Start Docker, then start the local database: `pnpm db:start` + - This runs Postgres 17 (matching production) and auto-creates a `teqo_test` database for the test suite. +4. Make sure `DATABASE_URL` in `.env.local` points at local: + ``` + DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo + ``` +5. Load a schema + content into the local database — pick one: + - **Mirror production content (recommended):** `PROD_DATABASE_URL="" pnpm db:pull` + Copies production's schema and content into local. It only reads production and **excludes supporter PII** (`contact`/`signature`/`subscription` data). + - **Empty schema from migrations:** `pnpm migrate` +6. Start the dev server: `pnpm dev` +7. Open: `http://localhost:3000` + +Useful scripts: `pnpm db:start` / `pnpm db:stop` (local Postgres), `pnpm db:pull` (refresh local content from prod). + +### Database migrations + +Schema is managed by committed Payload migrations (`src/migrations/`); `push` is disabled everywhere. + +To change the schema: + +1. Edit the collection/global/field config. +2. Generate a migration: `pnpm migrate:create ` +3. Review and commit the generated files in `src/migrations/` (both `.ts` and `.json`, plus `index.ts`). +4. Apply it locally: `pnpm migrate` (check status anytime with `pnpm migrate:status`). + +**Deploying to production:** `pnpm build` runs `payload migrate` before building, so every Vercel deploy automatically applies pending migrations to the production database. Do not run migrations against production by hand. + +### Running tests + +Tests use the isolated `teqo_test` database (config in `.env.test`). Prepare its schema once with: + +``` +DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test pnpm migrate +``` + +Then run `pnpm test` (or `pnpm test:int` / `pnpm test:e2e`). ## Tech Stack diff --git a/docker-compose.yml b/docker-compose.yml index a93bd399..204478df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,43 +1,28 @@ -version: '3' - services: - payload: - image: node:18-alpine + # Local development database. This is the ONLY database local dev and the + # test suite should ever touch — never point DATABASE_URL at the production + # Neon instance for local work (see the guard in scripts/guard-dev-db.mjs). + postgres: + # Pinned to match the production Neon major version (17), so pg_dump/restore + # from prod (see `pnpm db:pull`) works without version mismatches. + image: postgres:17-alpine + restart: unless-stopped + environment: + POSTGRES_USER: teqo + POSTGRES_PASSWORD: teqo + POSTGRES_DB: teqo ports: - - '3000:3000' + - '5432:5432' volumes: - - .:/home/node/app - - node_modules:/home/node/app/node_modules - working_dir: /home/node/app/ - command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev" - depends_on: - - mongo - # - postgres - env_file: - - .env - - # Ensure your DATABASE_URL uses 'mongo' as the hostname ie. mongodb://mongo/my-db-name - mongo: - image: mongo:latest - ports: - - '27017:27017' - command: - - --storageEngine=wiredTiger - volumes: - - data:/data/db - logging: - driver: none - - # Uncomment the following to use postgres - # postgres: - # restart: always - # image: postgres:latest - # volumes: - # - pgdata:/var/lib/postgresql/data - # ports: - # - "5432:5432" + - pgdata:/var/lib/postgresql/data + # Runs only on first initialization of the volume. Creates the dedicated + # teqo_test database so automated tests never share state with dev data. + - ./docker/postgres/init:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U teqo -d teqo'] + interval: 5s + timeout: 5s + retries: 10 volumes: - data: - # pgdata: - node_modules: + pgdata: diff --git a/docker/postgres/init/01-create-test-db.sql b/docker/postgres/init/01-create-test-db.sql new file mode 100644 index 00000000..be7409af --- /dev/null +++ b/docker/postgres/init/01-create-test-db.sql @@ -0,0 +1,5 @@ +-- Runs once, on first initialization of the Postgres data volume. +-- Creates the dedicated database used by the automated test suite so that +-- tests (which delete and recreate records) never share state with local +-- development data — and can never be pointed at production. +CREATE DATABASE teqo_test; diff --git a/package.json b/package.json index 9c9fd15b..3e6c95ed 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,17 @@ "license": "MIT", "type": "module", "scripts": { - "build": "cross-env NODE_OPTIONS=\"--no-deprecation --max-old-space-size=8000\" next build", - "dev": "cross-env NODE_OPTIONS=--no-deprecation next dev", - "devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev", + "build": "cross-env NODE_OPTIONS=--no-deprecation payload migrate && cross-env NODE_OPTIONS=\"--no-deprecation --max-old-space-size=8000\" next build", + "db:start": "docker compose up -d postgres", + "db:stop": "docker compose down", + "db:pull": "node scripts/db-pull.mjs", + "dev": "node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev", + "devsafe": "rm -rf .next && node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev", "generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap", "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types", + "migrate": "cross-env NODE_OPTIONS=--no-deprecation payload migrate", + "migrate:create": "cross-env NODE_OPTIONS=--no-deprecation payload migrate:create", + "migrate:status": "cross-env NODE_OPTIONS=--no-deprecation payload migrate:status", "lint": "cross-env NODE_OPTIONS=--no-deprecation eslint --cache --cache-location .next/cache/eslint/", "lint:fix": "cross-env NODE_OPTIONS=--no-deprecation eslint --fix", "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", diff --git a/playwright.config.ts b/playwright.config.ts index c60fa9dc..501d3ee4 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,10 +1,15 @@ import { defineConfig, devices } from '@playwright/test' +import { config as loadEnv } from 'dotenv' + +import { assertTestDatabase } from './tests/helpers/assertTestDatabase' /** - * Read environment variables from file. - * https://github.com/motdotla/dotenv + * e2e tests boot a real dev server that seeds/deletes records, so they must run + * against the isolated test database — never .env / .env.local (production). + * `override: true` guarantees the test DB wins over anything in the shell. */ -import 'dotenv/config' +loadEnv({ path: '.env.test', override: true }) +assertTestDatabase(process.env.DATABASE_URL) /** * See https://playwright.dev/docs/test-configuration. @@ -35,7 +40,17 @@ export default defineConfig({ ], webServer: { command: 'pnpm dev', - reuseExistingServer: true, + /* + * Never reuse a dev server a developer may already have running against the + * production database — always start a fresh one bound to the test DB. + */ + reuseExistingServer: false, url: 'http://localhost:3000', + /* Force the isolated test database into the server process. */ + env: { + DATABASE_URL: process.env.DATABASE_URL as string, + PAYLOAD_SECRET: process.env.PAYLOAD_SECRET ?? 'test-only-secret-not-used-in-production', + NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000', + }, }, }) diff --git a/scripts/db-pull.mjs b/scripts/db-pull.mjs new file mode 100644 index 00000000..6158ed63 --- /dev/null +++ b/scripts/db-pull.mjs @@ -0,0 +1,145 @@ +/** + * Pulls a CONTENT-ONLY copy of the production database into the local Postgres. + * + * Safety model: + * - Source (production) is only ever READ, via `pg_dump`. This script issues + * no writes against production and cannot: it never even opens a writable + * connection to the source. + * - Target must be a LOCAL host (same guard as the dev server). The script + * refuses to restore into anything that isn't localhost. + * - PII is excluded: the data of the `contact`, `signature` and `subscription` + * tables (real supporters' names, e-mails, phones, CEP) is NOT copied. Their + * table structure is kept so the app still works; only the rows are omitted. + * + * Usage: + * PROD_DATABASE_URL=postgres://... pnpm db:pull + * + * PROD_DATABASE_URL should be the UNPOOLED Neon connection string (the + * DATABASE_URL_UNPOOLED value in .env.local). + */ +import { execFileSync } from 'node:child_process' + +const PG_IMAGE = 'postgres:17-alpine' // must match the prod major version +const PII_TABLE_REGEX = '^(contact|signature|subscription)(_|$)' +const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1']) + +const prodUrl = process.env.PROD_DATABASE_URL +const localUrl = process.env.LOCAL_DATABASE_URL ?? 'postgresql://teqo:teqo@localhost:5432/teqo' + +const die = (message) => { + console.error(`\n[db:pull] ${message}\n`) + process.exit(1) +} + +if (!prodUrl) { + die( + 'PROD_DATABASE_URL is not set.\n' + + 'Run it with your UNPOOLED Neon URL (DATABASE_URL_UNPOOLED in .env.local):\n' + + ' PROD_DATABASE_URL="postgres://...neon.tech/neondb?sslmode=require" pnpm db:pull', + ) +} + +let targetHost +try { + targetHost = new URL(localUrl).hostname +} catch { + die('LOCAL_DATABASE_URL is not a valid connection string.') +} +if (!LOCAL_HOSTS.has(targetHost)) { + die(`Refusing to restore into non-local host "${targetHost}". This script only writes to local.`) +} + +// The local Postgres runs as the docker-compose "postgres" service. We run all +// restore commands inside that container so we don't depend on a local psql. +const containerId = execFileSync('docker', ['compose', 'ps', '-q', 'postgres'], { + encoding: 'utf8', +}) + .trim() + .split('\n')[0] + +if (!containerId) { + die('Local Postgres container is not running. Start it with `pnpm db:start` first.') +} + +const targetUrl = new URL(localUrl) +const targetDb = targetUrl.pathname.replace(/^\//, '') + +const runInPgImage = (shellCommand) => + execFileSync( + 'docker', + ['run', '--rm', '-e', `PGURL=${prodUrl}`, PG_IMAGE, 'sh', '-c', shellCommand], + { + encoding: 'utf8', + maxBuffer: 1024 * 1024 * 512, + }, + ) + +console.log(`[db:pull] Source (read-only): ${new URL(prodUrl).hostname}`) +console.log(`[db:pull] Target (local): ${targetHost}/${targetDb}`) + +// 1. Discover PII tables to exclude (contact/signature/subscription and their +// relationship/locale tables), directly from the production catalog. +console.log('[db:pull] Discovering PII tables to exclude...') +const piiTables = runInPgImage( + `psql "$PGURL" -tAc "SELECT table_name FROM information_schema.tables ` + + `WHERE table_schema='public' AND table_name ~ '${PII_TABLE_REGEX}' ORDER BY table_name;"`, +) + .trim() + .split('\n') + .filter(Boolean) + +console.log(`[db:pull] Excluding data from: ${piiTables.join(', ') || '(none found)'}`) + +const excludeArgs = [ + ...piiTables.map((t) => `--exclude-table-data='public.${t}'`), + // Never carry prod's migration tracking into local. + `--exclude-table='public.payload_migrations'`, + `--exclude-table='public.payload_migrations_id_seq'`, +].join(' ') + +// 2. Dump prod (schema for everything + data for non-PII tables), plain SQL. +console.log('[db:pull] Dumping production (this reads prod, never writes)...') +const dumpSql = runInPgImage(`pg_dump "$PGURL" --no-owner --no-privileges ${excludeArgs}`) + +// 3. Reset the local schema and restore into it. +console.log('[db:pull] Resetting local schema...') +execFileSync( + 'docker', + [ + 'exec', + '-i', + containerId, + 'psql', + '-U', + 'teqo', + '-d', + targetDb, + '-v', + 'ON_ERROR_STOP=1', + '-c', + 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;', + ], + { stdio: ['ignore', 'inherit', 'inherit'] }, +) + +console.log('[db:pull] Restoring into local...') +execFileSync( + 'docker', + [ + 'exec', + '-i', + containerId, + 'psql', + '-U', + 'teqo', + '-d', + targetDb, + '-v', + 'ON_ERROR_STOP=1', + '-f', + '-', + ], + { input: dumpSql, stdio: ['pipe', 'inherit', 'inherit'], maxBuffer: 1024 * 1024 * 512 }, +) + +console.log('\n[db:pull] Done. Local now mirrors production content (without supporter PII).') diff --git a/scripts/guard-dev-db.mjs b/scripts/guard-dev-db.mjs new file mode 100644 index 00000000..11decf64 --- /dev/null +++ b/scripts/guard-dev-db.mjs @@ -0,0 +1,57 @@ +/** + * Refuses to start the local dev server against a non-local database. + * + * Local development must use the Postgres from docker-compose. Pointing dev at + * the production Neon database (as .env / .env.local currently do) risks editing + * or deleting real data through the admin panel. This guard fails closed. + * + * Escape hatch (use with care): ALLOW_REMOTE_DB=true pnpm dev + */ +import { config as loadEnv } from 'dotenv' + +const OVERRIDE_FLAG = 'ALLOW_REMOTE_DB' +const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres']) + +// Mirror Next.js precedence (.env.local wins over .env) WITHOUT overriding a +// DATABASE_URL that is already set in the real environment (e.g. injected by +// Playwright's webServer). dotenv's default `override: false` guarantees this. +loadEnv({ path: '.env.local' }) +loadEnv({ path: '.env' }) + +const fail = (message) => { + console.error(`\n[guard-dev-db] ${message}\n`) + process.exit(1) +} + +const databaseUrl = process.env.DATABASE_URL + +if (!databaseUrl) { + fail('DATABASE_URL is not set. Refusing to start the dev server.') +} + +if (process.env[OVERRIDE_FLAG] === 'true' || process.env[OVERRIDE_FLAG] === '1') { + console.warn( + `\n[guard-dev-db] ${OVERRIDE_FLAG} is set — connecting to a remote database on purpose. Be careful.\n`, + ) + process.exit(0) +} + +let host +try { + host = new URL(databaseUrl).hostname +} catch { + fail('DATABASE_URL is not a valid connection string.') +} + +if (!LOCAL_HOSTS.has(host)) { + fail( + `DATABASE_URL points at a non-local host ("${host}").\n` + + 'This is almost certainly the production or a shared database.\n\n' + + 'Local development should use the local Postgres from docker-compose:\n' + + ' 1. pnpm db:start\n' + + ' 2. set DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo in .env.local\n\n' + + `If you REALLY mean to connect to a remote database, re-run with:\n ${OVERRIDE_FLAG}=true pnpm dev`, + ) +} + +console.log(`[guard-dev-db] OK — using local database host "${host}".`) diff --git a/src/migrations/20260715_163458_initial.json b/src/migrations/20260715_163458_initial.json new file mode 100644 index 00000000..b805a1c3 --- /dev/null +++ b/src/migrations/20260715_163458_initial.json @@ -0,0 +1,3169 @@ +{ + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users_sessions": { + "name": "users_sessions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "users_sessions_order_idx": { + "name": "users_sessions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_sessions_parent_id_idx": { + "name": "users_sessions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_sessions_parent_id_fk": { + "name": "users_sessions_parent_id_fk", + "tableFrom": "users_sessions", + "tableTo": "users", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "reset_password_token": { + "name": "reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_expiration": { + "name": "reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "login_attempts": { + "name": "login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "lock_until": { + "name": "lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_updated_at_idx": { + "name": "users_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_created_at_idx": { + "name": "users_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "alt": { + "name": "alt", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "media_updated_at_idx": { + "name": "media_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_created_at_idx": { + "name": "media_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_filename_idx": { + "name": "media_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.petition": { + "name": "petition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "body": { + "name": "body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "form_title": { + "name": "form_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "form_subtitle": { + "name": "form_subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "form_consent_id": { + "name": "form_consent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "_status": { + "name": "_status", + "type": "enum_petition_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + } + }, + "indexes": { + "petition_form_form_consent_idx": { + "name": "petition_form_form_consent_idx", + "columns": [ + { + "expression": "form_consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "petition_updated_at_idx": { + "name": "petition_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "petition_created_at_idx": { + "name": "petition_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "petition__status_idx": { + "name": "petition__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "petition_form_consent_id_consent_id_fk": { + "name": "petition_form_consent_id_consent_id_fk", + "tableFrom": "petition", + "tableTo": "consent", + "columnsFrom": [ + "form_consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._petition_v": { + "name": "_petition_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_subtitle": { + "name": "version_subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_enabled": { + "name": "version_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_body": { + "name": "version_body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version_form_title": { + "name": "version_form_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_form_subtitle": { + "name": "version_form_subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_form_consent_id": { + "name": "version_form_consent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__petition_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_petition_v_parent_idx": { + "name": "_petition_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_version_form_version_form_consent_idx": { + "name": "_petition_v_version_form_version_form_consent_idx", + "columns": [ + { + "expression": "version_form_consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_version_version_updated_at_idx": { + "name": "_petition_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_version_version_created_at_idx": { + "name": "_petition_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_version_version__status_idx": { + "name": "_petition_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_created_at_idx": { + "name": "_petition_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_updated_at_idx": { + "name": "_petition_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_latest_idx": { + "name": "_petition_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_petition_v_parent_id_petition_id_fk": { + "name": "_petition_v_parent_id_petition_id_fk", + "tableFrom": "_petition_v", + "tableTo": "petition", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_petition_v_version_form_consent_id_consent_id_fk": { + "name": "_petition_v_version_form_consent_id_consent_id_fk", + "tableFrom": "_petition_v", + "tableTo": "consent", + "columnsFrom": [ + "version_form_consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact": { + "name": "contact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "enum_contact_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "postal_code": { + "name": "postal_code", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_updated_at_idx": { + "name": "contact_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_created_at_idx": { + "name": "contact_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consent": { + "name": "consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "text": { + "name": "text", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "consent_updated_at_idx": { + "name": "consent_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "consent_created_at_idx": { + "name": "consent_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signature": { + "name": "signature", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "petition_id": { + "name": "petition_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "signature_contact_idx": { + "name": "signature_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_petition_idx": { + "name": "signature_petition_idx", + "columns": [ + { + "expression": "petition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_consent_idx": { + "name": "signature_consent_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_updated_at_idx": { + "name": "signature_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_created_at_idx": { + "name": "signature_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signature_contact_id_contact_id_fk": { + "name": "signature_contact_id_contact_id_fk", + "tableFrom": "signature", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "signature_petition_id_petition_id_fk": { + "name": "signature_petition_id_petition_id_fk", + "tableFrom": "signature", + "tableTo": "petition", + "columnsFrom": [ + "petition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "signature_consent_id_consent_id_fk": { + "name": "signature_consent_id_consent_id_fk", + "tableFrom": "signature", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subscription_contact_idx": { + "name": "subscription_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_consent_idx": { + "name": "subscription_consent_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_updated_at_idx": { + "name": "subscription_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_created_at_idx": { + "name": "subscription_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_contact_id_contact_id_fk": { + "name": "subscription_contact_id_contact_id_fk", + "tableFrom": "subscription", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "subscription_consent_id_consent_id_fk": { + "name": "subscription_consent_id_consent_id_fk", + "tableFrom": "subscription", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_kv": { + "name": "payload_kv", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payload_kv_key_idx": { + "name": "payload_kv_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_jobs_log": { + "name": "payload_jobs_log", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "task_slug": { + "name": "task_slug", + "type": "enum_payload_jobs_log_task_slug", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "task_i_d": { + "name": "task_i_d", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "enum_payload_jobs_log_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_jobs_log_order_idx": { + "name": "payload_jobs_log_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_log_parent_id_idx": { + "name": "payload_jobs_log_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_jobs_log_parent_id_fk": { + "name": "payload_jobs_log_parent_id_fk", + "tableFrom": "payload_jobs_log", + "tableTo": "payload_jobs", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_jobs": { + "name": "payload_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "total_tried": { + "name": "total_tried", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "task_slug": { + "name": "task_slug", + "type": "enum_payload_jobs_task_slug", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "queue": { + "name": "queue", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "wait_until": { + "name": "wait_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "processing": { + "name": "processing", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_jobs_completed_at_idx": { + "name": "payload_jobs_completed_at_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_total_tried_idx": { + "name": "payload_jobs_total_tried_idx", + "columns": [ + { + "expression": "total_tried", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_has_error_idx": { + "name": "payload_jobs_has_error_idx", + "columns": [ + { + "expression": "has_error", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_task_slug_idx": { + "name": "payload_jobs_task_slug_idx", + "columns": [ + { + "expression": "task_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_queue_idx": { + "name": "payload_jobs_queue_idx", + "columns": [ + { + "expression": "queue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_wait_until_idx": { + "name": "payload_jobs_wait_until_idx", + "columns": [ + { + "expression": "wait_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_processing_idx": { + "name": "payload_jobs_processing_idx", + "columns": [ + { + "expression": "processing", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_updated_at_idx": { + "name": "payload_jobs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_created_at_idx": { + "name": "payload_jobs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents": { + "name": "payload_locked_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "global_slug": { + "name": "global_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_locked_documents_global_slug_idx": { + "name": "payload_locked_documents_global_slug_idx", + "columns": [ + { + "expression": "global_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_updated_at_idx": { + "name": "payload_locked_documents_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_created_at_idx": { + "name": "payload_locked_documents_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents_rels": { + "name": "payload_locked_documents_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "petition_id": { + "name": "petition_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signature_id": { + "name": "signature_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_locked_documents_rels_order_idx": { + "name": "payload_locked_documents_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_parent_idx": { + "name": "payload_locked_documents_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_path_idx": { + "name": "payload_locked_documents_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_users_id_idx": { + "name": "payload_locked_documents_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_media_id_idx": { + "name": "payload_locked_documents_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_petition_id_idx": { + "name": "payload_locked_documents_rels_petition_id_idx", + "columns": [ + { + "expression": "petition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_contact_id_idx": { + "name": "payload_locked_documents_rels_contact_id_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_consent_id_idx": { + "name": "payload_locked_documents_rels_consent_id_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_signature_id_idx": { + "name": "payload_locked_documents_rels_signature_id_idx", + "columns": [ + { + "expression": "signature_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_subscription_id_idx": { + "name": "payload_locked_documents_rels_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_locked_documents_rels_parent_fk": { + "name": "payload_locked_documents_rels_parent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_locked_documents", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_users_fk": { + "name": "payload_locked_documents_rels_users_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_media_fk": { + "name": "payload_locked_documents_rels_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_petition_fk": { + "name": "payload_locked_documents_rels_petition_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "petition", + "columnsFrom": [ + "petition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_contact_fk": { + "name": "payload_locked_documents_rels_contact_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_consent_fk": { + "name": "payload_locked_documents_rels_consent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_signature_fk": { + "name": "payload_locked_documents_rels_signature_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "signature", + "columnsFrom": [ + "signature_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_subscription_fk": { + "name": "payload_locked_documents_rels_subscription_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "subscription", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences": { + "name": "payload_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_preferences_key_idx": { + "name": "payload_preferences_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_updated_at_idx": { + "name": "payload_preferences_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_created_at_idx": { + "name": "payload_preferences_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences_rels": { + "name": "payload_preferences_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_preferences_rels_order_idx": { + "name": "payload_preferences_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_parent_idx": { + "name": "payload_preferences_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_path_idx": { + "name": "payload_preferences_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_users_id_idx": { + "name": "payload_preferences_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_preferences_rels_parent_fk": { + "name": "payload_preferences_rels_parent_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "payload_preferences", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_users_fk": { + "name": "payload_preferences_rels_users_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_migrations": { + "name": "payload_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_migrations_updated_at_idx": { + "name": "payload_migrations_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_migrations_created_at_idx": { + "name": "payload_migrations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_settings_social_links": { + "name": "site_settings_social_links", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "enum_site_settings_social_links_platform", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "site_settings_social_links_order_idx": { + "name": "site_settings_social_links_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "site_settings_social_links_parent_id_idx": { + "name": "site_settings_social_links_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "site_settings_social_links_parent_id_fk": { + "name": "site_settings_social_links_parent_id_fk", + "tableFrom": "site_settings_social_links", + "tableTo": "site_settings", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_settings": { + "name": "site_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "header_title": { + "name": "header_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "_status": { + "name": "_status", + "type": "enum_site_settings_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "site_settings__status_idx": { + "name": "site_settings__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._site_settings_v_version_social_links": { + "name": "_site_settings_v_version_social_links", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "enum__site_settings_v_version_social_links_platform", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "_uuid": { + "name": "_uuid", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_site_settings_v_version_social_links_order_idx": { + "name": "_site_settings_v_version_social_links_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_version_social_links_parent_id_idx": { + "name": "_site_settings_v_version_social_links_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_site_settings_v_version_social_links_parent_id_fk": { + "name": "_site_settings_v_version_social_links_parent_id_fk", + "tableFrom": "_site_settings_v_version_social_links", + "tableTo": "_site_settings_v", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._site_settings_v": { + "name": "_site_settings_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "version_header_title": { + "name": "version_header_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__site_settings_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "autosave": { + "name": "autosave", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_site_settings_v_version_version__status_idx": { + "name": "_site_settings_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_created_at_idx": { + "name": "_site_settings_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_updated_at_idx": { + "name": "_site_settings_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_latest_idx": { + "name": "_site_settings_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_autosave_idx": { + "name": "_site_settings_v_autosave_idx", + "columns": [ + { + "expression": "autosave", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.home": { + "name": "home", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "home_image_idx": { + "name": "home_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "home_image_id_media_id_fk": { + "name": "home_image_id_media_id_fk", + "tableFrom": "home", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metadata_keywords": { + "name": "metadata_keywords", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "metadata_keywords_order_idx": { + "name": "metadata_keywords_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "metadata_keywords_parent_id_idx": { + "name": "metadata_keywords_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "metadata_keywords_parent_id_fk": { + "name": "metadata_keywords_parent_id_fk", + "tableFrom": "metadata_keywords", + "tableTo": "metadata", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metadata": { + "name": "metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "open_graph_site_name": { + "name": "open_graph_site_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "open_graph_description": { + "name": "open_graph_description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "twitter_creator": { + "name": "twitter_creator", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "twitter_description": { + "name": "twitter_description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "metadata_image_idx": { + "name": "metadata_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "metadata_image_id_media_id_fk": { + "name": "metadata_image_id_media_id_fk", + "tableFrom": "metadata", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.enum_petition_status": { + "name": "enum_petition_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__petition_v_version_status": { + "name": "enum__petition_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_contact_state": { + "name": "enum_contact_state", + "schema": "public", + "values": [ + "AC", + "AL", + "AM", + "AP", + "BA", + "CE", + "DF", + "ES", + "GO", + "MA", + "MG", + "MS", + "MT", + "PA", + "PB", + "PE", + "PI", + "PR", + "RJ", + "RN", + "RO", + "RR", + "RS", + "SC", + "SE", + "SP", + "TO" + ] + }, + "public.enum_payload_jobs_log_task_slug": { + "name": "enum_payload_jobs_log_task_slug", + "schema": "public", + "values": [ + "inline", + "schedulePublish" + ] + }, + "public.enum_payload_jobs_log_state": { + "name": "enum_payload_jobs_log_state", + "schema": "public", + "values": [ + "failed", + "succeeded" + ] + }, + "public.enum_payload_jobs_task_slug": { + "name": "enum_payload_jobs_task_slug", + "schema": "public", + "values": [ + "inline", + "schedulePublish" + ] + }, + "public.enum_site_settings_social_links_platform": { + "name": "enum_site_settings_social_links_platform", + "schema": "public", + "values": [ + "instagram", + "facebook", + "youtube", + "whatsapp" + ] + }, + "public.enum_site_settings_status": { + "name": "enum_site_settings_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__site_settings_v_version_social_links_platform": { + "name": "enum__site_settings_v_version_social_links_platform", + "schema": "public", + "values": [ + "instagram", + "facebook", + "youtube", + "whatsapp" + ] + }, + "public.enum__site_settings_v_version_status": { + "name": "enum__site_settings_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "id": "fe900030-6d91-48eb-ba03-99873a6c5c30", + "prevId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/src/migrations/20260715_163458_initial.ts b/src/migrations/20260715_163458_initial.ts new file mode 100644 index 00000000..c375ba51 --- /dev/null +++ b/src/migrations/20260715_163458_initial.ts @@ -0,0 +1,415 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + CREATE TYPE "public"."enum_petition_status" AS ENUM('draft', 'published'); + CREATE TYPE "public"."enum__petition_v_version_status" AS ENUM('draft', 'published'); + CREATE TYPE "public"."enum_contact_state" AS ENUM('AC', 'AL', 'AM', 'AP', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MG', 'MS', 'MT', 'PA', 'PB', 'PE', 'PI', 'PR', 'RJ', 'RN', 'RO', 'RR', 'RS', 'SC', 'SE', 'SP', 'TO'); + CREATE TYPE "public"."enum_payload_jobs_log_task_slug" AS ENUM('inline', 'schedulePublish'); + CREATE TYPE "public"."enum_payload_jobs_log_state" AS ENUM('failed', 'succeeded'); + CREATE TYPE "public"."enum_payload_jobs_task_slug" AS ENUM('inline', 'schedulePublish'); + CREATE TYPE "public"."enum_site_settings_social_links_platform" AS ENUM('instagram', 'facebook', 'youtube', 'whatsapp'); + CREATE TYPE "public"."enum_site_settings_status" AS ENUM('draft', 'published'); + CREATE TYPE "public"."enum__site_settings_v_version_social_links_platform" AS ENUM('instagram', 'facebook', 'youtube', 'whatsapp'); + CREATE TYPE "public"."enum__site_settings_v_version_status" AS ENUM('draft', 'published'); + CREATE TABLE "users_sessions" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "created_at" timestamp(3) with time zone, + "expires_at" timestamp(3) with time zone NOT NULL + ); + + CREATE TABLE "users" ( + "id" serial PRIMARY KEY NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "email" varchar NOT NULL, + "reset_password_token" varchar, + "reset_password_expiration" timestamp(3) with time zone, + "salt" varchar, + "hash" varchar, + "login_attempts" numeric DEFAULT 0, + "lock_until" timestamp(3) with time zone + ); + + CREATE TABLE "media" ( + "id" serial PRIMARY KEY NOT NULL, + "alt" varchar NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "url" varchar, + "thumbnail_u_r_l" varchar, + "filename" varchar, + "mime_type" varchar, + "filesize" numeric, + "width" numeric, + "height" numeric, + "focal_x" numeric, + "focal_y" numeric + ); + + CREATE TABLE "petition" ( + "id" varchar PRIMARY KEY NOT NULL, + "title" varchar, + "subtitle" varchar, + "enabled" boolean DEFAULT false, + "body" jsonb, + "form_title" varchar, + "form_subtitle" varchar, + "form_consent_id" integer, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "_status" "enum_petition_status" DEFAULT 'draft' + ); + + CREATE TABLE "_petition_v" ( + "id" serial PRIMARY KEY NOT NULL, + "parent_id" varchar, + "version_title" varchar, + "version_subtitle" varchar, + "version_enabled" boolean DEFAULT false, + "version_body" jsonb, + "version_form_title" varchar, + "version_form_subtitle" varchar, + "version_form_consent_id" integer, + "version_updated_at" timestamp(3) with time zone, + "version_created_at" timestamp(3) with time zone, + "version__status" "enum__petition_v_version_status" DEFAULT 'draft', + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "latest" boolean + ); + + CREATE TABLE "contact" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + "email" varchar NOT NULL, + "phone" varchar NOT NULL, + "state" "enum_contact_state" NOT NULL, + "city" varchar NOT NULL, + "postal_code" varchar, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE "consent" ( + "id" serial PRIMARY KEY NOT NULL, + "text" jsonb NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE "signature" ( + "id" serial PRIMARY KEY NOT NULL, + "contact_id" integer NOT NULL, + "petition_id" varchar NOT NULL, + "consent_id" integer NOT NULL, + "comment" varchar, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE "subscription" ( + "id" serial PRIMARY KEY NOT NULL, + "contact_id" integer NOT NULL, + "consent_id" integer NOT NULL, + "comment" varchar, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE "payload_kv" ( + "id" serial PRIMARY KEY NOT NULL, + "key" varchar NOT NULL, + "data" jsonb NOT NULL + ); + + CREATE TABLE "payload_jobs_log" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "executed_at" timestamp(3) with time zone NOT NULL, + "completed_at" timestamp(3) with time zone NOT NULL, + "task_slug" "enum_payload_jobs_log_task_slug" NOT NULL, + "task_i_d" varchar NOT NULL, + "input" jsonb, + "output" jsonb, + "state" "enum_payload_jobs_log_state" NOT NULL, + "error" jsonb + ); + + CREATE TABLE "payload_jobs" ( + "id" serial PRIMARY KEY NOT NULL, + "input" jsonb, + "completed_at" timestamp(3) with time zone, + "total_tried" numeric DEFAULT 0, + "has_error" boolean DEFAULT false, + "error" jsonb, + "task_slug" "enum_payload_jobs_task_slug", + "queue" varchar DEFAULT 'default', + "wait_until" timestamp(3) with time zone, + "processing" boolean DEFAULT false, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE "payload_locked_documents" ( + "id" serial PRIMARY KEY NOT NULL, + "global_slug" varchar, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE "payload_locked_documents_rels" ( + "id" serial PRIMARY KEY NOT NULL, + "order" integer, + "parent_id" integer NOT NULL, + "path" varchar NOT NULL, + "users_id" integer, + "media_id" integer, + "petition_id" varchar, + "contact_id" integer, + "consent_id" integer, + "signature_id" integer, + "subscription_id" integer + ); + + CREATE TABLE "payload_preferences" ( + "id" serial PRIMARY KEY NOT NULL, + "key" varchar, + "value" jsonb, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE "payload_preferences_rels" ( + "id" serial PRIMARY KEY NOT NULL, + "order" integer, + "parent_id" integer NOT NULL, + "path" varchar NOT NULL, + "users_id" integer + ); + + CREATE TABLE "payload_migrations" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar, + "batch" numeric, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE "site_settings_social_links" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "platform" "enum_site_settings_social_links_platform", + "url" varchar, + "label" varchar, + "enabled" boolean + ); + + CREATE TABLE "site_settings" ( + "id" serial PRIMARY KEY NOT NULL, + "header_title" varchar, + "_status" "enum_site_settings_status" DEFAULT 'draft', + "updated_at" timestamp(3) with time zone, + "created_at" timestamp(3) with time zone + ); + + CREATE TABLE "_site_settings_v_version_social_links" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" serial PRIMARY KEY NOT NULL, + "platform" "enum__site_settings_v_version_social_links_platform", + "url" varchar, + "label" varchar, + "enabled" boolean, + "_uuid" varchar + ); + + CREATE TABLE "_site_settings_v" ( + "id" serial PRIMARY KEY NOT NULL, + "version_header_title" varchar, + "version__status" "enum__site_settings_v_version_status" DEFAULT 'draft', + "version_updated_at" timestamp(3) with time zone, + "version_created_at" timestamp(3) with time zone, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "latest" boolean, + "autosave" boolean + ); + + CREATE TABLE "home" ( + "id" serial PRIMARY KEY NOT NULL, + "image_id" integer NOT NULL, + "updated_at" timestamp(3) with time zone, + "created_at" timestamp(3) with time zone + ); + + CREATE TABLE "metadata_keywords" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "keyword" varchar NOT NULL + ); + + CREATE TABLE "metadata" ( + "id" serial PRIMARY KEY NOT NULL, + "url" varchar NOT NULL, + "title" varchar NOT NULL, + "description" varchar NOT NULL, + "open_graph_site_name" varchar NOT NULL, + "open_graph_description" varchar NOT NULL, + "twitter_creator" varchar NOT NULL, + "twitter_description" varchar NOT NULL, + "image_id" integer, + "updated_at" timestamp(3) with time zone, + "created_at" timestamp(3) with time zone + ); + + ALTER TABLE "users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "petition" ADD CONSTRAINT "petition_form_consent_id_consent_id_fk" FOREIGN KEY ("form_consent_id") REFERENCES "public"."consent"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "_petition_v" ADD CONSTRAINT "_petition_v_parent_id_petition_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."petition"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "_petition_v" ADD CONSTRAINT "_petition_v_version_form_consent_id_consent_id_fk" FOREIGN KEY ("version_form_consent_id") REFERENCES "public"."consent"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "signature" ADD CONSTRAINT "signature_contact_id_contact_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."contact"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "signature" ADD CONSTRAINT "signature_petition_id_petition_id_fk" FOREIGN KEY ("petition_id") REFERENCES "public"."petition"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "signature" ADD CONSTRAINT "signature_consent_id_consent_id_fk" FOREIGN KEY ("consent_id") REFERENCES "public"."consent"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "subscription" ADD CONSTRAINT "subscription_contact_id_contact_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."contact"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "subscription" ADD CONSTRAINT "subscription_consent_id_consent_id_fk" FOREIGN KEY ("consent_id") REFERENCES "public"."consent"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "payload_jobs_log" ADD CONSTRAINT "payload_jobs_log_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."payload_jobs"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_locked_documents"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_petition_fk" FOREIGN KEY ("petition_id") REFERENCES "public"."petition"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_contact_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."contact"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_consent_fk" FOREIGN KEY ("consent_id") REFERENCES "public"."consent"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_signature_fk" FOREIGN KEY ("signature_id") REFERENCES "public"."signature"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_subscription_fk" FOREIGN KEY ("subscription_id") REFERENCES "public"."subscription"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "site_settings_social_links" ADD CONSTRAINT "site_settings_social_links_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."site_settings"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "_site_settings_v_version_social_links" ADD CONSTRAINT "_site_settings_v_version_social_links_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_site_settings_v"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "home" ADD CONSTRAINT "home_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "metadata_keywords" ADD CONSTRAINT "metadata_keywords_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."metadata"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "metadata" ADD CONSTRAINT "metadata_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order"); + CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id"); + CREATE INDEX "users_updated_at_idx" ON "users" USING btree ("updated_at"); + CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at"); + CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email"); + CREATE INDEX "media_updated_at_idx" ON "media" USING btree ("updated_at"); + CREATE INDEX "media_created_at_idx" ON "media" USING btree ("created_at"); + CREATE UNIQUE INDEX "media_filename_idx" ON "media" USING btree ("filename"); + CREATE INDEX "petition_form_form_consent_idx" ON "petition" USING btree ("form_consent_id"); + CREATE INDEX "petition_updated_at_idx" ON "petition" USING btree ("updated_at"); + CREATE INDEX "petition_created_at_idx" ON "petition" USING btree ("created_at"); + CREATE INDEX "petition__status_idx" ON "petition" USING btree ("_status"); + CREATE INDEX "_petition_v_parent_idx" ON "_petition_v" USING btree ("parent_id"); + CREATE INDEX "_petition_v_version_form_version_form_consent_idx" ON "_petition_v" USING btree ("version_form_consent_id"); + CREATE INDEX "_petition_v_version_version_updated_at_idx" ON "_petition_v" USING btree ("version_updated_at"); + CREATE INDEX "_petition_v_version_version_created_at_idx" ON "_petition_v" USING btree ("version_created_at"); + CREATE INDEX "_petition_v_version_version__status_idx" ON "_petition_v" USING btree ("version__status"); + CREATE INDEX "_petition_v_created_at_idx" ON "_petition_v" USING btree ("created_at"); + CREATE INDEX "_petition_v_updated_at_idx" ON "_petition_v" USING btree ("updated_at"); + CREATE INDEX "_petition_v_latest_idx" ON "_petition_v" USING btree ("latest"); + CREATE INDEX "contact_updated_at_idx" ON "contact" USING btree ("updated_at"); + CREATE INDEX "contact_created_at_idx" ON "contact" USING btree ("created_at"); + CREATE INDEX "consent_updated_at_idx" ON "consent" USING btree ("updated_at"); + CREATE INDEX "consent_created_at_idx" ON "consent" USING btree ("created_at"); + CREATE INDEX "signature_contact_idx" ON "signature" USING btree ("contact_id"); + CREATE INDEX "signature_petition_idx" ON "signature" USING btree ("petition_id"); + CREATE INDEX "signature_consent_idx" ON "signature" USING btree ("consent_id"); + CREATE INDEX "signature_updated_at_idx" ON "signature" USING btree ("updated_at"); + CREATE INDEX "signature_created_at_idx" ON "signature" USING btree ("created_at"); + CREATE INDEX "subscription_contact_idx" ON "subscription" USING btree ("contact_id"); + CREATE INDEX "subscription_consent_idx" ON "subscription" USING btree ("consent_id"); + CREATE INDEX "subscription_updated_at_idx" ON "subscription" USING btree ("updated_at"); + CREATE INDEX "subscription_created_at_idx" ON "subscription" USING btree ("created_at"); + CREATE UNIQUE INDEX "payload_kv_key_idx" ON "payload_kv" USING btree ("key"); + CREATE INDEX "payload_jobs_log_order_idx" ON "payload_jobs_log" USING btree ("_order"); + CREATE INDEX "payload_jobs_log_parent_id_idx" ON "payload_jobs_log" USING btree ("_parent_id"); + CREATE INDEX "payload_jobs_completed_at_idx" ON "payload_jobs" USING btree ("completed_at"); + CREATE INDEX "payload_jobs_total_tried_idx" ON "payload_jobs" USING btree ("total_tried"); + CREATE INDEX "payload_jobs_has_error_idx" ON "payload_jobs" USING btree ("has_error"); + CREATE INDEX "payload_jobs_task_slug_idx" ON "payload_jobs" USING btree ("task_slug"); + CREATE INDEX "payload_jobs_queue_idx" ON "payload_jobs" USING btree ("queue"); + CREATE INDEX "payload_jobs_wait_until_idx" ON "payload_jobs" USING btree ("wait_until"); + CREATE INDEX "payload_jobs_processing_idx" ON "payload_jobs" USING btree ("processing"); + CREATE INDEX "payload_jobs_updated_at_idx" ON "payload_jobs" USING btree ("updated_at"); + CREATE INDEX "payload_jobs_created_at_idx" ON "payload_jobs" USING btree ("created_at"); + CREATE INDEX "payload_locked_documents_global_slug_idx" ON "payload_locked_documents" USING btree ("global_slug"); + CREATE INDEX "payload_locked_documents_updated_at_idx" ON "payload_locked_documents" USING btree ("updated_at"); + CREATE INDEX "payload_locked_documents_created_at_idx" ON "payload_locked_documents" USING btree ("created_at"); + CREATE INDEX "payload_locked_documents_rels_order_idx" ON "payload_locked_documents_rels" USING btree ("order"); + CREATE INDEX "payload_locked_documents_rels_parent_idx" ON "payload_locked_documents_rels" USING btree ("parent_id"); + CREATE INDEX "payload_locked_documents_rels_path_idx" ON "payload_locked_documents_rels" USING btree ("path"); + CREATE INDEX "payload_locked_documents_rels_users_id_idx" ON "payload_locked_documents_rels" USING btree ("users_id"); + CREATE INDEX "payload_locked_documents_rels_media_id_idx" ON "payload_locked_documents_rels" USING btree ("media_id"); + CREATE INDEX "payload_locked_documents_rels_petition_id_idx" ON "payload_locked_documents_rels" USING btree ("petition_id"); + CREATE INDEX "payload_locked_documents_rels_contact_id_idx" ON "payload_locked_documents_rels" USING btree ("contact_id"); + CREATE INDEX "payload_locked_documents_rels_consent_id_idx" ON "payload_locked_documents_rels" USING btree ("consent_id"); + CREATE INDEX "payload_locked_documents_rels_signature_id_idx" ON "payload_locked_documents_rels" USING btree ("signature_id"); + CREATE INDEX "payload_locked_documents_rels_subscription_id_idx" ON "payload_locked_documents_rels" USING btree ("subscription_id"); + CREATE INDEX "payload_preferences_key_idx" ON "payload_preferences" USING btree ("key"); + CREATE INDEX "payload_preferences_updated_at_idx" ON "payload_preferences" USING btree ("updated_at"); + CREATE INDEX "payload_preferences_created_at_idx" ON "payload_preferences" USING btree ("created_at"); + CREATE INDEX "payload_preferences_rels_order_idx" ON "payload_preferences_rels" USING btree ("order"); + CREATE INDEX "payload_preferences_rels_parent_idx" ON "payload_preferences_rels" USING btree ("parent_id"); + CREATE INDEX "payload_preferences_rels_path_idx" ON "payload_preferences_rels" USING btree ("path"); + CREATE INDEX "payload_preferences_rels_users_id_idx" ON "payload_preferences_rels" USING btree ("users_id"); + CREATE INDEX "payload_migrations_updated_at_idx" ON "payload_migrations" USING btree ("updated_at"); + CREATE INDEX "payload_migrations_created_at_idx" ON "payload_migrations" USING btree ("created_at"); + CREATE INDEX "site_settings_social_links_order_idx" ON "site_settings_social_links" USING btree ("_order"); + CREATE INDEX "site_settings_social_links_parent_id_idx" ON "site_settings_social_links" USING btree ("_parent_id"); + CREATE INDEX "site_settings__status_idx" ON "site_settings" USING btree ("_status"); + CREATE INDEX "_site_settings_v_version_social_links_order_idx" ON "_site_settings_v_version_social_links" USING btree ("_order"); + CREATE INDEX "_site_settings_v_version_social_links_parent_id_idx" ON "_site_settings_v_version_social_links" USING btree ("_parent_id"); + CREATE INDEX "_site_settings_v_version_version__status_idx" ON "_site_settings_v" USING btree ("version__status"); + CREATE INDEX "_site_settings_v_created_at_idx" ON "_site_settings_v" USING btree ("created_at"); + CREATE INDEX "_site_settings_v_updated_at_idx" ON "_site_settings_v" USING btree ("updated_at"); + CREATE INDEX "_site_settings_v_latest_idx" ON "_site_settings_v" USING btree ("latest"); + CREATE INDEX "_site_settings_v_autosave_idx" ON "_site_settings_v" USING btree ("autosave"); + CREATE INDEX "home_image_idx" ON "home" USING btree ("image_id"); + CREATE INDEX "metadata_keywords_order_idx" ON "metadata_keywords" USING btree ("_order"); + CREATE INDEX "metadata_keywords_parent_id_idx" ON "metadata_keywords" USING btree ("_parent_id"); + CREATE INDEX "metadata_image_idx" ON "metadata" USING btree ("image_id");`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + DROP TABLE "users_sessions" CASCADE; + DROP TABLE "users" CASCADE; + DROP TABLE "media" CASCADE; + DROP TABLE "petition" CASCADE; + DROP TABLE "_petition_v" CASCADE; + DROP TABLE "contact" CASCADE; + DROP TABLE "consent" CASCADE; + DROP TABLE "signature" CASCADE; + DROP TABLE "subscription" CASCADE; + DROP TABLE "payload_kv" CASCADE; + DROP TABLE "payload_jobs_log" CASCADE; + DROP TABLE "payload_jobs" CASCADE; + DROP TABLE "payload_locked_documents" CASCADE; + DROP TABLE "payload_locked_documents_rels" CASCADE; + DROP TABLE "payload_preferences" CASCADE; + DROP TABLE "payload_preferences_rels" CASCADE; + DROP TABLE "payload_migrations" CASCADE; + DROP TABLE "site_settings_social_links" CASCADE; + DROP TABLE "site_settings" CASCADE; + DROP TABLE "_site_settings_v_version_social_links" CASCADE; + DROP TABLE "_site_settings_v" CASCADE; + DROP TABLE "home" CASCADE; + DROP TABLE "metadata_keywords" CASCADE; + DROP TABLE "metadata" CASCADE; + DROP TYPE "public"."enum_petition_status"; + DROP TYPE "public"."enum__petition_v_version_status"; + DROP TYPE "public"."enum_contact_state"; + DROP TYPE "public"."enum_payload_jobs_log_task_slug"; + DROP TYPE "public"."enum_payload_jobs_log_state"; + DROP TYPE "public"."enum_payload_jobs_task_slug"; + DROP TYPE "public"."enum_site_settings_social_links_platform"; + DROP TYPE "public"."enum_site_settings_status"; + DROP TYPE "public"."enum__site_settings_v_version_social_links_platform"; + DROP TYPE "public"."enum__site_settings_v_version_status";`) +} diff --git a/src/migrations/20260715_163500_consent_text_to_jsonb.ts b/src/migrations/20260715_163500_consent_text_to_jsonb.ts new file mode 100644 index 00000000..f1780710 --- /dev/null +++ b/src/migrations/20260715_163500_consent_text_to_jsonb.ts @@ -0,0 +1,43 @@ +import { MigrateDownArgs, MigrateUpArgs, sql } from '@payloadcms/db-postgres' + +/** + * Reconciles production schema drift: `consent.text` is a `richText` field in the + * Payload config (which maps to `jsonb`), but the production database still has it + * as `varchar`. Its single row already contains valid Lexical JSON, so the cast is + * lossless. This migration is idempotent — it only runs the conversion when the + * column is still `varchar`, so it is a no-op on any database already at `jsonb` + * (e.g. a fresh local DB built from the initial migration). + */ +export async function up({ db }: MigrateUpArgs): Promise { + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'consent' + AND column_name = 'text' + AND data_type = 'character varying' + ) THEN + ALTER TABLE "consent" ALTER COLUMN "text" TYPE jsonb USING "text"::jsonb; + END IF; + END $$; + `) +} + +export async function down({ db }: MigrateDownArgs): Promise { + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'consent' + AND column_name = 'text' + AND data_type = 'jsonb' + ) THEN + ALTER TABLE "consent" ALTER COLUMN "text" TYPE varchar USING "text"::text; + END IF; + END $$; + `) +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts new file mode 100644 index 00000000..662832e4 --- /dev/null +++ b/src/migrations/index.ts @@ -0,0 +1,15 @@ +import * as migration_20260715_163458_initial from './20260715_163458_initial' +import * as migration_20260715_163500_consent_text_to_jsonb from './20260715_163500_consent_text_to_jsonb' + +export const migrations = [ + { + up: migration_20260715_163458_initial.up, + down: migration_20260715_163458_initial.down, + name: '20260715_163458_initial', + }, + { + up: migration_20260715_163500_consent_text_to_jsonb.up, + down: migration_20260715_163500_consent_text_to_jsonb.down, + name: '20260715_163500_consent_text_to_jsonb', + }, +] diff --git a/src/payload.config.ts b/src/payload.config.ts index 03d35fcd..62629426 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -2,21 +2,21 @@ import { postgresAdapter } from '@payloadcms/db-postgres' import { lexicalEditor } from '@payloadcms/richtext-lexical' import path from 'path' import { buildConfig } from 'payload' -import { fileURLToPath } from 'url' import sharp from 'sharp' +import { fileURLToPath } from 'url' -import { Users } from './collections/Users' -import { Media } from './collections/Media' -import { SiteSettings } from './globals/SiteSettings' +import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob' import { pt } from 'payload/i18n/pt' +import { Consent } from './collections/Consent' +import { Contact } from './collections/Contact' +import { Media } from './collections/Media' +import { Petition } from './collections/Petition' +import { Signature } from './collections/Signature' +import { Subscription } from './collections/Subscription' +import { Users } from './collections/Users' import { HomePage } from './globals/HomePage' import { Metadata } from './globals/Metadata' -import { Petition } from './collections/Petition' -import { Contact } from './collections/Contact' -import { Consent } from './collections/Consent' -import { Signature } from './collections/Signature' -import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob' -import { Subscription } from './collections/Subscription' +import { SiteSettings } from './globals/SiteSettings' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) @@ -43,7 +43,10 @@ export default buildConfig({ pool: { connectionString: process.env.DATABASE_URL || '', }, - // Local dev can point at the shared production DB; never auto-push schema changes. + // Schema changes are applied via committed migrations (pnpm migrate:create / + // pnpm migrate), never auto-pushed. `pnpm build` runs `payload migrate` before + // building, so migrations apply to prod on every Vercel deploy. + migrationDir: path.resolve(dirname, 'migrations'), push: false, }), sharp, diff --git a/tests/helpers/assertTestDatabase.ts b/tests/helpers/assertTestDatabase.ts new file mode 100644 index 00000000..de55aeca --- /dev/null +++ b/tests/helpers/assertTestDatabase.ts @@ -0,0 +1,32 @@ +/** + * Fail-closed guard for the automated test suite. + * + * The tests delete and recreate records (see seedUser.ts), so they must NEVER + * run against the production database. Rather than trying to detect prod (an + * allowlist we could forget to update), we require the opposite: the database + * name MUST end with `_test`. Anything else — including the production Neon + * database — is rejected. + */ +export function assertTestDatabase(databaseUrl: string | undefined): void { + if (!databaseUrl) { + throw new Error( + 'DATABASE_URL is not set. Tests require a dedicated test database configured in .env.test ' + + '(e.g. postgresql://teqo:teqo@localhost:5432/teqo_test).', + ) + } + + let databaseName: string + try { + databaseName = new URL(databaseUrl).pathname.replace(/^\//, '') + } catch { + throw new Error('DATABASE_URL is not a valid PostgreSQL connection string.') + } + + if (!/_test$/.test(databaseName)) { + throw new Error( + `Refusing to run tests against database "${databaseName || '(unknown)'}". ` + + 'The test database name must end with "_test". ' + + 'Configure DATABASE_URL in .env.test (e.g. postgresql://teqo:teqo@localhost:5432/teqo_test).', + ) + } +} diff --git a/tests/helpers/seedUser.ts b/tests/helpers/seedUser.ts index f0f5c86f..0740a4db 100644 --- a/tests/helpers/seedUser.ts +++ b/tests/helpers/seedUser.ts @@ -1,6 +1,8 @@ import { getPayload } from 'payload' import config from '../../src/payload.config.js' +import { assertTestDatabase } from './assertTestDatabase' + export const testUser = { email: 'dev@payloadcms.com', password: 'test', @@ -10,6 +12,7 @@ export const testUser = { * Seeds a test user for e2e admin tests. */ export async function seedTestUser(): Promise { + assertTestDatabase(process.env.DATABASE_URL) const payload = await getPayload({ config }) // Delete existing test user if any @@ -33,6 +36,7 @@ export async function seedTestUser(): Promise { * Cleans up test user after tests */ export async function cleanupTestUser(): Promise { + assertTestDatabase(process.env.DATABASE_URL) const payload = await getPayload({ config }) await payload.delete({ diff --git a/vitest.setup.ts b/vitest.setup.ts index 1a76c852..f60b1af9 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1,4 +1,11 @@ -// Any setup scripts you might need go here +// Load the isolated test environment — NEVER .env / .env.local, which point at +// the production database. `override: true` guarantees the test database wins +// even if a prod DATABASE_URL is already exported in the shell. +import { config as loadEnv } from 'dotenv' -// Load .env files -import 'dotenv/config' +import { assertTestDatabase } from './tests/helpers/assertTestDatabase' + +loadEnv({ path: '.env.test', override: true }) + +// Fail closed: refuse to run the (destructive) suite against a non-test database. +assertTestDatabase(process.env.DATABASE_URL)