diff --git a/AGENTS.md b/AGENTS.md index ba8e902..010b1b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Commands -- **Test all**: `bun test` or `vitest` +- **Test all**: `bun test` - **Test single file**: `vitest src/__tests__/unit/path/to/test.test.ts` - **Test with UI**: `bun run test:ui` - **Dev server**: `bun run dev:backend` (auto-reload on port 8069) diff --git a/package.json b/package.json index 3e04974..3e75e4c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "test": "vitest", "test:ui": "vitest --ui", "start": "bun run src/server.ts", - "init_key": "bun run src/utils/generateInitialAPIKey.ts" + "init_key": "bun run src/utils/generateInitialAPIKey.ts", + "format": "bunx prettier --write ." }, "devDependencies": { "@types/bun": "latest", diff --git a/src/__tests__/unit/http/createdCheckout.test.ts b/src/__tests__/unit/http/createdCheckout.test.ts index 372881f..c3d00a9 100644 --- a/src/__tests__/unit/http/createdCheckout.test.ts +++ b/src/__tests__/unit/http/createdCheckout.test.ts @@ -21,12 +21,24 @@ class PaymentMock { public userId: string; public data: unknown; public readonly type = "PAYMENT" as const; + public reported_timestamp = { toISO: () => "2024-01-01T00:00:00.000Z" }; constructor(userId: string, data: unknown) { this.userId = userId; this.data = data; paymentConstructorCalls.push({ userId, data }); } + + serialize() { + return { + SQL: { + type: this.type, + userId: this.userId, + reported_timestamp: this.reported_timestamp, + data: this.data, + }, + }; + } } // Mock modules @@ -50,12 +62,24 @@ vi.mock("../../../events/RawEvents/Payment.ts", () => ({ public userId: string; public data: unknown; public readonly type = "PAYMENT" as const; + public reported_timestamp = { toISO: () => "2024-01-01T00:00:00.000Z" }; constructor(userId: string, data: unknown) { this.userId = userId; this.data = data; paymentConstructorCalls.push({ userId, data }); } + + serialize() { + return { + SQL: { + type: this.type, + userId: this.userId, + reported_timestamp: this.reported_timestamp, + data: this.data, + }, + }; + } }, })); diff --git a/src/__tests__/unit/storage/postgres/addKey.test.ts b/src/__tests__/unit/storage/postgres/addKey.test.ts index 65b14dc..36eb400 100644 --- a/src/__tests__/unit/storage/postgres/addKey.test.ts +++ b/src/__tests__/unit/storage/postgres/addKey.test.ts @@ -44,7 +44,8 @@ describe("PostgresAdapter - addKey handler", () => { ]); const adapter = new PostgresAdapter(addKeyEvent); - const result = await adapter.add(); + const serialized = addKeyEvent.serialize(); + const result = await adapter.add(serialized); expect(result).toEqual({ id: "api-key-id-123" }); }); @@ -63,7 +64,8 @@ describe("PostgresAdapter - addKey handler", () => { ]); const adapter = new PostgresAdapter(addKeyEvent); - await adapter.add(); + const serialized = addKeyEvent.serialize(); + await adapter.add(serialized); const insertCall = mockTransaction.values.mock.calls[0][0]; expect(insertCall.name).toBe(keyData.name); @@ -88,7 +90,10 @@ describe("PostgresAdapter - addKey handler", () => { }; const adapter = new PostgresAdapter(invalidEvent as any); - await expect(adapter.add()).rejects.toThrow("Missing data field"); + const serialized = invalidEvent.serialize() as any; + await expect(adapter.add(serialized)).rejects.toThrow( + "Missing data field", + ); }); it("throws error when name is missing", async () => { @@ -112,7 +117,10 @@ describe("PostgresAdapter - addKey handler", () => { }; const adapter = new PostgresAdapter(invalidEvent as any); - await expect(adapter.add()).rejects.toThrow("Invalid or missing 'name'"); + const serialized = invalidEvent.serialize() as any; + await expect(adapter.add(serialized)).rejects.toThrow( + "Invalid or missing 'name'", + ); }); it("throws error when key is missing", async () => { @@ -136,7 +144,10 @@ describe("PostgresAdapter - addKey handler", () => { }; const adapter = new PostgresAdapter(invalidEvent as any); - await expect(adapter.add()).rejects.toThrow("Invalid or missing 'key'"); + const serialized = invalidEvent.serialize() as any; + await expect(adapter.add(serialized)).rejects.toThrow( + "Invalid or missing 'key'", + ); }); it("throws error when key is empty string", async () => { @@ -162,7 +173,10 @@ describe("PostgresAdapter - addKey handler", () => { }; const adapter = new PostgresAdapter(invalidEvent as any); - await expect(adapter.add()).rejects.toThrow("API key cannot be empty"); + const serialized = invalidEvent.serialize() as any; + await expect(adapter.add(serialized)).rejects.toThrow( + "API key cannot be empty", + ); }); it("throws error when timestamp is empty", async () => { @@ -186,7 +200,8 @@ describe("PostgresAdapter - addKey handler", () => { }; const adapter = new PostgresAdapter(invalidEvent as any); - await expect(adapter.add()).rejects.toThrow( + const serialized = invalidEvent.serialize() as any; + await expect(adapter.add(serialized)).rejects.toThrow( "Timestamp is undefined or empty", ); }); @@ -205,7 +220,8 @@ describe("PostgresAdapter - addKey handler", () => { ); const adapter = new PostgresAdapter(addKeyEvent); - await expect(adapter.add()).rejects.toThrow(); + const serialized = addKeyEvent.serialize(); + await expect(adapter.add(serialized)).rejects.toThrow(); }); it("handles empty API key ID response", async () => { @@ -218,7 +234,8 @@ describe("PostgresAdapter - addKey handler", () => { mockTransaction.returning.mockResolvedValueOnce([]); const adapter = new PostgresAdapter(addKeyEvent); - await expect(adapter.add()).rejects.toThrow( + const serialized = addKeyEvent.serialize(); + await expect(adapter.add(serialized)).rejects.toThrow( "API key insert returned no record", ); }); @@ -233,7 +250,8 @@ describe("PostgresAdapter - addKey handler", () => { mockTransaction.returning.mockResolvedValueOnce([{}]); const adapter = new PostgresAdapter(addKeyEvent); - await expect(adapter.add()).rejects.toThrow( + const serialized = addKeyEvent.serialize(); + await expect(adapter.add(serialized)).rejects.toThrow( "API key insert returned object without id field", ); }); diff --git a/src/__tests__/unit/storage/postgres/addPayment.test.ts b/src/__tests__/unit/storage/postgres/addPayment.test.ts index 57d3487..7ab9511 100644 --- a/src/__tests__/unit/storage/postgres/addPayment.test.ts +++ b/src/__tests__/unit/storage/postgres/addPayment.test.ts @@ -42,7 +42,8 @@ describe("PostgresAdapter - addPayment handler", () => { mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-123" }]); const adapter = new PostgresAdapter(paymentEvent, "api-key-123"); - await adapter.add(); + const serialized = paymentEvent.serialize(); + await adapter.add(serialized); const eventInsertCall = mockTransaction.values.mock.calls[1][0]; expect(eventInsertCall.api_keyId).toBe("api-key-123"); @@ -56,7 +57,8 @@ describe("PostgresAdapter - addPayment handler", () => { mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-456" }]); const adapter = new PostgresAdapter(paymentEvent); - await adapter.add(); + const serialized = paymentEvent.serialize(); + await adapter.add(serialized); const eventInsertCall = mockTransaction.values.mock.calls[1][0]; expect(eventInsertCall.api_keyId).toBeUndefined(); @@ -70,7 +72,8 @@ describe("PostgresAdapter - addPayment handler", () => { mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-3" }]); const adapter = new PostgresAdapter(paymentEvent); - await adapter.add(); + const serialized = paymentEvent.serialize(); + await adapter.add(serialized); const paymentInsertCall = mockTransaction.values.mock.calls[2][0]; expect(paymentInsertCall.creditAmount).toBe(15000); @@ -85,7 +88,8 @@ describe("PostgresAdapter - addPayment handler", () => { mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-pos" }]); const adapter = new PostgresAdapter(paymentEvent); - await adapter.add(); + const serialized = paymentEvent.serialize(); + await adapter.add(serialized); const paymentInsertCall = mockTransaction.values.mock.calls[2][0]; expect(paymentInsertCall.creditAmount).toBe(1); @@ -111,7 +115,8 @@ describe("PostgresAdapter - addPayment handler", () => { }; const adapter = new PostgresAdapter(invalidEvent as any); - await expect(adapter.add()).rejects.toThrow(/positive/); + const serialized = invalidEvent.serialize() as any; + await expect(adapter.add(serialized)).rejects.toThrow(/positive/); }); it("throws error when creditAmount is negative", async () => { @@ -131,7 +136,8 @@ describe("PostgresAdapter - addPayment handler", () => { }; const adapter = new PostgresAdapter(invalidEvent as any); - await expect(adapter.add()).rejects.toThrow(/positive/); + const serialized = invalidEvent.serialize() as any; + await expect(adapter.add(serialized)).rejects.toThrow(/positive/); }); it("throws error when timestamp is empty", async () => { @@ -155,7 +161,8 @@ describe("PostgresAdapter - addPayment handler", () => { mockTransaction.returning.mockResolvedValueOnce([]); const adapter = new PostgresAdapter(invalidEvent as any); - await expect(adapter.add()).rejects.toThrow( + const serialized = invalidEvent.serialize() as any; + await expect(adapter.add(serialized)).rejects.toThrow( "Timestamp is undefined or empty", ); }); @@ -173,7 +180,8 @@ describe("PostgresAdapter - addPayment handler", () => { ); const adapter = new PostgresAdapter(paymentEvent); - await expect(adapter.add()).rejects.toThrow(); + const serialized = paymentEvent.serialize(); + await expect(adapter.add(serialized)).rejects.toThrow(); }); it("handles empty event ID response", async () => { @@ -185,7 +193,8 @@ describe("PostgresAdapter - addPayment handler", () => { mockTransaction.returning.mockResolvedValueOnce([]); const adapter = new PostgresAdapter(paymentEvent); - await expect(adapter.add()).rejects.toThrow( + const serialized = paymentEvent.serialize(); + await expect(adapter.add(serialized)).rejects.toThrow( "Event insert returned no ID", ); }); diff --git a/src/__tests__/unit/storage/postgres/addSdkCall.test.ts b/src/__tests__/unit/storage/postgres/addSdkCall.test.ts index a331d09..387b634 100644 --- a/src/__tests__/unit/storage/postgres/addSdkCall.test.ts +++ b/src/__tests__/unit/storage/postgres/addSdkCall.test.ts @@ -45,7 +45,8 @@ describe("PostgresAdapter - addSdkCall handler", () => { ]); const adapter = new PostgresAdapter(sdkCallEvent, "api-key-123"); - await adapter.add(); + const serialized = sdkCallEvent.serialize(); + await adapter.add(serialized); const eventInsertCall = mockTransaction.values.mock.calls[1][0]; expect(eventInsertCall.api_keyId).toBe("api-key-123"); @@ -60,7 +61,8 @@ describe("PostgresAdapter - addSdkCall handler", () => { mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-456" }]); const adapter = new PostgresAdapter(sdkCallEvent, "api-key-456"); - await adapter.add(); + const serialized = sdkCallEvent.serialize(); + await adapter.add(serialized); const sdkCallInsertCall = mockTransaction.values.mock.calls[2][0]; expect(sdkCallInsertCall.debitAmount).toBe(2500); @@ -75,7 +77,8 @@ describe("PostgresAdapter - addSdkCall handler", () => { mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-1" }]); const adapter = new PostgresAdapter(sdkCallEvent, "api-key-1"); - await adapter.add(); + const serialized = sdkCallEvent.serialize(); + await adapter.add(serialized); const eventInsertCall = mockTransaction.values.mock.calls[1][0]; expect(eventInsertCall).toHaveProperty("reported_timestamp"); @@ -91,7 +94,8 @@ describe("PostgresAdapter - addSdkCall handler", () => { mockTransaction.returning.mockResolvedValueOnce([{ id: "event-id-pos" }]); const adapter = new PostgresAdapter(sdkCallEvent, "api-key-pos"); - await adapter.add(); + const serialized = sdkCallEvent.serialize(); + await adapter.add(serialized); const insertedValues = mockTransaction.values.mock.calls.map( (c: any) => c[0], @@ -117,7 +121,8 @@ describe("PostgresAdapter - addSdkCall handler", () => { ); const adapter = new PostgresAdapter(sdkCallEvent, "api-key"); - await expect(adapter.add()).rejects.toThrow(); + const serialized = sdkCallEvent.serialize(); + await expect(adapter.add(serialized)).rejects.toThrow(); }); it("handles empty event ID response", async () => { @@ -129,7 +134,8 @@ describe("PostgresAdapter - addSdkCall handler", () => { mockTransaction.returning.mockResolvedValueOnce([]); const adapter = new PostgresAdapter(sdkCallEvent, "api-key"); - await expect(adapter.add()).rejects.toThrow( + const serialized = sdkCallEvent.serialize(); + await expect(adapter.add(serialized)).rejects.toThrow( "Event insert returned no ID", ); }); @@ -155,7 +161,8 @@ describe("PostgresAdapter - addSdkCall handler", () => { }; const adapter = new PostgresAdapter(invalidEvent as any, "api-key"); - await expect(adapter.add()).rejects.toThrow( + const serialized = invalidEvent.serialize() as any; + await expect(adapter.add(serialized)).rejects.toThrow( "Timestamp is undefined or empty", ); }); diff --git a/src/__tests__/unit/storage/postgres/priceRequestSdkCall.test.ts b/src/__tests__/unit/storage/postgres/priceRequestSdkCall.test.ts index 47facb8..38bfcef 100644 --- a/src/__tests__/unit/storage/postgres/priceRequestSdkCall.test.ts +++ b/src/__tests__/unit/storage/postgres/priceRequestSdkCall.test.ts @@ -33,7 +33,8 @@ describe("PostgresAdapter - priceRequestSdkCall handler", () => { mockDb.groupBy.mockResolvedValueOnce([{ price: "1500" }]); const adapter = new PostgresAdapter(requestEvent); - const price = await adapter.price(); + const serialized = requestEvent.serialize(); + const price = await adapter.price(serialized); expect(price).toBe(1500); }); @@ -47,7 +48,8 @@ describe("PostgresAdapter - priceRequestSdkCall handler", () => { mockDb.groupBy.mockResolvedValueOnce([]); const adapter = new PostgresAdapter(requestEvent); - const price = await adapter.price(); + const serialized = requestEvent.serialize(); + const price = await adapter.price(serialized); expect(price).toBe(0); }); @@ -61,7 +63,8 @@ describe("PostgresAdapter - priceRequestSdkCall handler", () => { mockDb.groupBy.mockResolvedValueOnce([{ price: null }]); const adapter = new PostgresAdapter(requestEvent); - const price = await adapter.price(); + const serialized = requestEvent.serialize(); + const price = await adapter.price(serialized); expect(price).toBe(0); }); @@ -75,7 +78,8 @@ describe("PostgresAdapter - priceRequestSdkCall handler", () => { mockDb.groupBy.mockResolvedValueOnce([{ price: "12345" }]); const adapter = new PostgresAdapter(requestEvent); - const price = await adapter.price(); + const serialized = requestEvent.serialize(); + const price = await adapter.price(serialized); expect(typeof price).toBe("number"); expect(price).toBe(12345); @@ -99,7 +103,8 @@ describe("PostgresAdapter - priceRequestSdkCall handler", () => { }; const adapter = new PostgresAdapter(invalidEvent as any); - await expect(adapter.price()).rejects.toThrow("Missing userId"); + const serialized = invalidEvent.serialize() as any; + await expect(adapter.price(serialized)).rejects.toThrow("Missing userId"); }); it("throws error when userId is empty string", async () => { @@ -119,7 +124,10 @@ describe("PostgresAdapter - priceRequestSdkCall handler", () => { }; const adapter = new PostgresAdapter(invalidEvent as any); - await expect(adapter.price()).rejects.toThrow("Invalid userId format"); + const serialized = invalidEvent.serialize() as any; + await expect(adapter.price(serialized)).rejects.toThrow( + "Invalid userId format", + ); }); }); @@ -135,7 +143,8 @@ describe("PostgresAdapter - priceRequestSdkCall handler", () => { ); const adapter = new PostgresAdapter(requestEvent); - await expect(adapter.price()).rejects.toThrow(); + const serialized = requestEvent.serialize(); + await expect(adapter.price(serialized)).rejects.toThrow(); }); it("handles null query result", async () => { @@ -147,7 +156,8 @@ describe("PostgresAdapter - priceRequestSdkCall handler", () => { mockDb.groupBy.mockResolvedValueOnce(null); const adapter = new PostgresAdapter(requestEvent); - await expect(adapter.price()).rejects.toThrow( + const serialized = requestEvent.serialize(); + await expect(adapter.price(serialized)).rejects.toThrow( "Price query returned null", ); }); @@ -161,7 +171,8 @@ describe("PostgresAdapter - priceRequestSdkCall handler", () => { mockDb.groupBy.mockResolvedValueOnce({ price: "1500" }); const adapter = new PostgresAdapter(requestEvent); - await expect(adapter.price()).rejects.toThrow( + const serialized = requestEvent.serialize(); + await expect(adapter.price(serialized)).rejects.toThrow( "Query result is not an array", ); }); diff --git a/src/events/AIEvents/AITokenUsage.ts b/src/events/AIEvents/AITokenUsage.ts index 939110b..00e7e61 100644 --- a/src/events/AIEvents/AITokenUsage.ts +++ b/src/events/AIEvents/AITokenUsage.ts @@ -1,15 +1,17 @@ -import type { AITokenUsageEventType } from "../../interface/event/Event"; +import type { + AITokenUsageEvent, + AITokenUsageEventData, +} from "../../interface/event/Event"; import { DateTime } from "luxon"; -import type { EventUnion } from "../../interface/event/Event"; -import { type UserId } from "../../config/identifiers"; +import type { UserId } from "../../config/identifiers"; -export class AITokenUsage implements AITokenUsageEventType { +export class AITokenUsage implements AITokenUsageEvent { public reported_timestamp: DateTime; public readonly type = "AI_TOKEN_USAGE" as const; constructor( public userId: UserId, - public data: EventUnion<"AI_TOKEN_USAGE">, + public data: AITokenUsageEventData, ) { this.reported_timestamp = DateTime.utc(); } diff --git a/src/events/RawEvents/AddKey.ts b/src/events/RawEvents/AddKey.ts index e57f2e8..d6a01c3 100644 --- a/src/events/RawEvents/AddKey.ts +++ b/src/events/RawEvents/AddKey.ts @@ -1,8 +1,7 @@ -import type { AddKeyEventType } from "../../interface/event/Event"; +import type { AddKeyEvent, AddKeyEventData } from "../../interface/event/Event"; import { DateTime } from "luxon"; -import type { AddKeyEventData } from "../../interface/event/Event"; -export class AddKey implements AddKeyEventType { +export class AddKey implements AddKeyEvent { public reported_timestamp: DateTime; public readonly type = "ADD_KEY" as const; diff --git a/src/events/RawEvents/Payment.ts b/src/events/RawEvents/Payment.ts index 5101a5f..1ccfba9 100644 --- a/src/events/RawEvents/Payment.ts +++ b/src/events/RawEvents/Payment.ts @@ -1,9 +1,11 @@ -import type { PaymentEventType } from "../../interface/event/Event"; +import type { + PaymentEvent, + PaymentEventData, +} from "../../interface/event/Event"; import { DateTime } from "luxon"; -import type { PaymentEventData } from "../../interface/event/Event"; -import { type UserId } from "../../config/identifiers"; +import type { UserId } from "../../config/identifiers"; -export class Payment implements PaymentEventType { +export class Payment implements PaymentEvent { public reported_timestamp: DateTime; public readonly type = "PAYMENT" as const; diff --git a/src/events/RawEvents/SDKCall.ts b/src/events/RawEvents/SDKCall.ts index 9a085be..005cd9b 100644 --- a/src/events/RawEvents/SDKCall.ts +++ b/src/events/RawEvents/SDKCall.ts @@ -1,15 +1,17 @@ -import type { SDKCallEventType } from "../../interface/event/Event"; +import type { + SDKCallEvent, + SDKCallEventData, +} from "../../interface/event/Event"; import { DateTime } from "luxon"; -import type { EventUnion } from "../../interface/event/Event"; -import { type UserId } from "../../config/identifiers"; +import type { UserId } from "../../config/identifiers"; -export class SDKCall implements SDKCallEventType { +export class SDKCall implements SDKCallEvent { public reported_timestamp: DateTime; public readonly type = "SDK_CALL" as const; constructor( public userId: UserId, - public data: EventUnion<"SDK_CALL">, + public data: SDKCallEventData, ) { this.reported_timestamp = DateTime.utc(); } diff --git a/src/events/RequestEvents/RequestPayment.ts b/src/events/RequestEvents/RequestPayment.ts index 742d351..af034e7 100644 --- a/src/events/RequestEvents/RequestPayment.ts +++ b/src/events/RequestEvents/RequestPayment.ts @@ -1,15 +1,16 @@ import type { - RequestPaymentEventType, + RequestPaymentEvent, RequestPaymentEventData, } from "../../interface/event/Event"; import { DateTime } from "luxon"; +import type { UserId } from "../../config/identifiers"; -export class RequestPayment implements RequestPaymentEventType { +export class RequestPayment implements RequestPaymentEvent { public reported_timestamp: DateTime; public readonly type = "REQUEST_PAYMENT" as const; constructor( - public userId: string, + public userId: UserId, public data: RequestPaymentEventData, ) { this.reported_timestamp = DateTime.utc(); diff --git a/src/events/RequestEvents/RequestSDKCall.ts b/src/events/RequestEvents/RequestSDKCall.ts index 9a03dad..b22e7d1 100644 --- a/src/events/RequestEvents/RequestSDKCall.ts +++ b/src/events/RequestEvents/RequestSDKCall.ts @@ -1,15 +1,16 @@ import type { RequestSDKCallEventData, - RequestSDKCallEventType, + RequestSDKCallEvent, } from "../../interface/event/Event"; import { DateTime } from "luxon"; +import type { UserId } from "../../config/identifiers"; -export class RequestSDKCall implements RequestSDKCallEventType { +export class RequestSDKCall implements RequestSDKCallEvent { public reported_timestamp: DateTime; public readonly type = "REQUEST_SDK_CALL" as const; constructor( - public userId: string, + public userId: UserId, public data: RequestSDKCallEventData, ) { this.reported_timestamp = DateTime.utc(); diff --git a/src/factory/StorageAdapterFactory.ts b/src/factory/StorageAdapterFactory.ts index 9e9874a..3b4062b 100644 --- a/src/factory/StorageAdapterFactory.ts +++ b/src/factory/StorageAdapterFactory.ts @@ -1,4 +1,4 @@ -import type { EventType } from "../interface/event/Event.ts"; +import type { Event } from "../interface/event/Event.ts"; import { PostgresAdapter } from "../storage/adapter/postgres/postgres.ts"; /** @@ -15,7 +15,7 @@ export class StorageAdapterFactory { * @param apiKeyId - Optional API key ID to associate with the event * @returns The storage adapter instance for the event type */ - public static async getStorageAdapter(event: EventType, apiKeyId?: string) { + public static async getStorageAdapter(event: Event, apiKeyId?: string) { switch (event.type) { case "SDK_CALL": { return new PostgresAdapter(event, apiKeyId); diff --git a/src/interface/event/Event.ts b/src/interface/event/Event.ts index 603e582..8fc7327 100644 --- a/src/interface/event/Event.ts +++ b/src/interface/event/Event.ts @@ -1,6 +1,9 @@ -import { DateTime } from "luxon"; -import { type UserId } from "../../config/identifiers"; +import type { DateTime } from "luxon"; +import type { UserId } from "../../config/identifiers"; +/** + * Event payload data structures + */ export type SDKCallEventData = { sdkCallType: "RAW" | "MIDDLEWARE_CALL"; debitAmount: number; @@ -29,7 +32,18 @@ export type RequestPaymentEventData = null; export type RequestSDKCallEventData = null; /** - * Mapping of event types to their data structures + * Event kind discriminator + */ +export type EventKind = + | "SDK_CALL" + | "AI_TOKEN_USAGE" + | "ADD_KEY" + | "PAYMENT" + | "REQUEST_PAYMENT" + | "REQUEST_SDK_CALL"; + +/** + * Mapping of event kinds to their data structures */ export type EventDataMap = { SDK_CALL: SDKCallEventData; @@ -40,84 +54,98 @@ export type EventDataMap = { REQUEST_SDK_CALL: RequestSDKCallEventData; }; -export type EventUnion = { - [K in keyof EventDataMap]: EventDataMap[K]; -}[T]; +/** + * Get event data type for a specific event kind + */ +export type EventData = EventDataMap[K]; -export type BaseEventMetadata = { - type: T; +/** + * Base SQL record structure for all events + */ +type BaseSqlRecord = { + type: K; reported_timestamp: DateTime; - data: EventDataMap[T]; + data: EventData; }; -type EventMetadataMap = { - ADD_KEY: BaseEventMetadata<"ADD_KEY">; - SDK_CALL: BaseEventMetadata<"SDK_CALL"> & { userId: UserId }; - AI_TOKEN_USAGE: BaseEventMetadata<"AI_TOKEN_USAGE"> & { userId: UserId }; - PAYMENT: BaseEventMetadata<"PAYMENT"> & { userId: UserId }; - REQUEST_PAYMENT: BaseEventMetadata<"REQUEST_PAYMENT"> & { userId: UserId }; - REQUEST_SDK_CALL: BaseEventMetadata<"REQUEST_SDK_CALL"> & { userId: UserId }; +/** + * SQL record structure for events that require userId + */ +type SqlRecordWithUserId = BaseSqlRecord & { + userId: UserId; }; -type EventStorageAdapterMap = { - SQL: { - [K in keyof EventDataMap]: EventMetadataMap[K]; - }[Type]; +/** + * Mapping of event kinds to their SQL record structures + */ +type SqlRecordMap = { + ADD_KEY: BaseSqlRecord<"ADD_KEY">; + SDK_CALL: SqlRecordWithUserId<"SDK_CALL">; + AI_TOKEN_USAGE: SqlRecordWithUserId<"AI_TOKEN_USAGE">; + PAYMENT: SqlRecordWithUserId<"PAYMENT">; + REQUEST_PAYMENT: SqlRecordWithUserId<"REQUEST_PAYMENT">; + REQUEST_SDK_CALL: SqlRecordWithUserId<"REQUEST_SDK_CALL">; }; -type EventStorageAdapterUnion = { - [K in keyof EventDataMap]: EventStorageAdapterMap; -}[T]; +/** + * Get SQL record type for a specific event kind + */ +export type SqlRecord = SqlRecordMap[K]; + +/** + * Serialized event format (wrapped in SQL adapter envelope) + */ +export type SerializedEvent = { + SQL: SqlRecord; +}; /** - * Base Event interface - all events in the system extend this + * Base Event interface - all events in the system implement this */ -export interface EventType< - Type extends keyof EventDataMap = keyof EventDataMap, -> { - type: Type; +export interface Event { + readonly type: K; readonly reported_timestamp: DateTime; - readonly data: EventDataMap[Type]; + readonly data: EventData; - serialize(): EventStorageAdapterUnion; + serialize(): SerializedEvent; } /** * SDK Call Event */ -export interface SDKCallEventType extends EventType<"SDK_CALL"> { +export interface SDKCallEvent extends Event<"SDK_CALL"> { readonly userId: UserId; } /** * AI Token Usage Event */ -export interface AITokenUsageEventType extends EventType<"AI_TOKEN_USAGE"> { +export interface AITokenUsageEvent extends Event<"AI_TOKEN_USAGE"> { readonly userId: UserId; } /** * Add Key Event */ -export interface AddKeyEventType extends EventType<"ADD_KEY"> {} +export interface AddKeyEvent extends Event<"ADD_KEY"> {} /** * Payment Event */ -export interface PaymentEventType extends EventType<"PAYMENT"> { +export interface PaymentEvent extends Event<"PAYMENT"> { readonly userId: UserId; } /** * Payment Request Event */ -export interface RequestPaymentEventType extends EventType<"REQUEST_PAYMENT"> { - readonly userId: string; +export interface RequestPaymentEvent extends Event<"REQUEST_PAYMENT"> { + readonly userId: UserId; } /** * SDK Call Request Event */ -export interface RequestSDKCallEventType extends EventType<"REQUEST_SDK_CALL"> { - readonly userId: string; +export interface RequestSDKCallEvent extends Event<"REQUEST_SDK_CALL"> { + readonly userId: UserId; } diff --git a/src/interface/storage/SQLStorageAdapter.ts b/src/interface/storage/SQLStorageAdapter.ts deleted file mode 100644 index bd948d2..0000000 --- a/src/interface/storage/SQLStorageAdapter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { EventType } from "../event/Event"; - -/** - * SQLStorageAdapter - Base interface for SQL-based storage implementations - * This is database-agnostic and can be implemented by PostgreSQL, SQLite, MySQL, etc. - */ -export interface SQLStorageAdapter { - readonly name: "POSTGRES" | "SQLITE" | "MYSQL"; - readonly connectionObject: unknown; - readonly event: EventType; - - add(): Promise; -} diff --git a/src/interface/storage/Storage.ts b/src/interface/storage/Storage.ts index 2f37e17..01685b0 100644 --- a/src/interface/storage/Storage.ts +++ b/src/interface/storage/Storage.ts @@ -1,13 +1,12 @@ -import type { EventType } from "../event/Event"; +import type { SerializedEvent, EventKind } from "../event/Event"; /** - * Storage - Consumes events + * Storage Adapter - consumes and persists events */ -export interface StorageAdapterType { +export interface StorageAdapter { name: string; connectionObject: unknown; - event: EventType; - add(): Promise<{ id: string } | void>; - price(): Promise; + add(serialized: SerializedEvent): Promise<{ id: string } | void>; + price(serialized: SerializedEvent): Promise; } diff --git a/src/routes/gRPC/auth/createAPIKey.ts b/src/routes/gRPC/auth/createAPIKey.ts index 34ccd2b..da592ae 100644 --- a/src/routes/gRPC/auth/createAPIKey.ts +++ b/src/routes/gRPC/auth/createAPIKey.ts @@ -81,7 +81,7 @@ export async function createAPIKey( try { const adapter = await StorageAdapterFactory.getStorageAdapter(addKeyEvent); - keyEventData = await adapter.add(); + keyEventData = await adapter.add(addKeyEvent.serialize()); if (!keyEventData) { throw APIKeyError.creationFailed("No ID returned"); } diff --git a/src/routes/gRPC/events/streamEvents.ts b/src/routes/gRPC/events/streamEvents.ts index b83fc97..a80df2e 100644 --- a/src/routes/gRPC/events/streamEvents.ts +++ b/src/routes/gRPC/events/streamEvents.ts @@ -2,7 +2,7 @@ import type { StreamEventRequest, StreamEventResponse, } from "../../../gen/event/v1/event_pb"; -import type { EventType, EventDataMap } from "../../../interface/event/Event"; +import type { Event, EventKind } from "../../../interface/event/Event"; import { StreamEventResponseSchema } from "../../../gen/event/v1/event_pb"; import { create } from "@bufbuild/protobuf"; import { EventError } from "../../../errors/event"; @@ -23,7 +23,7 @@ export async function streamEvents( context: HandlerContext, ): Promise { let eventsProcessed = 0; - const events: Array> = []; + const events: Array> = []; try { // Extract API key ID from context diff --git a/src/routes/gRPC/payment/createCheckoutLink.ts b/src/routes/gRPC/payment/createCheckoutLink.ts index 528ab36..29c4e24 100644 --- a/src/routes/gRPC/payment/createCheckoutLink.ts +++ b/src/routes/gRPC/payment/createCheckoutLink.ts @@ -100,9 +100,9 @@ export async function createCheckoutLink( // Get custom price from storage let custom_price: number; try { - const storageAdapter = await StorageAdapterFactory.getStorageAdapter( - new RequestPayment(validatedData.userId, null), - ); + const event = new RequestPayment(validatedData.userId, null); + const storageAdapter = + await StorageAdapterFactory.getStorageAdapter(event); if (!storageAdapter) { throw PaymentError.storageAdapterFailed( @@ -110,7 +110,7 @@ export async function createCheckoutLink( ); } - custom_price = await storageAdapter.price(); + custom_price = await storageAdapter.price(event.serialize()); if ( typeof custom_price !== "number" || diff --git a/src/routes/http/createdCheckout.ts b/src/routes/http/createdCheckout.ts index 8f40b5e..388c244 100644 --- a/src/routes/http/createdCheckout.ts +++ b/src/routes/http/createdCheckout.ts @@ -3,7 +3,6 @@ import type { Http2ServerRequest, Http2ServerResponse } from "node:http2"; import crypto from "node:crypto"; import { lemonSqueezySetup } from "@lemonsqueezy/lemonsqueezy.js"; import { Payment } from "../../events/RawEvents/Payment.ts"; -import { PostgresAdapter } from "../../storage/adapter/postgres/postgres.ts"; import { StorageAdapterFactory } from "../../factory/StorageAdapterFactory.ts"; import { logger } from "../../errors/logger.ts"; @@ -256,7 +255,7 @@ export async function handleLemonSqueezyWebhook( apiKeyId, ); - await adapter.add(); + await adapter.add(paymentEvent.serialize()); logger.logOperationInfo( OPERATION, diff --git a/src/storage/adapter/postgres/handlers/addAiTokenUsage.ts b/src/storage/adapter/postgres/handlers/addAiTokenUsage.ts index aa10290..a5284f0 100644 --- a/src/storage/adapter/postgres/handlers/addAiTokenUsage.ts +++ b/src/storage/adapter/postgres/handlers/addAiTokenUsage.ts @@ -5,8 +5,8 @@ import { aiTokenUsageEventsTable, } from "../../../db/postgres/schema"; import { StorageError } from "../../../../errors/storage"; -import { type BaseEventMetadata } from "../../../../interface/event/Event"; -import { type UserId } from "../../../../config/identifiers"; +import { type SqlRecord } from "../../../../interface/event/Event"; +import type { UserId } from "../../../../config/identifiers"; import { logger } from "../../../../errors/logger"; const OPERATION = "AddAiTokenUsage"; @@ -22,11 +22,7 @@ type AggregatedEvent = { }; export async function handleAddAiTokenUsage( - events: Array< - BaseEventMetadata<"AI_TOKEN_USAGE"> & { - userId: UserId; - } - >, + events: Array>, apiKeyId: string, ): Promise<{ id: string } | void> { const connectionObject = getPostgresDB(); diff --git a/src/storage/adapter/postgres/handlers/addKey.ts b/src/storage/adapter/postgres/handlers/addKey.ts index f67dd3a..aec8ee5 100644 --- a/src/storage/adapter/postgres/handlers/addKey.ts +++ b/src/storage/adapter/postgres/handlers/addKey.ts @@ -1,13 +1,13 @@ import { getPostgresDB } from "../../../db/postgres/db"; import { apiKeysTable } from "../../../db/postgres/schema"; import { StorageError } from "../../../../errors/storage"; -import { type BaseEventMetadata } from "../../../../interface/event/Event"; +import { type SqlRecord } from "../../../../interface/event/Event"; import { logger } from "../../../../errors/logger"; const OPERATION = "AddKey"; export async function handleAddKey( - event_data: BaseEventMetadata<"ADD_KEY">, + event_data: SqlRecord<"ADD_KEY">, ): Promise<{ id: string } | void> { const connectionObject = getPostgresDB(); diff --git a/src/storage/adapter/postgres/handlers/addPayment.ts b/src/storage/adapter/postgres/handlers/addPayment.ts index 2339558..01e03f7 100644 --- a/src/storage/adapter/postgres/handlers/addPayment.ts +++ b/src/storage/adapter/postgres/handlers/addPayment.ts @@ -5,16 +5,13 @@ import { paymentEventsTable, } from "../../../db/postgres/schema"; import { StorageError } from "../../../../errors/storage"; -import { type BaseEventMetadata } from "../../../../interface/event/Event"; -import { type UserId } from "../../../../config/identifiers"; +import { type SqlRecord } from "../../../../interface/event/Event"; import { logger } from "../../../../errors/logger"; const OPERATION = "AddPayment"; export async function handleAddPayment( - event_data: BaseEventMetadata<"PAYMENT"> & { - userId: UserId; - }, + event_data: SqlRecord<"PAYMENT">, apiKeyId?: string, ): Promise<{ id: string } | void> { const connectionObject = getPostgresDB(); diff --git a/src/storage/adapter/postgres/handlers/addSdkCall.ts b/src/storage/adapter/postgres/handlers/addSdkCall.ts index 22e4995..e9c52b2 100644 --- a/src/storage/adapter/postgres/handlers/addSdkCall.ts +++ b/src/storage/adapter/postgres/handlers/addSdkCall.ts @@ -5,16 +5,13 @@ import { sdkCallEventsTable, } from "../../../db/postgres/schema"; import { StorageError } from "../../../../errors/storage"; -import { type BaseEventMetadata } from "../../../../interface/event/Event"; -import { type UserId } from "../../../../config/identifiers"; +import { type SqlRecord } from "../../../../interface/event/Event"; import { logger } from "../../../../errors/logger"; const OPERATION = "AddSdkCall"; export async function handleAddSdkCall( - event_data: BaseEventMetadata<"SDK_CALL"> & { - userId: UserId; - }, + event_data: SqlRecord<"SDK_CALL">, apiKeyId: string, ): Promise<{ id: string } | void> { const connectionObject = getPostgresDB(); diff --git a/src/storage/adapter/postgres/handlers/priceRequestPayment.ts b/src/storage/adapter/postgres/handlers/priceRequestPayment.ts index a01ba93..0e5ca04 100644 --- a/src/storage/adapter/postgres/handlers/priceRequestPayment.ts +++ b/src/storage/adapter/postgres/handlers/priceRequestPayment.ts @@ -1,16 +1,13 @@ import { StorageError } from "../../../../errors/storage"; import { RequestSDKCall } from "../../../../events/RequestEvents/RequestSDKCall"; import { StorageAdapterFactory } from "../../../../factory"; -import { type BaseEventMetadata } from "../../../../interface/event/Event"; -import { type UserId } from "../../../../config/identifiers"; +import { type SqlRecord } from "../../../../interface/event/Event"; import { logger } from "../../../../errors/logger"; const OPERATION = "PriceRequestPayment"; export async function handlePriceRequestPayment( - event_data: BaseEventMetadata<"REQUEST_PAYMENT"> & { - userId: UserId; - }, + event_data: SqlRecord<"REQUEST_PAYMENT">, ): Promise { try { if (!event_data.userId) { @@ -24,9 +21,8 @@ export async function handlePriceRequestPayment( { userId: event_data.userId }, ); - const storageAdapter = await StorageAdapterFactory.getStorageAdapter( - new RequestSDKCall(event_data.userId, null), - ); + const event = new RequestSDKCall(event_data.userId, null); + const storageAdapter = await StorageAdapterFactory.getStorageAdapter(event); if (!storageAdapter) { throw StorageError.unknown( @@ -34,7 +30,7 @@ export async function handlePriceRequestPayment( ); } - const price = await storageAdapter.price(); + const price = await storageAdapter.price(event.serialize()); if (typeof price !== "number" || isNaN(price)) { throw StorageError.priceCalculationFailed( diff --git a/src/storage/adapter/postgres/handlers/priceRequestSdkCall.ts b/src/storage/adapter/postgres/handlers/priceRequestSdkCall.ts index ab6db02..1b69af6 100644 --- a/src/storage/adapter/postgres/handlers/priceRequestSdkCall.ts +++ b/src/storage/adapter/postgres/handlers/priceRequestSdkCall.ts @@ -2,16 +2,13 @@ import { getPostgresDB } from "../../../db/postgres/db"; import { sdkCallEventsTable, eventsTable } from "../../../db/postgres/schema"; import { StorageError } from "../../../../errors/storage"; import { eq, sum } from "drizzle-orm"; -import { type BaseEventMetadata } from "../../../../interface/event/Event"; -import { type UserId } from "../../../../config/identifiers"; +import { type SqlRecord } from "../../../../interface/event/Event"; import { logger } from "../../../../errors/logger"; const OPERATION = "PriceRequestSdkCall"; export async function handlePriceRequestSdkCall( - event_data: BaseEventMetadata<"REQUEST_SDK_CALL"> & { - userId: UserId; - }, + event_data: SqlRecord<"REQUEST_SDK_CALL">, ): Promise { const connectionObject = getPostgresDB(); diff --git a/src/storage/adapter/postgres/postgres.ts b/src/storage/adapter/postgres/postgres.ts index d6e03be..dac6a64 100644 --- a/src/storage/adapter/postgres/postgres.ts +++ b/src/storage/adapter/postgres/postgres.ts @@ -1,5 +1,5 @@ -import { type StorageAdapterType } from "../../../interface/storage/Storage"; -import { type EventType } from "../../../interface/event/Event"; +import { type StorageAdapter } from "../../../interface/storage/Storage"; +import { type Event } from "../../../interface/event/Event"; import { getPostgresDB } from "../../db/postgres/db"; import { StorageError } from "../../../errors/storage"; import { @@ -10,26 +10,27 @@ import { handlePriceRequestSdkCall, handleAddAiTokenUsage, } from "./handlers"; -import { logger } from "../../../errors/logger"; +import type { + SerializedEvent, + EventKind, +} from "../../../interface/event/Event"; -export class PostgresAdapter implements StorageAdapterType { +export class PostgresAdapter implements StorageAdapter { name: string; connectionObject; - event: EventType; apiKeyId?: string; - constructor(event: EventType, apiKeyId?: string) { + constructor(event: Event, apiKeyId?: string) { this.name = event.type; this.connectionObject = getPostgresDB(); - this.event = event; this.apiKeyId = apiKeyId; } - async add(): Promise<{ id: string } | void> { + async add(serialized: SerializedEvent) { let event_data; try { - const { SQL } = this.event.serialize(); + const { SQL } = serialized; event_data = SQL; if (!event_data) { @@ -82,11 +83,11 @@ export class PostgresAdapter implements StorageAdapterType { } } - async price(): Promise { + async price(serialized: SerializedEvent): Promise { let event_data; try { - const { SQL } = this.event.serialize(); + const { SQL } = serialized; event_data = SQL; if (!event_data) { diff --git a/src/utils/eventHelpers.ts b/src/utils/eventHelpers.ts index 887da9b..ed3f071 100644 --- a/src/utils/eventHelpers.ts +++ b/src/utils/eventHelpers.ts @@ -4,7 +4,7 @@ import { AuthError } from "../errors/auth"; import { EventError } from "../errors/event"; import { eventSchema } from "../zod/event"; import { ZodError } from "zod"; -import type { EventType } from "../interface/event/Event"; +import type { Event, SqlRecord } from "../interface/event/Event"; import { SDKCall } from "../events/RawEvents/SDKCall"; import { AITokenUsage } from "../events/AIEvents/AITokenUsage"; import { StorageAdapterFactory } from "../factory"; @@ -58,7 +58,7 @@ export function createEventInstance(eventSkeleton: { type: string; userId: string; data: any; -}): EventType { +}): Event { try { switch (eventSkeleton.type) { case "SDK_CALL": @@ -80,7 +80,7 @@ export function createEventInstance(eventSkeleton: { * Store the event using the appropriate storage adapter */ export async function storeEvent( - event: EventType, + event: Event, apiKeyId: string, ): Promise { try { @@ -88,7 +88,7 @@ export async function storeEvent( event, apiKeyId, ); - await adapter.add(); + await adapter.add(event.serialize()); } catch (error) { throw EventError.serializationError( "Failed to store event", @@ -101,7 +101,7 @@ export async function storeEvent( * Store multiple events in a batch - groups by type and uses batch operations when possible */ export async function storeEventsBatch( - events: EventType[], + events: Event[], apiKeyId: string, ): Promise { if (events.length === 0) { @@ -109,7 +109,7 @@ export async function storeEventsBatch( } // Group events by type - const eventsByType = new Map(); + const eventsByType = new Map(); for (const event of events) { const type = event.type; if (!eventsByType.has(type)) { @@ -123,11 +123,7 @@ export async function storeEventsBatch( try { if (type === "AI_TOKEN_USAGE") { // Batch process AI_TOKEN_USAGE events - const serializedEvents: Array< - import("../interface/event/Event").BaseEventMetadata<"AI_TOKEN_USAGE"> & { - userId: string; - } - > = []; + const serializedEvents: Array> = []; for (const event of typeEvents) { const { SQL } = event.serialize(); @@ -141,7 +137,7 @@ export async function storeEventsBatch( `Expected AI_TOKEN_USAGE but got ${SQL.type}`, ); } - serializedEvents.push(SQL as any); + serializedEvents.push(SQL as SqlRecord<"AI_TOKEN_USAGE">); } await handleAddAiTokenUsage(serializedEvents, apiKeyId);