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)