Skip to content

Commit 23d3ea8

Browse files
authored
refactor: shared types package, outbox events, TanStack Query, plugin system (#541)
1 parent d91929f commit 23d3ea8

60 files changed

Lines changed: 2955 additions & 577 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Types Contract
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'packages/types/**'
7+
- 'backend/prisma/schema.prisma'
8+
- 'backend/src/**'
9+
- 'frontend/**'
10+
- 'packages/sdk/src/**'
11+
push:
12+
branches: [main]
13+
paths:
14+
- 'packages/types/**'
15+
- 'backend/prisma/schema.prisma'
16+
17+
jobs:
18+
shared-types:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0
24+
- uses: actions/setup-node@v4
25+
with:
26+
node-version: 22
27+
cache: npm
28+
- run: npm ci
29+
- run: npm run generate:prisma-types --workspace @agenticpay/types
30+
- run: npm run build --workspace @agenticpay/types
31+
- run: npm run check:duplicates --workspace @agenticpay/types
32+
- run: TYPE_API_BASE_REF=origin/main npm run check:breaking --workspace @agenticpay/types
33+
- run: npm run changelog --workspace @agenticpay/types
34+
- run: npm run migration-guide --workspace @agenticpay/types

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"generate:vapid-keys": "node scripts/generate-vapid-keys.js"
3535
},
3636
"dependencies": {
37+
"@agenticpay/types": "*",
3738
"@prisma/client": "^5.22.0",
3839
"@sentry/node": "^10.50.0",
3940
"@sentry/profiling-node": "^10.50.0",
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
DROP TABLE IF EXISTS "plugin_audit_logs";
2+
DROP TABLE IF EXISTS "plugin_configs";
3+
DROP TABLE IF EXISTS "plugins";
4+
DROP TABLE IF EXISTS "outbox_events";
5+
DROP TYPE IF EXISTS "PluginStatus";
6+
DROP TYPE IF EXISTS "OutboxEventStatus";
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
-- CreateEnum
2+
CREATE TYPE "OutboxEventStatus" AS ENUM ('pending', 'publishing', 'published', 'dead_letter');
3+
4+
-- CreateEnum
5+
CREATE TYPE "PluginStatus" AS ENUM ('installed', 'enabled', 'disabled', 'error');
6+
7+
-- CreateTable
8+
CREATE TABLE "outbox_events" (
9+
"id" TEXT NOT NULL,
10+
"aggregate_type" TEXT NOT NULL,
11+
"aggregate_id" TEXT NOT NULL,
12+
"event_type" TEXT NOT NULL,
13+
"payload" JSONB NOT NULL,
14+
"status" "OutboxEventStatus" NOT NULL DEFAULT 'pending',
15+
"attempts" INTEGER NOT NULL DEFAULT 0,
16+
"last_error" TEXT,
17+
"published_at" TIMESTAMP(3),
18+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
19+
"updated_at" TIMESTAMP(3) NOT NULL,
20+
21+
CONSTRAINT "outbox_events_pkey" PRIMARY KEY ("id")
22+
);
23+
24+
-- CreateTable
25+
CREATE TABLE "plugins" (
26+
"id" TEXT NOT NULL,
27+
"name" TEXT NOT NULL,
28+
"version" TEXT NOT NULL,
29+
"source" TEXT NOT NULL,
30+
"status" "PluginStatus" NOT NULL DEFAULT 'installed',
31+
"compatibility" JSONB NOT NULL,
32+
"health" JSONB NOT NULL,
33+
"installed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
34+
"updated_at" TIMESTAMP(3) NOT NULL,
35+
"disabled_at" TIMESTAMP(3),
36+
37+
CONSTRAINT "plugins_pkey" PRIMARY KEY ("id")
38+
);
39+
40+
-- CreateTable
41+
CREATE TABLE "plugin_configs" (
42+
"id" TEXT NOT NULL,
43+
"plugin_id" TEXT NOT NULL,
44+
"environment" TEXT NOT NULL DEFAULT 'default',
45+
"config" JSONB NOT NULL,
46+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
47+
"updated_at" TIMESTAMP(3) NOT NULL,
48+
49+
CONSTRAINT "plugin_configs_pkey" PRIMARY KEY ("id")
50+
);
51+
52+
-- CreateTable
53+
CREATE TABLE "plugin_audit_logs" (
54+
"id" TEXT NOT NULL,
55+
"plugin_id" TEXT NOT NULL,
56+
"actor_id" TEXT,
57+
"action" TEXT NOT NULL,
58+
"details" JSONB,
59+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
60+
61+
CONSTRAINT "plugin_audit_logs_pkey" PRIMARY KEY ("id")
62+
);
63+
64+
-- CreateIndex
65+
CREATE INDEX "outbox_events_status_created_at_idx" ON "outbox_events"("status", "created_at");
66+
67+
-- CreateIndex
68+
CREATE INDEX "outbox_events_aggregate_type_aggregate_id_idx" ON "outbox_events"("aggregate_type", "aggregate_id");
69+
70+
-- CreateIndex
71+
CREATE INDEX "outbox_events_event_type_idx" ON "outbox_events"("event_type");
72+
73+
-- CreateIndex
74+
CREATE INDEX "outbox_events_published_at_idx" ON "outbox_events"("published_at");
75+
76+
-- CreateIndex
77+
CREATE UNIQUE INDEX "plugins_name_key" ON "plugins"("name");
78+
79+
-- CreateIndex
80+
CREATE INDEX "plugins_status_idx" ON "plugins"("status");
81+
82+
-- CreateIndex
83+
CREATE INDEX "plugins_name_version_idx" ON "plugins"("name", "version");
84+
85+
-- CreateIndex
86+
CREATE UNIQUE INDEX "plugin_configs_plugin_id_environment_key" ON "plugin_configs"("plugin_id", "environment");
87+
88+
-- CreateIndex
89+
CREATE INDEX "plugin_audit_logs_plugin_id_created_at_idx" ON "plugin_audit_logs"("plugin_id", "created_at");
90+
91+
-- AddForeignKey
92+
ALTER TABLE "plugin_configs" ADD CONSTRAINT "plugin_configs_plugin_id_fkey" FOREIGN KEY ("plugin_id") REFERENCES "plugins"("id") ON DELETE CASCADE ON UPDATE CASCADE;
93+
94+
-- AddForeignKey
95+
ALTER TABLE "plugin_audit_logs" ADD CONSTRAINT "plugin_audit_logs_plugin_id_fkey" FOREIGN KEY ("plugin_id") REFERENCES "plugins"("id") ON DELETE CASCADE ON UPDATE CASCADE;

backend/prisma/schema.prisma

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,20 @@ enum PaymentLinkStatus {
7373
disabled
7474
}
7575

76+
enum OutboxEventStatus {
77+
pending
78+
publishing
79+
published
80+
dead_letter
81+
}
82+
83+
enum PluginStatus {
84+
installed
85+
enabled
86+
disabled
87+
error
88+
}
89+
7690
// ─── Models ───────────────────────────────────────────────────────────────────
7791

7892
model User {
@@ -268,6 +282,74 @@ model AuditLog {
268282
@@map("audit_logs")
269283
}
270284

285+
model OutboxEvent {
286+
id String @id @default(uuid())
287+
aggregateType String @map("aggregate_type")
288+
aggregateId String @map("aggregate_id")
289+
eventType String @map("event_type")
290+
payload Json
291+
status OutboxEventStatus @default(pending)
292+
attempts Int @default(0)
293+
lastError String? @map("last_error")
294+
publishedAt DateTime? @map("published_at")
295+
createdAt DateTime @default(now()) @map("created_at")
296+
updatedAt DateTime @updatedAt @map("updated_at")
297+
298+
@@index([status, createdAt])
299+
@@index([aggregateType, aggregateId])
300+
@@index([eventType])
301+
@@index([publishedAt])
302+
@@map("outbox_events")
303+
}
304+
305+
model Plugin {
306+
id String @id @default(uuid())
307+
name String @unique
308+
version String
309+
source String
310+
status PluginStatus @default(installed)
311+
compatibility Json
312+
health Json
313+
installedAt DateTime @default(now()) @map("installed_at")
314+
updatedAt DateTime @updatedAt @map("updated_at")
315+
disabledAt DateTime? @map("disabled_at")
316+
317+
configs PluginConfig[]
318+
auditLogs PluginAuditLog[]
319+
320+
@@index([status])
321+
@@index([name, version])
322+
@@map("plugins")
323+
}
324+
325+
model PluginConfig {
326+
id String @id @default(uuid())
327+
pluginId String @map("plugin_id")
328+
environment String @default("default")
329+
config Json
330+
createdAt DateTime @default(now()) @map("created_at")
331+
updatedAt DateTime @updatedAt @map("updated_at")
332+
333+
plugin Plugin @relation(fields: [pluginId], references: [id], onDelete: Cascade)
334+
335+
@@unique([pluginId, environment])
336+
@@map("plugin_configs")
337+
}
338+
339+
model PluginAuditLog {
340+
id String @id @default(uuid())
341+
pluginId String @map("plugin_id")
342+
actorId String? @map("actor_id")
343+
action String
344+
details Json?
345+
createdAt DateTime @default(now()) @map("created_at")
346+
347+
plugin Plugin @relation(fields: [pluginId], references: [id], onDelete: Cascade)
348+
349+
@@index([pluginId, createdAt])
350+
@@map("plugin_audit_logs")
351+
}
352+
271353
model AuditAnchor {
272354
id String @id @default(uuid())
273355
latestHash String @map("latest_hash")

backend/src/events/event-types.ts

Lines changed: 8 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,12 @@
1-
export type DomainEventType =
2-
| 'payment.created'
3-
| 'payment.executed'
4-
| 'payment.failed'
5-
| 'payment.cancelled'
6-
| 'project.created'
7-
| 'project.funded'
8-
| 'project.work_submitted'
9-
| 'project.work_approved'
10-
| 'project.disputed'
11-
| 'project.cancelled'
12-
| 'project.completed'
13-
| 'verification.requested'
14-
| 'verification.passed'
15-
| 'verification.failed'
16-
| 'invoice.generated'
17-
| 'receipt.minted'
18-
| 'receipt.transferred'
19-
| 'receipt.burned'
20-
| 'refund.requested'
21-
| 'refund.approved'
22-
| 'refund.rejected'
23-
| 'split.created'
24-
| 'split.executed';
1+
export type {
2+
DomainEvent,
3+
DomainEventType,
4+
EventHandler,
5+
EventMetadata,
6+
StoredEvent,
7+
} from '@agenticpay/types/events';
258

26-
export interface DomainEvent<T = unknown> {
27-
id: string;
28-
type: DomainEventType;
29-
aggregateId: string;
30-
aggregateType: string;
31-
version: number;
32-
payload: T;
33-
metadata: EventMetadata;
34-
occurredAt: string;
35-
}
36-
37-
export interface EventMetadata {
38-
correlationId?: string;
39-
causationId?: string;
40-
userId?: string;
41-
ipAddress?: string;
42-
userAgent?: string;
43-
}
44-
45-
export interface StoredEvent<T = unknown> extends DomainEvent<T> {
46-
sequenceNumber: number;
47-
streamId: string;
48-
}
9+
import type { StoredEvent } from '@agenticpay/types/events';
4910

5011
export interface EventStream {
5112
streamId: string;
@@ -57,8 +18,6 @@ export interface EventStream {
5718
updatedAt: string;
5819
}
5920

60-
export type EventHandler<T = unknown> = (event: StoredEvent<T>) => void | Promise<void>;
61-
6221
export interface PaymentCreatedPayload {
6322
from: string;
6423
to: string;

backend/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ import { rateLimitAnalyticsRouter } from './routes/rate-limit-analytics.js';
109109
import { startScheduledRotation, stopScheduledRotation } from './config/credential-rotation.js';
110110
import devDevRouter from './routes/dev/reload.js';
111111
import { sessionsRouter } from './routes/sessions.js';
112+
import { pluginsRouter } from './routes/plugins.js';
113+
import { outboxRouter } from './routes/outbox.js';
114+
import { startOutboxPublisher, stopOutboxPublisher } from './outbox/index.js';
112115

113116
// Validate environment variables at startup
114117
validateEnv();
@@ -244,6 +247,7 @@ apiV1Router.use('/catalog', catalogRouter);
244247
apiV1Router.use('/jobs', jobsRouter);
245248
apiV1Router.use('/queue', queueRouter);
246249
apiV1Router.use('/queue', bullMQMonitorRouter);
250+
apiV1Router.use('/outbox', outboxRouter);
247251
apiV1Router.use('/sla', slaRouter);
248252
apiV1Router.use('/onboarding', onboardingRouter);
249253
apiV1Router.use('/legacy', legacyRouter);
@@ -328,6 +332,9 @@ app.use('/api/v1/payment-links', paymentLinksRouter);
328332
// Merchant tax report generation (summary, 1099-K, VAT, nexus, CSV export)
329333
app.use('/api/v1/tax', taxRouter);
330334

335+
// Third-party backend plugins
336+
app.use('/api/v1/admin/plugins', pluginsRouter);
337+
331338
// Project + milestone delivery approval workflow
332339
app.use('/api/v1/projects', projectsRouter);
333340

@@ -412,6 +419,7 @@ if (config.queue.enabled) {
412419
paymentQueue.start();
413420
}
414421
startWebhookWorker();
422+
startOutboxPublisher({ useBullMQ: Boolean(process.env.REDIS_URL) });
415423

416424
// Auto-escalation cron
417425
setInterval(async () => {
@@ -477,6 +485,7 @@ server.listen(config.server.port, () => {
477485

478486
// Webhook worker
479487
startWebhookWorker();
488+
startOutboxPublisher({ useBullMQ: Boolean(process.env.REDIS_URL) });
480489

481490
// Auto-escalation cron
482491
setInterval(async () => {
@@ -535,6 +544,7 @@ const shutdown = (signal: string) => {
535544
messageQueue.stop();
536545
paymentQueue.stop();
537546
stopWebhookWorker();
547+
void stopOutboxPublisher();
538548
console.log('Message queue stopped.');
539549
} catch (err) {
540550
console.error('Error stopping message queue:', err);

backend/src/outbox/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './types.js';
2+
export * from './writer.js';
3+
export * from './publisher.js';
4+
export * from './metrics.js';

0 commit comments

Comments
 (0)