From 0e0ad9defefa6e7fd7a19a3039aab404551c75d5 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Tue, 21 Apr 2026 20:52:00 -0500 Subject: [PATCH] fixup: federation_audit_log DESC indexes + reserved M4 columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add .desc() to all three federation_audit_log created_at indexes for reverse-chronological scans (PRD section 7.3) - Add reserved nullable columns query_hash, outcome, bytes_out per TASKS.md M2-01 spec (written by M4, columns reserved now to avoid retroactive migration) - Regenerate migration 0008 in-place (replaces 0008_careless_lake.sql with 0008_smart_lyja.sql containing DESC indexes + new columns) - Update integration test: add reserved columns to CREATE TABLE in beforeAll; add 7th test for peer→grant cascade delete Co-Authored-By: Claude Sonnet 4.6 --- ..._careless_lake.sql => 0008_smart_lyja.sql} | 11 +-- packages/db/drizzle/meta/0008_snapshot.json | 26 +++++-- packages/db/drizzle/meta/_journal.json | 4 +- .../db/src/federation.integration.test.ts | 69 +++++++++++++++++-- packages/db/src/schema.ts | 14 +++- 5 files changed, 107 insertions(+), 17 deletions(-) rename packages/db/drizzle/{0008_careless_lake.sql => 0008_smart_lyja.sql} (92%) diff --git a/packages/db/drizzle/0008_careless_lake.sql b/packages/db/drizzle/0008_smart_lyja.sql similarity index 92% rename from packages/db/drizzle/0008_careless_lake.sql rename to packages/db/drizzle/0008_smart_lyja.sql index 63d4a38..d57e55e 100644 --- a/packages/db/drizzle/0008_careless_lake.sql +++ b/packages/db/drizzle/0008_smart_lyja.sql @@ -23,7 +23,10 @@ CREATE TABLE "federation_audit_log" ( "result_count" integer, "denied_reason" text, "latency_ms" integer, - "created_at" timestamp with time zone DEFAULT now() NOT NULL + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "query_hash" text, + "outcome" text, + "bytes_out" integer ); --> statement-breakpoint CREATE TABLE "federation_grants" ( @@ -63,9 +66,9 @@ ALTER TABLE "federation_grants" ADD CONSTRAINT "federation_grants_subject_user_i ALTER TABLE "federation_grants" ADD CONSTRAINT "federation_grants_peer_id_federation_peers_id_fk" FOREIGN KEY ("peer_id") REFERENCES "public"."federation_peers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint CREATE INDEX "admin_tokens_user_id_idx" ON "admin_tokens" USING btree ("user_id");--> statement-breakpoint CREATE UNIQUE INDEX "admin_tokens_hash_idx" ON "admin_tokens" USING btree ("token_hash");--> statement-breakpoint -CREATE INDEX "federation_audit_log_peer_created_at_idx" ON "federation_audit_log" USING btree ("peer_id","created_at");--> statement-breakpoint -CREATE INDEX "federation_audit_log_subject_created_at_idx" ON "federation_audit_log" USING btree ("subject_user_id","created_at");--> statement-breakpoint -CREATE INDEX "federation_audit_log_created_at_idx" ON "federation_audit_log" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "federation_audit_log_peer_created_at_idx" ON "federation_audit_log" USING btree ("peer_id","created_at" DESC NULLS LAST);--> statement-breakpoint +CREATE INDEX "federation_audit_log_subject_created_at_idx" ON "federation_audit_log" USING btree ("subject_user_id","created_at" DESC NULLS LAST);--> statement-breakpoint +CREATE INDEX "federation_audit_log_created_at_idx" ON "federation_audit_log" USING btree ("created_at" DESC NULLS LAST);--> statement-breakpoint CREATE INDEX "federation_grants_subject_status_idx" ON "federation_grants" USING btree ("subject_user_id","status");--> statement-breakpoint CREATE INDEX "federation_grants_peer_status_idx" ON "federation_grants" USING btree ("peer_id","status");--> statement-breakpoint CREATE INDEX "federation_peers_cert_serial_idx" ON "federation_peers" USING btree ("cert_serial");--> statement-breakpoint diff --git a/packages/db/drizzle/meta/0008_snapshot.json b/packages/db/drizzle/meta/0008_snapshot.json index a430804..7b4e38e 100644 --- a/packages/db/drizzle/meta/0008_snapshot.json +++ b/packages/db/drizzle/meta/0008_snapshot.json @@ -1,5 +1,5 @@ { - "id": "e5d39db4-c672-4085-9f17-0f936c3e1143", + "id": "1ecd9663-a2eb-4819-a5a5-818a0e84fd95", "prevId": "3431aafd-8ea0-499d-989c-d01e995f4764", "version": "7", "dialect": "postgresql", @@ -967,6 +967,24 @@ "primaryKey": false, "notNull": true, "default": "now()" + }, + "query_hash": { + "name": "query_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bytes_out": { + "name": "bytes_out", + "type": "integer", + "primaryKey": false, + "notNull": false } }, "indexes": { @@ -982,7 +1000,7 @@ { "expression": "created_at", "isExpression": false, - "asc": true, + "asc": false, "nulls": "last" } ], @@ -1003,7 +1021,7 @@ { "expression": "created_at", "isExpression": false, - "asc": true, + "asc": false, "nulls": "last" } ], @@ -1018,7 +1036,7 @@ { "expression": "created_at", "isExpression": false, - "asc": true, + "asc": false, "nulls": "last" } ], diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 0bd0739..62ac676 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -61,8 +61,8 @@ { "idx": 8, "version": "7", - "when": 1776821378331, - "tag": "0008_careless_lake", + "when": 1776822435828, + "tag": "0008_smart_lyja", "breakpoints": true } ] diff --git a/packages/db/src/federation.integration.test.ts b/packages/db/src/federation.integration.test.ts index 982eef4..5f58105 100644 --- a/packages/db/src/federation.integration.test.ts +++ b/packages/db/src/federation.integration.test.ts @@ -114,20 +114,23 @@ beforeAll(async () => { result_count integer, denied_reason text, latency_ms integer, - created_at timestamp with time zone NOT NULL DEFAULT now() + created_at timestamp with time zone NOT NULL DEFAULT now(), + query_hash text, + outcome text, + bytes_out integer ) `; await sql` CREATE INDEX IF NOT EXISTS federation_audit_log_peer_created_at_idx - ON federation_audit_log (peer_id, created_at) + ON federation_audit_log (peer_id, created_at DESC NULLS LAST) `; await sql` CREATE INDEX IF NOT EXISTS federation_audit_log_subject_created_at_idx - ON federation_audit_log (subject_user_id, created_at) + ON federation_audit_log (subject_user_id, created_at DESC NULLS LAST) `; await sql` CREATE INDEX IF NOT EXISTS federation_audit_log_created_at_idx - ON federation_audit_log (created_at) + ON federation_audit_log (created_at DESC NULLS LAST) `; }); @@ -360,4 +363,62 @@ describe.skipIf(!run)('federation schema — integration', () => { `, ).rejects.toThrow(); }, 10_000); + + // ── 7. FK cascade: peer delete cascades to federation_grants ───────────── + + it('cascade-deletes federation_grants when the owning peer is deleted', async () => { + const PEER3_ID = `f2000003-0000-4000-8000-000000000003`; + const cascadeGrantUserId = `${T}-cascade-grant-user`; + + // Insert a dedicated user and peer for this test. + await sql!` + INSERT INTO users (id, name, email, email_verified, created_at, updated_at) + VALUES (${cascadeGrantUserId}, ${'Cascade Grant User'}, ${cascadeGrantUserId + '@example.com'}, false, now(), now()) + ON CONFLICT (id) DO NOTHING + `; + await sql!` + INSERT INTO federation_peers + (id, common_name, display_name, cert_pem, cert_serial, cert_not_after, state, created_at) + VALUES ( + ${PEER3_ID}, + ${T + '-gateway-cascade-peer'}, + ${'Cascade Peer'}, + ${'cert-pem-cascade'}, + ${T + '-serial-003'}, + now() + interval '1 year', + ${'active'}, + now() + ) + ON CONFLICT (id) DO NOTHING + `; + + const scopeJson = JSON.stringify({ resources: ['tasks'] }); + await sql!` + INSERT INTO federation_grants + (subject_user_id, peer_id, scope, status, created_at) + VALUES ( + ${cascadeGrantUserId}, + ${PEER3_ID}, + ${scopeJson}::jsonb, + ${'active'}, + now() + ) + `; + + const before = await sql!` + SELECT count(*)::int AS cnt FROM federation_grants WHERE peer_id = ${PEER3_ID} + `; + expect(before[0]!['cnt']).toBe(1); + + // Delete peer → grants should cascade-delete. + await sql!`DELETE FROM federation_peers WHERE id = ${PEER3_ID}`; + + const after = await sql!` + SELECT count(*)::int AS cnt FROM federation_grants WHERE peer_id = ${PEER3_ID} + `; + expect(after[0]!['cnt']).toBe(0); + + // Cleanup + await sql!`DELETE FROM users WHERE id = ${cascadeGrantUserId}`.catch(() => {}); + }, 15_000); }); diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index b4091d5..c6c6bc1 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -759,13 +759,21 @@ export const federationAuditLog = pgTable( latencyMs: integer('latency_ms'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + + // Reserved for M4 — see PRD 7.3 + /** SHA-256 of the normalised GraphQL/REST query string; written by M4 search. */ + queryHash: text('query_hash'), + /** Request outcome: "allowed" | "denied" | "partial"; written by M4. */ + outcome: text('outcome'), + /** Response payload size in bytes; written by M4. */ + bytesOut: integer('bytes_out'), }, (t) => [ // Per-peer request history in reverse chronological order. - index('federation_audit_log_peer_created_at_idx').on(t.peerId, t.createdAt), + index('federation_audit_log_peer_created_at_idx').on(t.peerId, t.createdAt.desc()), // Per-user access log in reverse chronological order. - index('federation_audit_log_subject_created_at_idx').on(t.subjectUserId, t.createdAt), + index('federation_audit_log_subject_created_at_idx').on(t.subjectUserId, t.createdAt.desc()), // Global time-range scans (dashboards, rate-limit windows). - index('federation_audit_log_created_at_idx').on(t.createdAt), + index('federation_audit_log_created_at_idx').on(t.createdAt.desc()), ], );