Feat/indexer db foundation - #51
Conversation
…igrations) Adds the PostgreSQL-backed persistence foundation for the indexer in the `@fundable-indexer/common` package, covering issues #20-#23: #20 Database tooling - Add exact-pinned deps: drizzle-orm 0.45.2, postgres 3.4.9, zod 3.25.67, drizzle-kit 0.31.10 (dev); update bun.lock - Add drizzle.config.ts (postgresql, schema entrypoint, ./migrations out) - Add initial migrations/ directory with Drizzle journal - Add db:generate / db:migrate scripts (package + root indexer:db:* ) - Document the migration workflow in indexer/README.md #21 Config loader + validation - Add zod-based loadConfig in common/src/config; validates required INDEXER_DATABASE_URL, parses numeric INDEXER_PORT / POLL_INTERVAL_MS / START_LEDGER, aggregates problems into a clear ConfigValidationError #22 Connection factory - Add createDbClient in common/src/db wrapping postgres.js + Drizzle, reading from validated config, with an injectable sql factory for tests #23 Health check - Add checkDbHealth running `select 1`, returning typed healthy/unhealthy results without throwing Also scope root ESLint away from the indexer workspace (it is linted by Biome) and ignore generated migration metadata in Biome. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(indexer): database foundation — config, connection factory, health check, migrations
Upstream dev adopted a TypeORM-based indexer foundation (RPC client, IndexedEvent entity + repository, poller, handler registry, plus the streams and distributions handler stacks). PR #46 had introduced a parallel Drizzle-based DB foundation for the same indexer/common package. Resolution: adopt upstream's TypeORM stack and drop the now-orphaned Drizzle layer, while preserving PR #46's richer environment-config loader. - config: keep upstream's RPC config (config/loadConfig/Config) and re-export PR #46's validated env loader as loadIndexerConfig (renamed at the re-export boundary to avoid colliding with the RPC loadConfig). - index/exports: union the two barrels; drop the Drizzle db exports. - package.json: keep typeorm/pg/@stellar/stellar-sdk/zod; drop drizzle-orm/postgres/drizzle-kit deps and the db:generate/db:migrate scripts (root and common). - remove Drizzle files: drizzle.config.ts, migrations/, db/client.*, db/health.*, db/index.ts, db/schema.ts. - README: document the TypeORM persistence layer instead of Drizzle. - bun.lock: regenerated. Verified: indexer type-check and tests (44 passing) green.
Claude/pr 46 conflicts qk0zwo
📝 WalkthroughWalkthroughAdds a Zod-based environment config loader ( ChangesEnvironment Config Loader
Linting and Docs
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@indexer/common/src/config/env.ts`:
- Around line 25-31: The positive integer env validator in positiveIntFromString
still accepts 0, so update the refinement in env.ts to require values greater
than 0 while keeping the existing safe-integer check. Use the existing
positiveIntFromString symbol to locate the parser and make sure invalid
zero-valued inputs like INDEXER_PORT and POLL_INTERVAL_MS fail validation
instead of being parsed successfully.
- Around line 34-38: The INDEXER_DATABASE_URL validation in env config is too
permissive because it accepts any URL instead of a PostgreSQL DSN. Update the
schema in the env.ts config validator to enforce a Postgres connection string
(for example by requiring a postgres/postgresql scheme or using a stricter
custom refinement) so invalid values are rejected before startup. Keep the
existing required/non-empty checks, but make the final validation specific to
the database URL expected by the downstream connection code.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 066092c1-71b4-4f7e-9be7-ef591a9fb361
📒 Files selected for processing (7)
eslint.config.mjsindexer/README.mdindexer/biome.jsoncindexer/common/src/config/env.test.tsindexer/common/src/config/env.tsindexer/common/src/config/index.tsindexer/common/src/index.ts
| const positiveIntFromString = z | ||
| .string() | ||
| .trim() | ||
| .min(1, "must not be empty") | ||
| .regex(/^\d+$/, "must be a positive integer") | ||
| .transform((value) => Number.parseInt(value, 10)) | ||
| .refine((value) => Number.isSafeInteger(value), "must be a safe integer"); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Reject 0 in the numeric env validator.
Line 29 says “positive integer”, but /^\d+$/ plus Number.isSafeInteger still accepts 0. That lets INDEXER_PORT=0 bind an ephemeral port and POLL_INTERVAL_MS=0 turn the poller into a tight loop instead of failing fast.
Suggested fix
const positiveIntFromString = z
.string()
.trim()
.min(1, "must not be empty")
.regex(/^\d+$/, "must be a positive integer")
.transform((value) => Number.parseInt(value, 10))
- .refine((value) => Number.isSafeInteger(value), "must be a safe integer");
+ .refine((value) => Number.isSafeInteger(value), "must be a safe integer")
+ .refine((value) => value > 0, "must be a positive integer");📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const positiveIntFromString = z | |
| .string() | |
| .trim() | |
| .min(1, "must not be empty") | |
| .regex(/^\d+$/, "must be a positive integer") | |
| .transform((value) => Number.parseInt(value, 10)) | |
| .refine((value) => Number.isSafeInteger(value), "must be a safe integer"); | |
| const positiveIntFromString = z | |
| .string() | |
| .trim() | |
| .min(1, "must not be empty") | |
| .regex(/^\d+$/, "must be a positive integer") | |
| .transform((value) => Number.parseInt(value, 10)) | |
| .refine((value) => Number.isSafeInteger(value), "must be a safe integer") | |
| .refine((value) => value > 0, "must be a positive integer"); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indexer/common/src/config/env.ts` around lines 25 - 31, The positive integer
env validator in positiveIntFromString still accepts 0, so update the refinement
in env.ts to require values greater than 0 while keeping the existing
safe-integer check. Use the existing positiveIntFromString symbol to locate the
parser and make sure invalid zero-valued inputs like INDEXER_PORT and
POLL_INTERVAL_MS fail validation instead of being parsed successfully.
| INDEXER_DATABASE_URL: z | ||
| .string({ required_error: "is required" }) | ||
| .trim() | ||
| .min(1, "is required") | ||
| .url("must be a valid connection URL"), |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Require a PostgreSQL DSN, not just any URL.
Lines 34-38 accept any absolute URL, so https://example.com passes validation even though downstream code expects a Postgres connection string. That pushes a config error into the connection/startup path instead of catching it here.
Suggested fix
INDEXER_DATABASE_URL: z
.string({ required_error: "is required" })
.trim()
.min(1, "is required")
- .url("must be a valid connection URL"),
+ .url("must be a valid connection URL")
+ .refine((value) => {
+ const protocol = new URL(value).protocol;
+ return protocol === "postgres:" || protocol === "postgresql:";
+ }, "must be a PostgreSQL connection URL"),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| INDEXER_DATABASE_URL: z | |
| .string({ required_error: "is required" }) | |
| .trim() | |
| .min(1, "is required") | |
| .url("must be a valid connection URL"), | |
| INDEXER_DATABASE_URL: z | |
| .string({ required_error: "is required" }) | |
| .trim() | |
| .min(1, "is required") | |
| .url("must be a valid connection URL") | |
| .refine((value) => { | |
| const protocol = new URL(value).protocol; | |
| return protocol === "postgres:" || protocol === "postgresql:"; | |
| }, "must be a PostgreSQL connection URL"), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indexer/common/src/config/env.ts` around lines 34 - 38, The
INDEXER_DATABASE_URL validation in env config is too permissive because it
accepts any URL instead of a PostgreSQL DSN. Update the schema in the env.ts
config validator to enforce a Postgres connection string (for example by
requiring a postgres/postgresql scheme or using a stricter custom refinement) so
invalid values are rejected before startup. Keep the existing required/non-empty
checks, but make the final validation specific to the database URL expected by
the downstream connection code.
Summary
Adds the PostgreSQL-backed persistence foundation for the indexer in the
@fundable-indexer/commonpackage. This is tooling + infrastructure only — no domain tables are defined yet (that's deferred to later scoped issues).Implements issues #20, #21, #22, #23.
#20 — Database tooling
drizzle-orm@0.45.2,postgres@3.4.9,zod@3.25.67,drizzle-kit@0.31.10(dev);bun.lockupdatedindexer/common/drizzle.config.ts(postgresql dialect,src/db/schema.tsentrypoint,./migrationsout dir)migrations/directory with Drizzle journaldb:generate/db:migrate(incommon) andindexer:db:generate/indexer:db:migrate(root)indexer/README.md#21 — Config loader + validation
loadConfig()incommon/src/config— zod-validated, accepts an env record for testabilityINDEXER_DATABASE_URL; parses numericINDEXER_PORT/POLL_INTERVAL_MS/START_LEDGER; defaultsINDEXER_LOG_LEVELConfigValidationError; treats blank values as unset#22 — Connection factory
createDbClient()incommon/src/db— wraps postgres.js + Drizzle, reads from validated config, lazy (no socket until first query), injectablesqlfactory for tests,close()for graceful shutdown#23 — Health check
checkDbHealth()runsselect 1and returns a typedhealthy/unhealthyresult (with latency + error) without throwingIncidental
indexer/**(the workspace is linted by Biome) — prevents the two linters' differing global assumptions from conflictingmigrations/metadataVerification
All six gate commands pass:
bun run type-checkbun run testbun run lintbun run indexer:type-checkbun run indexer:testbun run indexer:lintbun run indexer:db:generatealso verified — reports "no schema changes" (expected, no tables yet).Closes #20
Closes #21
Closes #22
Closes #23
Summary by CodeRabbit
New Features
Documentation
Tests