Skip to content

Feat/indexer db foundation - #51

Merged
pragmaticAweds merged 4 commits into
Fundable-Protocol:devfrom
promisszn:feat/indexer-db-foundation
Jun 29, 2026
Merged

pragmaticAweds merged 4 commits into
Fundable-Protocol:devfrom
promisszn:feat/indexer-db-foundation

Conversation

@promisszn

@promisszn promisszn commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the PostgreSQL-backed persistence foundation for the indexer in the @fundable-indexer/common package. 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

  • Adds exact-pinned deps: drizzle-orm@0.45.2, postgres@3.4.9, zod@3.25.67, drizzle-kit@0.31.10 (dev); bun.lock updated
  • indexer/common/drizzle.config.ts (postgresql dialect, src/db/schema.ts entrypoint, ./migrations out dir)
  • Initial migrations/ directory with Drizzle journal
  • Scripts: db:generate / db:migrate (in common) and indexer:db:generate / indexer:db:migrate (root)
  • Migration workflow documented in indexer/README.md

#21 — Config loader + validation

  • loadConfig() in common/src/config — zod-validated, accepts an env record for testability
  • Requires INDEXER_DATABASE_URL; parses numeric INDEXER_PORT / POLL_INTERVAL_MS / START_LEDGER; defaults INDEXER_LOG_LEVEL
  • Aggregates every problem into a single clear ConfigValidationError; treats blank values as unset

#22 — Connection factory

  • createDbClient() in common/src/db — wraps postgres.js + Drizzle, reads from validated config, lazy (no socket until first query), injectable sql factory for tests, close() for graceful shutdown

#23 — Health check

  • checkDbHealth() runs select 1 and returns a typed healthy / unhealthy result (with latency + error) without throwing

Incidental

  • Root ESLint now ignores indexer/** (the workspace is linted by Biome) — prevents the two linters' differing global assumptions from conflicting
  • Biome ignores generated migrations/ metadata

Verification

All six gate commands pass:

Command Result
bun run type-check
bun run test ✅ 10 pass, coverage met
bun run lint ✅ 0 errors
bun run indexer:type-check
bun run indexer:test ✅ 12 pass (common)
bun run indexer:lint

bun run indexer:db:generate also verified — reports "no schema changes" (expected, no tables yet).

Closes #20
Closes #21
Closes #22
Closes #23

Summary by CodeRabbit

  • New Features

    • Added stricter runtime configuration validation for the indexer, with clearer error messages when required settings are missing or invalid.
    • Improved support for optional defaults and empty-value handling in configuration inputs.
  • Documentation

    • Updated the indexer README to reflect the current setup, including validation, persistence, and background processing components.
  • Tests

    • Added coverage for valid configuration, default values, and multiple validation failures.

promisszn and others added 4 commits June 26, 2026 23:00
…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.
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Zod-based environment config loader (loadConfig/loadIndexerConfig) and ConfigValidationError class to indexer/common, with Vitest tests covering valid and invalid inputs. Excludes indexer/** from ESLint and **/migrations from Biome. Updates the indexer README to document the implemented foundation components.

Changes

Environment Config Loader

Layer / File(s) Summary
IndexerConfig interface, Zod schema, and ConfigValidationError
indexer/common/src/config/env.ts
Defines the IndexerConfig interface, positive-integer Zod validators, configSchema covering all env vars, and ConfigValidationError error subclass with issues list.
loadConfig() and public exports
indexer/common/src/config/env.ts, indexer/common/src/config/index.ts, indexer/common/src/index.ts
Implements loadConfig() with env normalization, safeParse, and conditional startLedger; re-exports as loadIndexerConfig from the config index and top-level common index.
Tests
indexer/common/src/config/env.test.ts
Vitest suite covering valid config, logLevel default, empty START_LEDGER, missing/invalid required fields, and multi-error aggregation into a single ConfigValidationError.

Linting and Docs

Layer / File(s) Summary
ESLint and Biome ignore updates
eslint.config.mjs, indexer/biome.jsonc
Adds indexer/** to ESLint ignores and **/migrations to Biome ignores.
README
indexer/README.md
Documents loadIndexerConfig, TypeORM/PostgreSQL persistence layout, and updates the status section to list implemented components.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 Hoppy news from the warren today,
The config is validated, hip-hooray!
Zod checks each env with a careful sniff,
Bad URLs and ports get the cold-shoulder whiff.
Migrations hide from Biome's keen eye,
And the README now waves its proud flag high! 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description has summary and verification, but it omits required Area, Scope, Indexer Safety, and Notes sections. Add the missing template sections, especially Area, Scope, Indexer Safety, and Notes, and align verification with the repo checklist.
Linked Issues check ⚠️ Warning Only #21 is clearly represented; the tooling, connection factory, and health check requested in #20, #22, and #23 are not shown in the changes. Add the missing Drizzle tooling, DB connection factory, and health check implementation with tests, or narrow the linked issue scope.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the indexer database foundation work.
Out of Scope Changes check ✅ Passed The README and lint config changes support the indexer foundation, and no clearly unrelated code changes are evident.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between e507f8d and a007668.

📒 Files selected for processing (7)
  • eslint.config.mjs
  • indexer/README.md
  • indexer/biome.jsonc
  • indexer/common/src/config/env.test.ts
  • indexer/common/src/config/env.ts
  • indexer/common/src/config/index.ts
  • indexer/common/src/index.ts

Comment on lines +25 to +31
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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

Comment on lines +34 to +38
INDEXER_DATABASE_URL: z
.string({ required_error: "is required" })
.trim()
.min(1, "is required")
.url("must be a valid connection URL"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

@pragmaticAweds
pragmaticAweds merged commit e2a06a4 into Fundable-Protocol:dev Jun 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants