-
Notifications
You must be signed in to change notification settings - Fork 20
feat: add deterministic identity storage for indexed events #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| # Indexed Event System - Example Usage | ||
|
|
||
| ## Overview | ||
|
|
||
| The indexed event system provides deterministic identity storage for Soroban events | ||
| to prevent duplicates during retries, restarts, and replays. | ||
|
|
||
| ## Basic Usage | ||
|
|
||
| ### 1. Initialize Database Connection | ||
|
|
||
| ```typescript | ||
| import { initializeIndexerDataSource } from "@fundable-indexer/common/db"; | ||
|
|
||
| const config = { | ||
| INDEXER_DATABASE_HOST: process.env.INDEXER_DATABASE_HOST, | ||
| INDEXER_DATABASE_PORT: process.env.INDEXER_DATABASE_PORT, | ||
| INDEXER_DATABASE_USERNAME: process.env.INDEXER_DATABASE_USERNAME, | ||
| INDEXER_DATABASE_PASSWORD: process.env.INDEXER_DATABASE_PASSWORD, | ||
| INDEXER_DATABASE_NAME: process.env.INDEXER_DATABASE_NAME, | ||
| INDEXER_DATABASE_SSL: process.env.INDEXER_DATABASE_SSL, | ||
| }; | ||
|
|
||
| const dataSource = await initializeIndexerDataSource(config); | ||
| ``` | ||
|
|
||
| ### 2. Store Events with Deduplication | ||
|
|
||
| ```typescript | ||
| import { IndexedEventRepository } from "@fundable-indexer/common/db"; | ||
|
|
||
| const repository = new IndexedEventRepository(dataSource); | ||
|
|
||
| const eventData = { | ||
| contractId: "CAFEBABE", | ||
| ledgerNumber: BigInt(123456), | ||
| transactionHash: "0x1234567890abcdef", | ||
| eventIndex: 0, | ||
| eventData: { | ||
| type: "PaymentStreamCreated", | ||
| streamId: "stream-123", | ||
| amount: "1000000", | ||
| }, | ||
| eventTopics: ["PaymentStreamCreated", "CAFEBABE"], | ||
| processedBy: "streams", | ||
| }; | ||
|
|
||
| // Safe insert - won't create duplicates | ||
| const storedEvent = await repository.insertSafely(eventData); | ||
| console.log(`Event stored with ID: ${storedEvent.id}`); | ||
| ``` | ||
|
|
||
| ### 3. Check if Event is Already Processed | ||
|
|
||
| ```typescript | ||
| const isProcessed = await repository.isProcessed( | ||
| "CAFEBABE", | ||
| BigInt(123456), | ||
| "0x1234567890abcdef", | ||
| 0, | ||
| ); | ||
|
|
||
| if (isProcessed) { | ||
| console.log("Event already processed, skipping..."); | ||
| } else { | ||
| console.log("Event not yet processed, handling..."); | ||
| } | ||
| ``` | ||
|
|
||
| ### 4. Query Events for Replay/Debug | ||
|
|
||
| ```typescript | ||
| // Get events for a ledger range | ||
| const events = await repository.getByLedgerRange( | ||
| BigInt(123000), | ||
| BigInt(124000), | ||
| "streams", | ||
| ); | ||
|
|
||
| // Get latest processed ledger | ||
| const latestLedger = await repository.getLatestLedger("streams"); | ||
| console.log(`Latest processed ledger: ${latestLedger}`); | ||
|
|
||
| // Get events for a specific contract | ||
| const contractEvents = await repository.getByContract("CAFEBABE", 10); | ||
| ``` | ||
|
|
||
| ## Migration | ||
|
|
||
| The `CreateIndexedEventTable1704000000001` migration creates the table with: | ||
|
|
||
| 1. Unique constraint on `(contract_id, ledger_number, transaction_hash, event_index)` | ||
| 2. Indexes for efficient queries by contract, ledger, transaction hash, and domain | ||
| 3. Composite indexes for common query patterns | ||
|
|
||
| Run the migration: | ||
| ```bash | ||
| bun run migration:run | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
|
||
| Run the tests: | ||
| ```bash | ||
| bun run indexer:test | ||
| ``` | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| Add to your `.env` file: | ||
| ```env | ||
| # Indexer Database Configuration | ||
| INDEXER_DATABASE_HOST=localhost | ||
| INDEXER_DATABASE_PORT=5432 | ||
| INDEXER_DATABASE_USERNAME=postgres | ||
| INDEXER_DATABASE_PASSWORD=postgres | ||
| INDEXER_DATABASE_NAME=fundable_indexer | ||
| INDEXER_DATABASE_SSL=false | ||
| ``` |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,108 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import "reflect-metadata"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { DataSource } from "typeorm"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { IndexedEventEntity } from "./indexed-event.entity.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Environment variables required for indexer database connection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface IndexerDatabaseConfig { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INDEXER_DATABASE_HOST: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INDEXER_DATABASE_PORT: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INDEXER_DATABASE_USERNAME: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INDEXER_DATABASE_PASSWORD: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INDEXER_DATABASE_NAME: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INDEXER_DATABASE_SSL?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Validate required database configuration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function validateIndexerDatabaseConfig( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config: Partial<IndexerDatabaseConfig>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): asserts config is IndexerDatabaseConfig { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const missingKeys = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["INDEXER_DATABASE_HOST", config.INDEXER_DATABASE_HOST], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["INDEXER_DATABASE_PORT", config.INDEXER_DATABASE_PORT], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["INDEXER_DATABASE_USERNAME", config.INDEXER_DATABASE_USERNAME], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["INDEXER_DATABASE_PASSWORD", config.INDEXER_DATABASE_PASSWORD], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["INDEXER_DATABASE_NAME", config.INDEXER_DATABASE_NAME], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(([, value]) => !value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(([key]) => key); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (missingKeys.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `Missing required indexer database env vars: ${missingKeys.join(", ")}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Create indexer data source | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function createIndexerDataSource( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config: IndexerDatabaseConfig, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): DataSource { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const port = Number(config.INDEXER_DATABASE_PORT); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const useSsl = config.INDEXER_DATABASE_SSL === "true"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new DataSource({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| host: config.INDEXER_DATABASE_HOST, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| port: Number.isNaN(port) ? 5432 : port, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| username: config.INDEXER_DATABASE_USERNAME, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| password: config.INDEXER_DATABASE_PASSWORD, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| database: config.INDEXER_DATABASE_NAME, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "postgres", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| connectTimeoutMS: 5000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| synchronize: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logging: process.env.NODE_ENV === "development", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entities: [IndexedEventEntity], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| migrations: ["src/migrations/*.js"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...(useSsl ? { ssl: { rejectUnauthorized: false } } : {}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Do not disable TLS certificate verification by default. Using Suggested fix- ...(useSsl ? { ssl: { rejectUnauthorized: false } } : {}),
+ ...(useSsl ? { ssl: true } : {}),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Singleton instance of indexer data source | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let indexerDataSource: DataSource | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Get or initialize the indexer data source | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function getIndexerDataSource(): DataSource { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!indexerDataSource) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Indexer data source not initialized. Call initializeIndexerDataSource first.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return indexerDataSource; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Initialize indexer data source with environment configuration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function initializeIndexerDataSource( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config: IndexerDatabaseConfig, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<DataSource> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validateIndexerDatabaseConfig(config); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (indexerDataSource?.isInitialized) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return indexerDataSource; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| indexerDataSource = createIndexerDataSource(config); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await indexerDataSource.initialize(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return indexerDataSource; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+85
to
+97
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Guard singleton initialization against concurrent calls. Concurrent Suggested fix let indexerDataSource: DataSource | null = null;
+let indexerDataSourceInitPromise: Promise<DataSource> | null = null;
@@
export async function initializeIndexerDataSource(
config: IndexerDatabaseConfig,
): Promise<DataSource> {
validateIndexerDatabaseConfig(config);
-
+
if (indexerDataSource?.isInitialized) {
return indexerDataSource;
}
+ if (indexerDataSourceInitPromise) {
+ return indexerDataSourceInitPromise;
+ }
- indexerDataSource = createIndexerDataSource(config);
- await indexerDataSource.initialize();
-
- return indexerDataSource;
+ indexerDataSource = createIndexerDataSource(config);
+ indexerDataSourceInitPromise = indexerDataSource.initialize().then(() => indexerDataSource!);
+ try {
+ await indexerDataSourceInitPromise;
+ return indexerDataSource;
+ } finally {
+ indexerDataSourceInitPromise = null;
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Close indexer data source connection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function closeIndexerDataSource(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (indexerDataSource?.isInitialized) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await indexerDataSource.destroy(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| indexerDataSource = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| export { IndexedEventEntity } from "./indexed-event.entity.js"; | ||
| export { | ||
| IndexedEventRepository, | ||
| type IndexedEventData, | ||
| } from "./indexed-event.repository.js"; | ||
| export { | ||
| type IndexerDatabaseConfig, | ||
| validateIndexerDatabaseConfig, | ||
| createIndexerDataSource, | ||
| getIndexerDataSource, | ||
| initializeIndexerDataSource, | ||
| closeIndexerDataSource, | ||
| } from "./data-source.js"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { describe, expect, test, beforeEach } from "vitest"; | ||
| import { IndexedEventEntity } from "./indexed-event.entity.js"; | ||
|
|
||
| describe("IndexedEventEntity", () => { | ||
| let entity: IndexedEventEntity; | ||
|
|
||
| beforeEach(() => { | ||
| entity = new IndexedEventEntity(); | ||
| entity.contractId = "CAFEBABE"; | ||
| entity.ledgerNumber = BigInt(123456); | ||
| entity.transactionHash = "0x1234567890abcdef"; | ||
| entity.eventIndex = 0; | ||
| entity.eventData = { type: "TestEvent", amount: "100" }; | ||
| entity.eventTopics = ["topic1", "topic2"]; | ||
| entity.processedBy = "streams"; | ||
| }); | ||
|
|
||
| test("generates ULID when id is not provided", () => { | ||
| entity.generateId(); | ||
| expect(entity.id).toBeDefined(); | ||
| expect(entity.id.length).toBe(26); // ULID length | ||
| expect(entity.id).toMatch(/^[0-9A-Z]{26}$/); | ||
| }); | ||
|
|
||
| test("preserves existing id", () => { | ||
| const existingId = "01J0XYZABCDEFGHIJKLMNOPQR"; | ||
| entity.id = existingId; | ||
| entity.generateId(); | ||
| expect(entity.id).toBe(existingId); | ||
| }); | ||
|
|
||
| test("has required fields for deterministic identity", () => { | ||
| expect(entity.contractId).toBe("CAFEBABE"); | ||
| expect(entity.ledgerNumber).toBe(BigInt(123456)); | ||
| expect(entity.transactionHash).toBe("0x1234567890abcdef"); | ||
| expect(entity.eventIndex).toBe(0); | ||
| }); | ||
|
|
||
| test("has JSON data fields", () => { | ||
| expect(entity.eventData).toEqual({ type: "TestEvent", amount: "100" }); | ||
| expect(entity.eventTopics).toEqual(["topic1", "topic2"]); | ||
| }); | ||
|
|
||
| test("has metadata fields", () => { | ||
| expect(entity.processedBy).toBe("streams"); | ||
| expect(entity.createdAt).toBeUndefined(); // Will be set by DB | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
Repository: Fundable-Protocol/Backend
Length of output: 484
Add
reflect-metadatatopackage.jsondependencies.The file
indexer/common/src/db/data-source.tsexplicitly importsreflect-metadata. Since this file is exposed via the new./dbentrypoint inindexer/common/package.json, consumers using this module will encounter a runtime failure (ReferenceError) ifreflect-metadatais not installed in their ownnode_modules.🤖 Prompt for AI Agents