Skip to content

Commit dd8aa2a

Browse files
authored
refactor(storage): extract WebhookRepository across all three storage adapters (#125)
refactor(storage): extract WebhookRepository from storage adapters Addresses the TODO on postgres.adapter.ts L49. Extracts all webhook and delivery methods from PostgresAdapter, SqliteAdapter, and MemoryAdapter into dedicated WebhookRepository classes under apps/api/src/storage/adapters/repositories/. Each adapter becomes a thin facade that delegates to its repository. Zero behavior changes - existing webhook-storage test suite (25 tests) passes unchanged. Establishes the pattern for extracting the remaining domains (ACL, anomaly, slowlog, commandlog, latency, memory, hotkeys, etc.) in follow-up PRs. Made-with: Cursor
1 parent d89ba70 commit dd8aa2a

6 files changed

Lines changed: 865 additions & 645 deletions

File tree

apps/api/src/storage/adapters/memory.adapter.ts

Lines changed: 15 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import type {
4242
MetricForecastSettings,
4343
MetricKind,
4444
} from '@betterdb/shared';
45+
import { WebhookMemoryRepository } from './repositories/webhook.memory.repository';
4546

4647
export class MemoryAdapter implements StoragePort {
4748
private aclEntries: StoredAclEntry[] = [];
@@ -56,11 +57,10 @@ export class MemoryAdapter implements StoragePort {
5657
private vectorIndexSnapshots: VectorIndexSnapshot[] = [];
5758
private metricForecastSettings: Map<string, MetricForecastSettings> = new Map();
5859
private settings: AppSettings | null = null;
59-
private webhooks: Map<string, Webhook> = new Map();
60-
private deliveries: Map<string, WebhookDelivery> = new Map();
60+
private readonly MAX_DELIVERIES_PER_WEBHOOK = 1000;
61+
private readonly webhookRepo = new WebhookMemoryRepository(this.MAX_DELIVERIES_PER_WEBHOOK);
6162
private idCounter = 1;
6263
private ready: boolean = false;
63-
private readonly MAX_DELIVERIES_PER_WEBHOOK = 1000;
6464

6565
async initialize(): Promise<void> {
6666
this.ready = true;
@@ -755,167 +755,66 @@ export class MemoryAdapter implements StoragePort {
755755
}
756756

757757
async createWebhook(webhook: Omit<Webhook, 'id' | 'createdAt' | 'updatedAt'>): Promise<Webhook> {
758-
const id = randomUUID();
759-
const now = Date.now();
760-
const newWebhook: Webhook = {
761-
id,
762-
...webhook,
763-
createdAt: now,
764-
updatedAt: now,
765-
};
766-
this.webhooks.set(id, newWebhook);
767-
return { ...newWebhook };
758+
return this.webhookRepo.createWebhook(webhook);
768759
}
769760

770761
async getWebhook(id: string): Promise<Webhook | null> {
771-
const webhook = this.webhooks.get(id);
772-
return webhook ? { ...webhook } : null;
762+
return this.webhookRepo.getWebhook(id);
773763
}
774764

775765
async getWebhooksByInstance(connectionId?: string): Promise<Webhook[]> {
776-
let webhooks = Array.from(this.webhooks.values());
777-
if (connectionId) {
778-
webhooks = webhooks.filter((w) => w.connectionId === connectionId || !w.connectionId);
779-
} else {
780-
// No connectionId provided - only return global webhooks (not scoped to any connection)
781-
webhooks = webhooks.filter((w) => !w.connectionId);
782-
}
783-
return webhooks.sort((a, b) => b.createdAt - a.createdAt).map((w) => ({ ...w }));
766+
return this.webhookRepo.getWebhooksByInstance(connectionId);
784767
}
785768

786769
async getWebhooksByEvent(event: WebhookEventType, connectionId?: string): Promise<Webhook[]> {
787-
let webhooks = Array.from(this.webhooks.values()).filter(
788-
(w) => w.enabled && w.events.includes(event),
789-
);
790-
if (connectionId) {
791-
// Return webhooks scoped to this connection OR global webhooks (no connectionId)
792-
webhooks = webhooks.filter((w) => w.connectionId === connectionId || !w.connectionId);
793-
} else {
794-
// No connectionId provided - only return global webhooks (not scoped to any connection)
795-
webhooks = webhooks.filter((w) => !w.connectionId);
796-
}
797-
return webhooks.map((w) => ({ ...w }));
770+
return this.webhookRepo.getWebhooksByEvent(event, connectionId);
798771
}
799772

800773
async updateWebhook(
801774
id: string,
802775
updates: Partial<Omit<Webhook, 'id' | 'createdAt' | 'updatedAt'>>,
803776
): Promise<Webhook | null> {
804-
const webhook = this.webhooks.get(id);
805-
if (!webhook) return null;
806-
807-
// Filter out undefined values to prevent overwriting existing fields
808-
const definedUpdates = Object.fromEntries(
809-
Object.entries(updates).filter(([_, value]) => value !== undefined),
810-
);
811-
812-
const updated: Webhook = {
813-
...webhook,
814-
...definedUpdates,
815-
id,
816-
createdAt: webhook.createdAt,
817-
updatedAt: Date.now(),
818-
};
819-
this.webhooks.set(id, updated);
820-
return { ...updated };
777+
return this.webhookRepo.updateWebhook(id, updates);
821778
}
822779

823780
async deleteWebhook(id: string): Promise<boolean> {
824-
const deleted = this.webhooks.delete(id);
825-
if (deleted) {
826-
const deliveriesToDelete = Array.from(this.deliveries.entries())
827-
.filter(([_, d]) => d.webhookId === id)
828-
.map(([id]) => id);
829-
deliveriesToDelete.forEach((deliveryId) => this.deliveries.delete(deliveryId));
830-
}
831-
return deleted;
781+
return this.webhookRepo.deleteWebhook(id);
832782
}
833783

834784
async createDelivery(
835785
delivery: Omit<WebhookDelivery, 'id' | 'createdAt'>,
836786
): Promise<WebhookDelivery> {
837-
const now = Date.now();
838-
const id = randomUUID();
839-
const newDelivery: WebhookDelivery = {
840-
id,
841-
...delivery,
842-
createdAt: now,
843-
};
844-
this.deliveries.set(id, newDelivery);
845-
846-
const webhookDeliveries = Array.from(this.deliveries.values())
847-
.filter((d) => d.webhookId === delivery.webhookId)
848-
.sort((a, b) => b.createdAt - a.createdAt);
849-
850-
if (webhookDeliveries.length > this.MAX_DELIVERIES_PER_WEBHOOK) {
851-
const toDelete = webhookDeliveries.slice(this.MAX_DELIVERIES_PER_WEBHOOK);
852-
toDelete.forEach((d) => this.deliveries.delete(d.id));
853-
}
854-
855-
return { ...newDelivery };
787+
return this.webhookRepo.createDelivery(delivery);
856788
}
857789

858790
async getDelivery(id: string): Promise<WebhookDelivery | null> {
859-
const delivery = this.deliveries.get(id);
860-
return delivery ? { ...delivery } : null;
791+
return this.webhookRepo.getDelivery(id);
861792
}
862793

863794
async getDeliveriesByWebhook(
864795
webhookId: string,
865796
limit: number = 50,
866797
offset: number = 0,
867798
): Promise<WebhookDelivery[]> {
868-
return Array.from(this.deliveries.values())
869-
.filter((d) => d.webhookId === webhookId)
870-
.sort((a, b) => b.createdAt - a.createdAt)
871-
.slice(offset, offset + limit)
872-
.map((d) => ({ ...d }));
799+
return this.webhookRepo.getDeliveriesByWebhook(webhookId, limit, offset);
873800
}
874801

875802
async updateDelivery(
876803
id: string,
877804
updates: Partial<Omit<WebhookDelivery, 'id' | 'webhookId' | 'createdAt'>>,
878805
): Promise<boolean> {
879-
const delivery = this.deliveries.get(id);
880-
if (!delivery) return false;
881-
882-
const updated: WebhookDelivery = {
883-
...delivery,
884-
...updates,
885-
id,
886-
webhookId: delivery.webhookId,
887-
createdAt: delivery.createdAt,
888-
};
889-
this.deliveries.set(id, updated);
890-
return true;
806+
return this.webhookRepo.updateDelivery(id, updates);
891807
}
892808

893809
async getRetriableDeliveries(
894810
limit: number = 100,
895811
connectionId?: string,
896812
): Promise<WebhookDelivery[]> {
897-
const now = Date.now();
898-
let deliveries = Array.from(this.deliveries.values()).filter(
899-
(d) => d.status === 'retrying' && d.nextRetryAt && d.nextRetryAt <= now,
900-
);
901-
if (connectionId) {
902-
deliveries = deliveries.filter((d) => d.connectionId === connectionId);
903-
}
904-
return deliveries
905-
.sort((a, b) => (a.nextRetryAt || 0) - (b.nextRetryAt || 0))
906-
.slice(0, limit)
907-
.map((d) => ({ ...d }));
813+
return this.webhookRepo.getRetriableDeliveries(limit, connectionId);
908814
}
909815

910816
async pruneOldDeliveries(cutoffTimestamp: number, connectionId?: string): Promise<number> {
911-
const before = this.deliveries.size;
912-
Array.from(this.deliveries.entries())
913-
.filter(
914-
([_, d]) =>
915-
d.createdAt < cutoffTimestamp && (!connectionId || d.connectionId === connectionId),
916-
)
917-
.forEach(([id]) => this.deliveries.delete(id));
918-
return before - this.deliveries.size;
817+
return this.webhookRepo.pruneOldDeliveries(cutoffTimestamp, connectionId);
919818
}
920819

921820
// Slow Log Methods

0 commit comments

Comments
 (0)