fixup: federation_audit_log DESC indexes + reserved M4 columns
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,10 @@ CREATE TABLE "federation_audit_log" (
|
|||||||
"result_count" integer,
|
"result_count" integer,
|
||||||
"denied_reason" text,
|
"denied_reason" text,
|
||||||
"latency_ms" integer,
|
"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
|
--> statement-breakpoint
|
||||||
CREATE TABLE "federation_grants" (
|
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
|
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 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 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_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");--> 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");--> 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_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_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
|
CREATE INDEX "federation_peers_cert_serial_idx" ON "federation_peers" USING btree ("cert_serial");--> statement-breakpoint
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "e5d39db4-c672-4085-9f17-0f936c3e1143",
|
"id": "1ecd9663-a2eb-4819-a5a5-818a0e84fd95",
|
||||||
"prevId": "3431aafd-8ea0-499d-989c-d01e995f4764",
|
"prevId": "3431aafd-8ea0-499d-989c-d01e995f4764",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@@ -967,6 +967,24 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": "now()"
|
"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": {
|
"indexes": {
|
||||||
@@ -982,7 +1000,7 @@
|
|||||||
{
|
{
|
||||||
"expression": "created_at",
|
"expression": "created_at",
|
||||||
"isExpression": false,
|
"isExpression": false,
|
||||||
"asc": true,
|
"asc": false,
|
||||||
"nulls": "last"
|
"nulls": "last"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -1003,7 +1021,7 @@
|
|||||||
{
|
{
|
||||||
"expression": "created_at",
|
"expression": "created_at",
|
||||||
"isExpression": false,
|
"isExpression": false,
|
||||||
"asc": true,
|
"asc": false,
|
||||||
"nulls": "last"
|
"nulls": "last"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -1018,7 +1036,7 @@
|
|||||||
{
|
{
|
||||||
"expression": "created_at",
|
"expression": "created_at",
|
||||||
"isExpression": false,
|
"isExpression": false,
|
||||||
"asc": true,
|
"asc": false,
|
||||||
"nulls": "last"
|
"nulls": "last"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -61,8 +61,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 8,
|
"idx": 8,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1776821378331,
|
"when": 1776822435828,
|
||||||
"tag": "0008_careless_lake",
|
"tag": "0008_smart_lyja",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -114,20 +114,23 @@ beforeAll(async () => {
|
|||||||
result_count integer,
|
result_count integer,
|
||||||
denied_reason text,
|
denied_reason text,
|
||||||
latency_ms integer,
|
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`
|
await sql`
|
||||||
CREATE INDEX IF NOT EXISTS federation_audit_log_peer_created_at_idx
|
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`
|
await sql`
|
||||||
CREATE INDEX IF NOT EXISTS federation_audit_log_subject_created_at_idx
|
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`
|
await sql`
|
||||||
CREATE INDEX IF NOT EXISTS federation_audit_log_created_at_idx
|
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();
|
).rejects.toThrow();
|
||||||
}, 10_000);
|
}, 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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -759,13 +759,21 @@ export const federationAuditLog = pgTable(
|
|||||||
latencyMs: integer('latency_ms'),
|
latencyMs: integer('latency_ms'),
|
||||||
|
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
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) => [
|
(t) => [
|
||||||
// Per-peer request history in reverse chronological order.
|
// 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.
|
// 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).
|
// 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()),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user