diff --git a/__fixtures__/seed/README.md b/__fixtures__/seed/README.md index 3f1a359a2b..02fbc5b87b 100644 --- a/__fixtures__/seed/README.md +++ b/__fixtures__/seed/README.md @@ -6,46 +6,71 @@ Composable SQL seed layers for integration testing. Each layer builds on the pre | Layer | Files | What it provides | |-------|-------|-----------------| -| **services** | `services/setup.sql` | Roles, extensions, stamps, metaschema tables, services tables, settings tables, modules tables, grants | +| **base** | `base/setup.sql` | `uuid-ossp` extension, `stamps` schema + `timestamps()` trigger | +| **services** | `services/setup.sql` | Everything in base + `citext`, metaschema tables, services tables, settings tables, modules tables, grants (self-contained) | | **services data** | `services/test-data.sql` | Example database (`simple-pets`), 3 schemas, 5 APIs, 2 domains, API→schema linkage, animals metaschema entries | | **app-schemas** | `app-schemas/simple-pets/schema.sql` | `simple-pets-*` schemas, animals table with constraints/indexes/triggers | | **app data** | `app-schemas/simple-pets/test-data.sql` | 5 test animals (Buddy, Max, Whiskers, Mittens, Tweety) | +> **Note:** Roles (`administrator`, `authenticated`, `anonymous`) are created upstream by `pgsql-test`'s `createBaseRoles()` — seed SQL should never create roles. + ## Usage with pgsql-test -```typescript -import { getConnections } from 'pgsql-test'; -import path from 'path'; +### Non-services tests (no metaschema) +```typescript const SEED = path.resolve(__dirname, '../../../__fixtures__/seed'); -const { db, teardown } = await getConnections({ - seed: seed.sqlfile([ - `${SEED}/services/setup.sql`, - `${SEED}/services/test-data.sql`, - `${SEED}/app-schemas/simple-pets/schema.sql`, - `${SEED}/app-schemas/simple-pets/test-data.sql`, - ]) -}); +seed.sqlfile([ + `${SEED}/base/setup.sql`, // extensions, stamps + // ... your app-specific schema + data +]) +``` + +### Services-enabled tests (metaschema + domain resolution) + +```typescript +seed.sqlfile([ + `${SEED}/services/setup.sql`, // metaschema + services DDL + `${SEED}/app-schemas/simple-pets/schema.sql`, // app tables + `${SEED}/services/test-data.sql`, // API + domain rows + `${SEED}/app-schemas/simple-pets/test-data.sql`, // test animals +]) ``` ## Composition Pick only the layers you need: +- **Base only** (extensions + stamps, no metaschema): `base/setup.sql` + your own schema/data - **Metaschema + services only** (no app tables): `services/setup.sql` + `services/test-data.sql` -- **Full stack with app data**: all four files in order +- **Full stack with app data**: `services/setup.sql` + `app-schemas/*` + `services/test-data.sql` + `app-schemas/*/test-data.sql` - **Custom app schema**: `services/setup.sql` + `services/test-data.sql` + your own schema/data SQL +> **Note:** `services/setup.sql` is self-contained — it includes everything from `base/setup.sql` plus metaschema and services DDL. You do NOT need to load both `base/setup.sql` and `services/setup.sql`. + ## Consumers -These test files use the shared fixtures (no local duplicates): +These test files use the shared fixtures: -| Test file | Seed files used | -|-----------|----------------| -| `graphql/server-test/__tests__/server.integration.test.ts` | `services/*` + `app-schemas/simple-pets/*` (services scenarios) | -| `graphql/server-test/__tests__/express-context.integration.test.ts` | `services/*` + `app-schemas/simple-pets/*` | +| Test file | Shared fixtures used | +|-----------|---------------------| +| `graphql/server-test/__tests__/server.integration.test.ts` | `base/*` (simple-seed), `services/*` + `app-schemas/*` (services scenarios) | +| `graphql/server-test/__tests__/express-context.integration.test.ts` | `services/*` + `app-schemas/*` | | `graphql/server-test/__tests__/upload.integration.test.ts` | `services/setup.sql` (DDL only, storage data is local) | +| `graphql/server-test/__tests__/cli-e2e.test.ts` | `base/*` + `app-schemas/*` (animals), `base/*` (search) | +| `graphql/server-test/__tests__/search.integration.test.ts` | `base/*` | +| `graphql/server-test/__tests__/schema-snapshot.test.ts` | `base/*` | + +## Local Fixtures (not shared) + +Some test scenarios have unique content that stays in `graphql/server-test/__fixtures__/seed/`: + +| Directory | What's unique | +|-----------|--------------| +| `search-seed/` | `extensions.sql` (pg_trgm + pgvector), `schema.sql` (articles table with tsvector/trigram/vector columns), `test-data.sql` | +| `schema-snapshot/` | `schema.sql` (5-table blog schema: users, posts, tags, post_tags, comments), `test-data.sql` | +| `simple-seed-storage/` | `setup.sql` (storage module, JWT functions), `schema.sql` (3-tenant storage schemas), `test-data.sql` | ## Well-Known IDs diff --git a/__fixtures__/seed/base/setup.sql b/__fixtures__/seed/base/setup.sql new file mode 100644 index 0000000000..1ef1848579 --- /dev/null +++ b/__fixtures__/seed/base/setup.sql @@ -0,0 +1,38 @@ +-- Shared fixture: minimal base setup +-- +-- Creates extensions and the stamps schema needed by all test scenarios. +-- Roles (administrator, authenticated, anonymous) are created upstream by +-- pgsql-test's createUserRole() — do NOT recreate them here. +-- +-- This is the smallest common denominator — tests that also need +-- metaschema / services should load services/setup.sql instead +-- (which is self-contained and includes everything here). +-- +-- Usage: +-- seed.sqlfile([ +-- shared('base', 'setup.sql'), +-- // ... then your app-specific schema + data +-- ]) + +-- ===================================================== +-- EXTENSIONS +-- ===================================================== + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- ===================================================== +-- STAMPS (timestamp triggers) +-- ===================================================== + +CREATE SCHEMA IF NOT EXISTS stamps; + +CREATE OR REPLACE FUNCTION stamps.timestamps() +RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.created_at = COALESCE(NEW.created_at, now()); + END IF; + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/__fixtures__/seed/services/setup.sql b/__fixtures__/seed/services/setup.sql index 5bd3727104..5ef63eebf1 100644 --- a/__fixtures__/seed/services/setup.sql +++ b/__fixtures__/seed/services/setup.sql @@ -2,6 +2,8 @@ -- -- Creates the minimum tables needed to emulate a production Constructive -- database with API resolution, domain routing, and RLS module support. +-- Roles (administrator, authenticated, anonymous) are created upstream by +-- pgsql-test's createUserRole() — do NOT recreate them here. -- -- Usage (via pgsql-test seed adapter): -- seed.sqlfile([ @@ -17,26 +19,6 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "citext"; -DO $$ -BEGIN - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'administrator') THEN - CREATE ROLE administrator; - END IF; - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticated') THEN - CREATE ROLE authenticated; - END IF; - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anonymous') THEN - CREATE ROLE anonymous; - END IF; - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'app_user') THEN - CREATE ROLE app_user; - END IF; - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'app_admin') THEN - CREATE ROLE app_admin; - END IF; -END -$$; - CREATE SCHEMA IF NOT EXISTS stamps; CREATE OR REPLACE FUNCTION stamps.timestamps() diff --git a/graphql/server-test/__fixtures__/seed/schema-snapshot/setup.sql b/graphql/server-test/__fixtures__/seed/schema-snapshot/setup.sql deleted file mode 100644 index 8fe1d5b34a..0000000000 --- a/graphql/server-test/__fixtures__/seed/schema-snapshot/setup.sql +++ /dev/null @@ -1,35 +0,0 @@ --- Setup for schema-snapshot test scenario --- Creates the required extensions and roles - --- Ensure uuid-ossp extension is available -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- Create required roles if they don't exist -DO $$ -BEGIN - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'administrator') THEN - CREATE ROLE administrator; - END IF; - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticated') THEN - CREATE ROLE authenticated; - END IF; - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anonymous') THEN - CREATE ROLE anonymous; - END IF; -END -$$; - --- Create stamps schema for timestamp trigger if not exists -CREATE SCHEMA IF NOT EXISTS stamps; - --- Create timestamps trigger function -CREATE OR REPLACE FUNCTION stamps.timestamps() -RETURNS TRIGGER AS $$ -BEGIN - IF TG_OP = 'INSERT' THEN - NEW.created_at = COALESCE(NEW.created_at, now()); - END IF; - NEW.updated_at = now(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; diff --git a/graphql/server-test/__fixtures__/seed/search-seed/extensions.sql b/graphql/server-test/__fixtures__/seed/search-seed/extensions.sql new file mode 100644 index 0000000000..605127dba4 --- /dev/null +++ b/graphql/server-test/__fixtures__/seed/search-seed/extensions.sql @@ -0,0 +1,14 @@ +-- Search-specific extensions (loaded after base/setup.sql) +-- +-- Adds pg_trgm for fuzzy matching and optionally pgvector for similarity search. + +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + +-- pgvector may not be available in all environments +DO $$ +BEGIN + CREATE EXTENSION IF NOT EXISTS "vector"; +EXCEPTION WHEN OTHERS THEN + RAISE NOTICE 'pgvector extension not available, skipping'; +END +$$; diff --git a/graphql/server-test/__fixtures__/seed/search-seed/setup.sql b/graphql/server-test/__fixtures__/seed/search-seed/setup.sql deleted file mode 100644 index 169f5ca8f8..0000000000 --- a/graphql/server-test/__fixtures__/seed/search-seed/setup.sql +++ /dev/null @@ -1,45 +0,0 @@ --- Setup for search-seed test scenario --- Creates the required extensions and roles for search testing - --- Core extensions -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -CREATE EXTENSION IF NOT EXISTS "pg_trgm"; - --- Try to create vector extension (may not be available in all environments) -DO $$ -BEGIN - CREATE EXTENSION IF NOT EXISTS "vector"; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'pgvector extension not available, skipping'; -END -$$; - --- Create required roles if they don't exist -DO $$ -BEGIN - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'administrator') THEN - CREATE ROLE administrator; - END IF; - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticated') THEN - CREATE ROLE authenticated; - END IF; - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anonymous') THEN - CREATE ROLE anonymous; - END IF; -END -$$; - --- Create stamps schema for timestamp trigger if not exists -CREATE SCHEMA IF NOT EXISTS stamps; - --- Create timestamps trigger function -CREATE OR REPLACE FUNCTION stamps.timestamps() -RETURNS TRIGGER AS $$ -BEGIN - IF TG_OP = 'INSERT' THEN - NEW.created_at = COALESCE(NEW.created_at, now()); - END IF; - NEW.updated_at = now(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; diff --git a/graphql/server-test/__fixtures__/seed/simple-seed/schema.sql b/graphql/server-test/__fixtures__/seed/simple-seed/schema.sql deleted file mode 100644 index c9fe18d088..0000000000 --- a/graphql/server-test/__fixtures__/seed/simple-seed/schema.sql +++ /dev/null @@ -1,54 +0,0 @@ --- Schema creation for simple-seed test scenario --- Creates the simple-pets schemas and animals table - --- Create schemas -CREATE SCHEMA IF NOT EXISTS "simple-pets-public"; -CREATE SCHEMA IF NOT EXISTS "simple-pets-pets-public"; - --- Grant schema usage -GRANT USAGE ON SCHEMA "simple-pets-public" TO administrator, authenticated, anonymous; -GRANT USAGE ON SCHEMA "simple-pets-pets-public" TO administrator, authenticated, anonymous; - --- Set default privileges -ALTER DEFAULT PRIVILEGES IN SCHEMA "simple-pets-public" - GRANT ALL ON TABLES TO administrator; -ALTER DEFAULT PRIVILEGES IN SCHEMA "simple-pets-public" - GRANT USAGE ON SEQUENCES TO administrator, authenticated; -ALTER DEFAULT PRIVILEGES IN SCHEMA "simple-pets-public" - GRANT ALL ON FUNCTIONS TO administrator, authenticated, anonymous; - -ALTER DEFAULT PRIVILEGES IN SCHEMA "simple-pets-pets-public" - GRANT ALL ON TABLES TO administrator; -ALTER DEFAULT PRIVILEGES IN SCHEMA "simple-pets-pets-public" - GRANT USAGE ON SEQUENCES TO administrator, authenticated; -ALTER DEFAULT PRIVILEGES IN SCHEMA "simple-pets-pets-public" - GRANT ALL ON FUNCTIONS TO administrator, authenticated, anonymous; - --- Create animals table -CREATE TABLE IF NOT EXISTS "simple-pets-pets-public".animals ( - id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - name text NOT NULL, - species text NOT NULL, - owner_id uuid, - created_at timestamptz DEFAULT now(), - updated_at timestamptz DEFAULT now(), - CONSTRAINT animals_name_chk CHECK (character_length(name) <= 256), - CONSTRAINT animals_species_chk CHECK (character_length(species) <= 100) -); - --- Create timestamp trigger -DROP TRIGGER IF EXISTS timestamps_tg ON "simple-pets-pets-public".animals; -CREATE TRIGGER timestamps_tg - BEFORE INSERT OR UPDATE - ON "simple-pets-pets-public".animals - FOR EACH ROW - EXECUTE PROCEDURE stamps.timestamps(); - --- Create indexes -CREATE INDEX IF NOT EXISTS animals_created_at_idx ON "simple-pets-pets-public".animals (created_at); -CREATE INDEX IF NOT EXISTS animals_updated_at_idx ON "simple-pets-pets-public".animals (updated_at); - --- Grant table permissions (allow anonymous to do CRUD for tests) -GRANT SELECT, INSERT, UPDATE, DELETE ON "simple-pets-pets-public".animals TO administrator; -GRANT SELECT, INSERT, UPDATE, DELETE ON "simple-pets-pets-public".animals TO authenticated; -GRANT SELECT, INSERT, UPDATE, DELETE ON "simple-pets-pets-public".animals TO anonymous; diff --git a/graphql/server-test/__fixtures__/seed/simple-seed/setup.sql b/graphql/server-test/__fixtures__/seed/simple-seed/setup.sql deleted file mode 100644 index 1b44bc1e5b..0000000000 --- a/graphql/server-test/__fixtures__/seed/simple-seed/setup.sql +++ /dev/null @@ -1,35 +0,0 @@ --- Setup for simple-seed test scenario --- Creates the required schemas and extensions - --- Ensure uuid-ossp extension is available -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- Create required roles if they don't exist -DO $$ -BEGIN - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'administrator') THEN - CREATE ROLE administrator; - END IF; - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticated') THEN - CREATE ROLE authenticated; - END IF; - IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anonymous') THEN - CREATE ROLE anonymous; - END IF; -END -$$; - --- Create stamps schema for timestamp trigger if not exists -CREATE SCHEMA IF NOT EXISTS stamps; - --- Create timestamps trigger function -CREATE OR REPLACE FUNCTION stamps.timestamps() -RETURNS TRIGGER AS $$ -BEGIN - IF TG_OP = 'INSERT' THEN - NEW.created_at = COALESCE(NEW.created_at, now()); - END IF; - NEW.updated_at = now(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; diff --git a/graphql/server-test/__fixtures__/seed/simple-seed/test-data.sql b/graphql/server-test/__fixtures__/seed/simple-seed/test-data.sql deleted file mode 100644 index 9d8c296f3f..0000000000 --- a/graphql/server-test/__fixtures__/seed/simple-seed/test-data.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Test data for simple-seed scenario --- Inserts 5 animals: 2 Dogs, 2 Cats, 1 Bird - -INSERT INTO "simple-pets-pets-public".animals (id, name, species, owner_id, created_at, updated_at) -VALUES - ('a0000001-0000-0000-0000-000000000001', 'Buddy', 'Dog', NULL, now(), now()), - ('a0000001-0000-0000-0000-000000000002', 'Max', 'Dog', NULL, now(), now()), - ('a0000001-0000-0000-0000-000000000003', 'Whiskers', 'Cat', NULL, now(), now()), - ('a0000001-0000-0000-0000-000000000004', 'Mittens', 'Cat', NULL, now(), now()), - ('a0000001-0000-0000-0000-000000000005', 'Tweety', 'Bird', NULL, now(), now()) -ON CONFLICT (id) DO NOTHING; diff --git a/graphql/server-test/__tests__/cli-e2e.test.ts b/graphql/server-test/__tests__/cli-e2e.test.ts index 9a26611ad0..96de755f80 100644 --- a/graphql/server-test/__tests__/cli-e2e.test.ts +++ b/graphql/server-test/__tests__/cli-e2e.test.ts @@ -79,9 +79,12 @@ function resolveNodePaths(): string[] { return [...dirs]; } -const seedRoot = path.join(__dirname, '..', '__fixtures__', 'seed'); +const localSeedRoot = path.join(__dirname, '..', '__fixtures__', 'seed'); +const sharedSeedRoot = path.join(__dirname, '..', '..', '..', '__fixtures__', 'seed'); const sql = (seedDir: string, file: string) => - path.join(seedRoot, seedDir, file); + path.join(localSeedRoot, seedDir, file); +const shared = (...segments: string[]) => + path.join(sharedSeedRoot, ...segments); const TOOL_NAME = 'cli-e2e-test'; @@ -395,9 +398,9 @@ describe('CLI E2E — generated CLI against real DB', () => { }, [ seed.sqlfile([ - sql('simple-seed', 'setup.sql'), - sql('simple-seed', 'schema.sql'), - sql('simple-seed', 'test-data.sql'), + shared('base', 'setup.sql'), + shared('app-schemas', 'simple-pets', 'schema.sql'), + shared('app-schemas', 'simple-pets', 'test-data.sql'), ]), ], ); @@ -783,7 +786,8 @@ describe('CLI E2E — search commands against real DB', () => { }, [ seed.sqlfile([ - sql('search-seed', 'setup.sql'), + shared('base', 'setup.sql'), + sql('search-seed', 'extensions.sql'), sql('search-seed', 'schema.sql'), sql('search-seed', 'test-data.sql'), ]), @@ -1126,7 +1130,8 @@ describe('CLI E2E — embedder / --auto-embed', () => { }, [ seed.sqlfile([ - sql('search-seed', 'setup.sql'), + shared('base', 'setup.sql'), + sql('search-seed', 'extensions.sql'), sql('search-seed', 'schema.sql'), sql('search-seed', 'test-data.sql'), ]), diff --git a/graphql/server-test/__tests__/schema-snapshot.test.ts b/graphql/server-test/__tests__/schema-snapshot.test.ts index a2b9b2c22b..3caac09d18 100644 --- a/graphql/server-test/__tests__/schema-snapshot.test.ts +++ b/graphql/server-test/__tests__/schema-snapshot.test.ts @@ -28,9 +28,12 @@ import { getConnections, seed } from '../src'; jest.setTimeout(60000); -const seedRoot = path.join(__dirname, '..', '__fixtures__', 'seed'); +const localSeedRoot = path.join(__dirname, '..', '__fixtures__', 'seed'); +const sharedSeedRoot = path.join(__dirname, '..', '..', '..', '__fixtures__', 'seed'); const sql = (seedDir: string, file: string) => - path.join(seedRoot, seedDir, file); + path.join(localSeedRoot, seedDir, file); +const shared = (...segments: string[]) => + path.join(sharedSeedRoot, ...segments); const schemas = ['snapshot_public']; @@ -49,7 +52,7 @@ describe('Schema Snapshot', () => { }, [ seed.sqlfile([ - sql('schema-snapshot', 'setup.sql'), + shared('base', 'setup.sql'), sql('schema-snapshot', 'schema.sql'), sql('schema-snapshot', 'test-data.sql'), ]), diff --git a/graphql/server-test/__tests__/search.integration.test.ts b/graphql/server-test/__tests__/search.integration.test.ts index f18bbf96eb..57efe12390 100644 --- a/graphql/server-test/__tests__/search.integration.test.ts +++ b/graphql/server-test/__tests__/search.integration.test.ts @@ -20,9 +20,12 @@ import type supertest from 'supertest'; jest.setTimeout(60000); -const seedRoot = path.join(__dirname, '..', '__fixtures__', 'seed'); +const localSeedRoot = path.join(__dirname, '..', '__fixtures__', 'seed'); +const sharedSeedRoot = path.join(__dirname, '..', '..', '..', '__fixtures__', 'seed'); const sql = (seedDir: string, file: string) => - path.join(seedRoot, seedDir, file); + path.join(localSeedRoot, seedDir, file); +const shared = (...segments: string[]) => + path.join(sharedSeedRoot, ...segments); const schemas = ['search_public']; @@ -48,7 +51,8 @@ describe('Unified Search — server integration', () => { }, [ seed.sqlfile([ - sql('search-seed', 'setup.sql'), + shared('base', 'setup.sql'), + sql('search-seed', 'extensions.sql'), sql('search-seed', 'schema.sql'), sql('search-seed', 'test-data.sql'), ]), diff --git a/graphql/server-test/__tests__/server.integration.test.ts b/graphql/server-test/__tests__/server.integration.test.ts index f8fef844b2..875a441327 100644 --- a/graphql/server-test/__tests__/server.integration.test.ts +++ b/graphql/server-test/__tests__/server.integration.test.ts @@ -12,10 +12,7 @@ import type supertest from 'supertest'; jest.setTimeout(30000); -const localSeedRoot = path.join(__dirname, '..', '__fixtures__', 'seed'); const sharedSeedRoot = path.join(__dirname, '..', '..', '..', '__fixtures__', 'seed'); -const sql = (seedDir: string, file: string) => - path.join(localSeedRoot, seedDir, file); const shared = (...segments: string[]) => path.join(sharedSeedRoot, ...segments); const schemas = ['simple-pets-public', 'simple-pets-pets-public']; @@ -110,10 +107,11 @@ const seedFilesFor = (seedDir: Scenario['seedDir']) => { shared('app-schemas', 'simple-pets', 'test-data.sql'), ]; } + // simple-seed: base setup + shared app-schemas return [ - sql(seedDir, 'setup.sql'), - sql(seedDir, 'schema.sql'), - sql(seedDir, 'test-data.sql'), + shared('base', 'setup.sql'), + shared('app-schemas', 'simple-pets', 'schema.sql'), + shared('app-schemas', 'simple-pets', 'test-data.sql'), ]; };