What ships (verifiable without live DB, 64 new tests): - db/migrations/0000_initial_schema.sql (Drizzle-generated, 7 tables) + .down.sql + registry entry - db/migrations/rehearse.ts: forward-then-rollback round-trip with row-count hash check (DoD 2.2) - infra/docker-compose.yml: postgres 17 + redis 7 + openobserve for local dev (5433/6380/5080) - packages/schema/src/rate-limit.ts: pluggable store; 4 tests including 21st-of-20 reject (DoD 2.4) - packages/schema/src/csrf.ts: HMAC double-submit token; 8 tests covering forgery + tamper + malformed - packages/schema/src/authz.ts: 3-role Cerbos-equivalent rules (operator/approver/viewer); 6 tests - packages/schema/src/api-contracts.ts: Zod schemas for /api/content, /api/approvals, /api/publications, /api/feature-flags + idempotencyKeyOf; 11 tests What defers to live-DB session: - 2.3 admin route handlers integration tests (401/403/200/422 contract suite) - 2.2 actual rehearsal execution against staging DB Total: 79/79 tests pass across 9 files in 4 packages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
105 lines
4.1 KiB
SQL
105 lines
4.1 KiB
SQL
CREATE TABLE IF NOT EXISTS "approvals" (
|
|
"id" serial PRIMARY KEY NOT NULL,
|
|
"content_id" integer NOT NULL,
|
|
"outlet" text NOT NULL,
|
|
"approved_by" text NOT NULL,
|
|
"approved_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"notes" text
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "audit" (
|
|
"id" serial PRIMARY KEY NOT NULL,
|
|
"ts" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"actor" text NOT NULL,
|
|
"action" text NOT NULL,
|
|
"subject_type" text NOT NULL,
|
|
"subject_id" text NOT NULL,
|
|
"correlation_id" text NOT NULL,
|
|
"payload_jsonb" jsonb
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "content" (
|
|
"id" serial PRIMARY KEY NOT NULL,
|
|
"vault_path" text NOT NULL,
|
|
"slug" text NOT NULL,
|
|
"title" text NOT NULL,
|
|
"body_sanitized" text NOT NULL,
|
|
"frontmatter_jsonb" jsonb NOT NULL,
|
|
"content_hash" text NOT NULL,
|
|
"version" integer DEFAULT 1 NOT NULL,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
CONSTRAINT "content_vault_path_unique" UNIQUE("vault_path")
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "linkedin_tokens" (
|
|
"id" serial PRIMARY KEY NOT NULL,
|
|
"subject_type" text NOT NULL,
|
|
"subject_urn" text NOT NULL,
|
|
"access_token_ct" text NOT NULL,
|
|
"refresh_token_ct" text,
|
|
"access_expires_at" timestamp with time zone NOT NULL,
|
|
"refresh_expires_at" timestamp with time zone,
|
|
"scopes" text NOT NULL,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "metrics" (
|
|
"id" serial PRIMARY KEY NOT NULL,
|
|
"publication_id" integer NOT NULL,
|
|
"collected_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"impressions" integer DEFAULT 0 NOT NULL,
|
|
"reactions" integer DEFAULT 0 NOT NULL,
|
|
"comments" integer DEFAULT 0 NOT NULL,
|
|
"shares" integer DEFAULT 0 NOT NULL,
|
|
"clicks" integer DEFAULT 0 NOT NULL,
|
|
"raw_jsonb" jsonb
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "outlet_feature_flags" (
|
|
"outlet" text PRIMARY KEY NOT NULL,
|
|
"enabled" boolean DEFAULT true NOT NULL,
|
|
"reason" text,
|
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"updated_by" text NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "publications" (
|
|
"id" serial PRIMARY KEY NOT NULL,
|
|
"content_id" integer NOT NULL,
|
|
"outlet" text NOT NULL,
|
|
"status" text NOT NULL,
|
|
"scheduled_at" timestamp with time zone,
|
|
"published_at" timestamp with time zone,
|
|
"external_id" text,
|
|
"external_url" text,
|
|
"idempotency_key" text NOT NULL,
|
|
"error" text,
|
|
"metadata_jsonb" jsonb
|
|
);
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "approvals" ADD CONSTRAINT "approvals_content_id_content_id_fk" FOREIGN KEY ("content_id") REFERENCES "public"."content"("id") ON DELETE no action ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "metrics" ADD CONSTRAINT "metrics_publication_id_publications_id_fk" FOREIGN KEY ("publication_id") REFERENCES "public"."publications"("id") ON DELETE no action ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "publications" ADD CONSTRAINT "publications_content_id_content_id_fk" FOREIGN KEY ("content_id") REFERENCES "public"."content"("id") ON DELETE no action ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "audit_ts_idx" ON "audit" USING btree ("ts");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "audit_correlation_idx" ON "audit" USING btree ("correlation_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "content_slug_idx" ON "content" USING btree ("slug");--> statement-breakpoint
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "linkedin_tokens_subject_idx" ON "linkedin_tokens" USING btree ("subject_urn");--> statement-breakpoint
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "publications_idempotency_key_idx" ON "publications" USING btree ("idempotency_key");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "publications_content_outlet_idx" ON "publications" USING btree ("content_id","outlet"); |