Skip to content

feat(indexer/distributions): define distributions database schema#44

Open
Zeemnew wants to merge 1 commit into
Fundable-Protocol:devfrom
Zeemnew:feat/issue-36-distributions-db-schema
Open

feat(indexer/distributions): define distributions database schema#44
Zeemnew wants to merge 1 commit into
Fundable-Protocol:devfrom
Zeemnew:feat/issue-36-distributions-db-schema

Conversation

@Zeemnew

@Zeemnew Zeemnew commented Jun 26, 2026

Copy link
Copy Markdown

Overview

This PR defines the distributions domain database schema for the Soroban indexer. It introduces distribution_batch and claim_action tables so distribution handlers and future API resolvers can persist and query indexed data. Token amounts use NUMERIC(78, 0) to avoid floating-point precision loss, and batch status fields support pause/resume indexing behavior.

Related Issue

Closes #36

Changes

📦 DistributionBatch Table

  • [ADD] indexer/distributions/src/db/entity/DistributionBatch.ts

  • Added the core distribution_batch TypeORM entity.

  • Tracks contract ID, distributor, token, total/claimed amounts, recipient count, and on-chain metadata (uniqueRef, ledgerNumber, txHash).

  • Added pause/resume lifecycle fields: status, pausedAt, and resumedAt.

  • Uses NUMERIC(78, 0) columns for integer-safe token amounts.

  • [ADD] indexer/distributions/src/db/status.ts

  • Defined supported batch statuses: active, paused, completed, cancelled.

  • Added a type guard for validating status values.

🧾 ClaimAction Table

  • [ADD] indexer/distributions/src/db/entity/ClaimAction.ts
  • Added the claim_action TypeORM entity linked to distribution_batch via batchId.
  • Stores claimant address, claimed amount, transaction hash, ledger number, and event timestamp.
  • Uses NUMERIC(78, 0) for claim amounts.

🗄️ Migration & Indexes

  • [ADD] indexer/common/src/db/migrations/0002_create_distributions_schema.sql
  • Idempotent SQL migration with schema_migrations guard.
  • Creates distribution_batch and claim_action tables.
  • Adds indexes for common queries: contract, distributor, status, created_at, batch_id, claimant, and tx_hash.
  • Enforces status constraint for pause/resume behavior.

🔧 Package & Config

  • [MODIFY] indexer/distributions/package.json

  • Added typeorm dependency for entity definitions.

  • [MODIFY] indexer/distributions/src/index.ts

  • Exported distribution entities, status helpers, and distributionsEntities registry.

  • [MODIFY] indexer/tsconfig.base.json

  • Enabled TypeORM decorator support (experimentalDecorators, emitDecoratorMetadata).

🧪 Tests

  • [ADD] indexer/distributions/src/db/schema.test.ts
  • Added tests for batch status values and pause/resume lifecycle.
  • Added tests validating entity table mappings and numeric amount columns.
  • Added tests validating migration SQL structure, constraints, and indexes.

📚 Documentation

  • [MODIFY] indexer/README.md
  • Documented the new distributions database schema location.

Verification Results

bun run type-check ✅ passed
bun run test ✅ passed
bun run lint ✅ passed (existing repo warnings only)
bun run indexer:type-check ✅ passed
bun run indexer:test ✅ passed (12/12)
bun run indexer:lint ✅ passed
Acceptance Criteria Status
Define DistributionBatch table
Define ClaimAction table
Add status fields for pause/resume behavior
Add migrations and indexes for common queries
Represent token amounts without floating-point precision loss
Relevant tests are added or updated
Documentation is updated when needed
bun run type-check passes
bun run test passes
bun run lint passes
bun run indexer:type-check passes
bun run indexer:test passes
bun run indexer:lint passes

Summary by CodeRabbit

  • New Features

    • Added distributions database support with new batch and claim records, lifecycle statuses, and public exports for use by the app.
    • Added a schema migration and validation coverage to ensure the new distributions data structure is set up correctly.
  • Documentation

    • Updated the indexer status notes to clarify current setup and where the distributions database schema is defined.

Add DistributionBatch and ClaimAction tables with pause/resume status
fields, integer-safe token amounts, SQL migration, and schema tests.

Closes Fundable-Protocol#36
@drips-wave

drips-wave Bot commented Jun 26, 2026

Copy link
Copy Markdown

@Zeemnew Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds distributions database support: new TypeORM entities for distribution batches and claim actions, shared status utilities and exports, an idempotent SQL migration that creates the tables and indexes, test coverage for entity and migration metadata, and README references to the schema files.

Changes

Distributions schema

Layer / File(s) Summary
Status contract and setup
indexer/distributions/src/db/status.ts, indexer/distributions/src/index.ts, indexer/distributions/package.json, indexer/tsconfig.base.json, indexer/README.md
Adds the allowed distribution batch statuses, re-exports the new entities and helpers, enables TypeORM decorators and Vitest globals, and updates the README with schema and migration references.
DistributionBatch entity
indexer/distributions/src/db/entity/DistributionBatch.ts
Maps DistributionBatch to distribution_batch with indexed identifiers, numeric total and claimed amounts, pause/resume timestamps, lifecycle fields, and a one-to-many claims relation.
ClaimAction entity
indexer/distributions/src/db/entity/ClaimAction.ts
Maps ClaimAction to claim_action with a DistributionBatch foreign key, claimant and amount columns, ledger and transaction metadata, and query indexes.
Schema migration
indexer/common/src/db/migrations/0002_create_distributions_schema.sql
Creates the schema migration tracking table, guards repeat runs, creates distribution_batch and claim_action with constraints and indexes, and records the applied version.
Schema tests
indexer/distributions/src/db/schema.test.ts
Checks the status values, entity metadata, relation metadata, numeric precision, and migration SQL for the new schema.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #38 — Adds the distributions schema, entities, and status model that the event handlers would persist to and update.

Suggested reviewers

  • mubarak23

Poem

I dug two tables in the moonlit loam,
With status flags to guide them home.
My paws left indexes bright and neat,
And tests made every column sweet. 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: defining the distributions database schema for the indexer.
Description check ✅ Passed It covers the summary, linked issue, changes, and verification, though it doesn't use the exact checklist template.
Linked Issues check ✅ Passed The PR implements the scoped schema, status, migration, precision, tests, and docs required by #36.
Out of Scope Changes check ✅ Passed The non-schema updates are supporting changes for the indexer and remain within the linked issue scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

Warning

⚠️ This pull request shows signs of AI-generated slop (ai_padded_prose). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@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: 3

🧹 Nitpick comments (1)
indexer/tsconfig.base.json (1)

23-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider scoping vitest/globals to a test tsconfig.

Placing "types": ["vitest/globals"] in the shared base config injects Vitest test globals into production source compilation for every package, and explicitly setting types also disables automatic inclusion of other @types/*. Decorator options here are correct; only the test-globals placement is worth reconsidering (e.g., a tsconfig.test.json override).

🤖 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/tsconfig.base.json` around lines 23 - 25, Move the vitest/globals
type entry out of the shared tsconfig.base.json so production compilation is not
polluted with test-only globals and automatic `@types` inclusion is preserved.
Keep the decorator settings in the base config, and scope vitest/globals to a
dedicated test tsconfig override such as tsconfig.test.json used by the test
setup. Use the shared base config symbol tsconfig.base.json as the place to
remove the test-only types and the test tsconfig as the new location for them.
🤖 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/distributions/package.json`:
- Around line 18-19: Add the missing reflect-metadata setup for TypeORM
decorators: the `indexer/distributions` package currently depends on `typeorm`
but never installs or loads `reflect-metadata`, so decorator-based entity
metadata can fail at runtime. Update the relevant package manifest(s) in the
`indexer/` workspace to include `reflect-metadata`, and import it once at the
application entrypoint before any TypeORM entities or DataSource initialization
run. Use the package entry bootstrap and the TypeORM entity-loading path as the
places to verify the import happens first.

In `@indexer/distributions/src/db/entity/ClaimAction.ts`:
- Around line 22-30: The ClaimAction relation is mismatched with the database
column naming, causing TypeORM to look for the wrong foreign key column. Update
the scalar column and the relation in ClaimAction so the `batchId` property
explicitly maps to the snake_case database column used by the migration, and
change the `@JoinColumn` on the `DistributionBatch` relation to reference that
same database column name. Keep the mapping consistent with the naming strategy
so the relationship can resolve correctly.

In `@indexer/distributions/src/db/entity/DistributionBatch.ts`:
- Around line 27-96: The DistributionBatch entity uses camelCase property names,
but the database schema is snake_case, so TypeORM will generate mismatched
column names unless the datasource is configured correctly. Update the
AppDataSource setup in data-source.ts to use SnakeNamingStrategy so entities
like DistributionBatch, contractId, totalAmount, and createdAt map to the
existing snake_case columns in the migration.

---

Nitpick comments:
In `@indexer/tsconfig.base.json`:
- Around line 23-25: Move the vitest/globals type entry out of the shared
tsconfig.base.json so production compilation is not polluted with test-only
globals and automatic `@types` inclusion is preserved. Keep the decorator settings
in the base config, and scope vitest/globals to a dedicated test tsconfig
override such as tsconfig.test.json used by the test setup. Use the shared base
config symbol tsconfig.base.json as the place to remove the test-only types and
the test tsconfig as the new location for them.
🪄 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: 054e836c-ac0f-4c7e-89c8-563795dc5df4

📥 Commits

Reviewing files that changed from the base of the PR and between bcfa08d and a86b71e.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • indexer/README.md
  • indexer/common/src/db/migrations/0002_create_distributions_schema.sql
  • indexer/distributions/package.json
  • indexer/distributions/src/db/entity/ClaimAction.ts
  • indexer/distributions/src/db/entity/DistributionBatch.ts
  • indexer/distributions/src/db/schema.test.ts
  • indexer/distributions/src/db/status.ts
  • indexer/distributions/src/index.ts
  • indexer/tsconfig.base.json

Comment on lines +18 to +19
"@fundable-indexer/common": "workspace:*",
"typeorm": "^0.3.20"

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Is reflect-metadata declared anywhere?
fd -t f 'package.json' indexer --exec sh -c 'echo "== {} =="; cat {}' | rg -n 'reflect-metadata' || echo "reflect-metadata NOT declared in any indexer package.json"

# Is it imported anywhere?
rg -nP '''(import|require)\([^)]*reflect-metadata|import\s+["'\'']reflect-metadata''' indexer || echo "reflect-metadata NOT imported anywhere"

Repository: Fundable-Protocol/Backend

Length of output: 260


Missing reflect-metadata dependency for decorator-based TypeORM entities.

TypeORM 0.3.x entities using decorators require reflect-metadata to be installed and imported once at startup. Verification confirms it is missing from all package.json files in the indexer/ workspace and is not imported anywhere.

🛠️ Proposed dependency addition
     "`@fundable-indexer/common`": "workspace:*",
+    "reflect-metadata": "^0.2.2",
     "typeorm": "^0.3.20"

Add import "reflect-metadata"; at the application entrypoint before loading any entity metadata to prevent runtime failures in metadata resolution.

📝 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
"@fundable-indexer/common": "workspace:*",
"typeorm": "^0.3.20"
"`@fundable-indexer/common`": "workspace:*",
"reflect-metadata": "^0.2.2",
"typeorm": "^0.3.20"
🤖 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/distributions/package.json` around lines 18 - 19, Add the missing
reflect-metadata setup for TypeORM decorators: the `indexer/distributions`
package currently depends on `typeorm` but never installs or loads
`reflect-metadata`, so decorator-based entity metadata can fail at runtime.
Update the relevant package manifest(s) in the `indexer/` workspace to include
`reflect-metadata`, and import it once at the application entrypoint before any
TypeORM entities or DataSource initialization run. Use the package entry
bootstrap and the TypeORM entity-loading path as the places to verify the import
happens first.

Comment on lines +22 to +30
@Column({ type: "varchar", comment: "Distribution batch this claim belongs to" })
batchId!: string;

@ManyToOne(
() => DistributionBatch,
(batch) => batch.claims,
{ onDelete: "CASCADE" },
)
@JoinColumn({ name: "batchId" })

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.

🗄️ Data Integrity & Integration | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd --extension ts --glob '*migration*' --type f | grep -i claim

Repository: Fundable-Protocol/Backend

Length of output: 163


🏁 Script executed:

fd --extension ts --type f "src/db/migrations" | head -20

Repository: Fundable-Protocol/Backend

Length of output: 163


🏁 Script executed:

find . -type f -name "*migration*.ts" -path "*/src/*" | head -20

Repository: Fundable-Protocol/Backend

Length of output: 199


🏁 Script executed:

find . -type d -name "migrations" | head -5

Repository: Fundable-Protocol/Backend

Length of output: 215


🏁 Script executed:

grep -r "batch_id\|batchId" ./src/migrations --include="*.ts" | head -20

Repository: Fundable-Protocol/Backend

Length of output: 163


🏁 Script executed:

grep -r "batch_id\|batchId" ./indexer/common/src/db/migrations --include="*.ts" | head -20

Repository: Fundable-Protocol/Backend

Length of output: 163


🏁 Script executed:

ls -la ./src/migrations/

Repository: Fundable-Protocol/Backend

Length of output: 438


🏁 Script executed:

grep -r "batch_id\|batchId" ./src/migrations ./indexer/common/src/db/migrations --include="*.js" --include="*.ts" 2>/dev/null | head -20

Repository: Fundable-Protocol/Backend

Length of output: 163


🏁 Script executed:

cat ./indexer/distributions/src/db/entity/ClaimAction.ts

Repository: Fundable-Protocol/Backend

Length of output: 1542


🏁 Script executed:

find ./indexer -type f \( -name "*.js" -o -name "*.ts" \) | xargs grep -l "claim_action" | grep -i migration | head -10

Repository: Fundable-Protocol/Backend

Length of output: 163


🏁 Script executed:

find ./indexer -type f \( -name "*migration*" -o -name "*Migrate*" \) | head -10

Repository: Fundable-Protocol/Backend

Length of output: 163


🏁 Script executed:

grep -r "claim_action" --include="*.js" --include="*.ts" . | grep -i "createTable\|addColumn" | head -10

Repository: Fundable-Protocol/Backend

Length of output: 163


Align the @JoinColumn name with the database column to prevent foreign key lookup failures.

The current code explicitly sets @JoinColumn({ name: "batchId" }), which forces TypeORM to look for a column named batchId in the database. However, the database schema (created via the migration) uses the snake_case batch_id following the global naming strategy. This mismatch causes the ORM to fail in resolving the relationship.

Also update the scalar @Column definition to explicitly map to batch_id for clarity and consistency.

Suggested fix:

Detailed Change
  `@Index`("claim_action_batch_id_idx", ["batchId"])
  `@Index`("claim_action_claimant_idx", ["claimant"])
  `@Index`("claim_action_tx_hash_idx", ["txHash"])
  `@Index`("claim_action_batch_claimant_idx", ["batchId", "claimant"])
  export class ClaimAction {
    `@PrimaryGeneratedColumn`("uuid")
    id!: string;
  
-   `@Column`({ type: "varchar", comment: "Distribution batch this claim belongs to" })
+   `@Column`({ name: "batch_id", type: "varchar", comment: "Distribution batch this claim belongs to" })
    batchId!: string;
  
    `@ManyToOne`(
      () => DistributionBatch,
      (batch) => batch.claims,
      { onDelete: "CASCADE" },
    )
-   `@JoinColumn`({ name: "batchId" })
+   `@JoinColumn`({ name: "batch_id" })
    batch!: DistributionBatch;
📝 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
@Column({ type: "varchar", comment: "Distribution batch this claim belongs to" })
batchId!: string;
@ManyToOne(
() => DistributionBatch,
(batch) => batch.claims,
{ onDelete: "CASCADE" },
)
@JoinColumn({ name: "batchId" })
`@Column`({ name: "batch_id", type: "varchar", comment: "Distribution batch this claim belongs to" })
batchId!: string;
`@ManyToOne`(
() => DistributionBatch,
(batch) => batch.claims,
{ onDelete: "CASCADE" },
)
`@JoinColumn`({ name: "batch_id" })
batch!: DistributionBatch;
🤖 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/distributions/src/db/entity/ClaimAction.ts` around lines 22 - 30, The
ClaimAction relation is mismatched with the database column naming, causing
TypeORM to look for the wrong foreign key column. Update the scalar column and
the relation in ClaimAction so the `batchId` property explicitly maps to the
snake_case database column used by the migration, and change the `@JoinColumn`
on the `DistributionBatch` relation to reference that same database column name.
Keep the mapping consistent with the naming strategy so the relationship can
resolve correctly.

Comment on lines +27 to +96
@Column({ type: "varchar", comment: "Soroban contract that emitted the batch" })
contractId!: string;

@Column({ type: "varchar", comment: "Address that created the distribution batch" })
distributor!: string;

@Column({ type: "varchar", comment: "Token asset contract address" })
token!: string;

@Column({
type: "numeric",
precision: 78,
scale: 0,
comment: "Total allocated amount in the token's smallest unit",
})
totalAmount!: string;

@Column({
type: "numeric",
precision: 78,
scale: 0,
default: "0",
comment: "Total amount claimed so far in the token's smallest unit",
})
claimedAmount!: string;

@Column({ type: "integer", default: 0, comment: "Number of intended recipients" })
recipientCount!: number;

@Column({
type: "varchar",
default: "active",
comment: "Batch lifecycle status for pause/resume behavior",
})
status!: DistributionBatchStatus;

@Column({
type: "bigint",
nullable: true,
comment: "Unix timestamp when the batch was paused",
})
pausedAt!: string | null;

@Column({
type: "bigint",
nullable: true,
comment: "Unix timestamp when the batch was last resumed",
})
resumedAt!: string | null;

@Column({ type: "varchar", comment: "Contract-unique reference for the batch" })
uniqueRef!: string;

@Column({ type: "bigint", comment: "Ledger where the batch was created" })
ledgerNumber!: string;

@Column({ type: "varchar", comment: "Transaction hash that created the batch" })
txHash!: string;

@OneToMany(
() => ClaimAction,
(claim) => claim.batch,
)
claims!: ClaimAction[];

@CreateDateColumn()
createdAt!: Date;

@UpdateDateColumn()
updatedAt!: Date;

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.

🗄️ Data Integrity & Integration | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect datasource / TypeORM config for an explicit snake_case naming strategy.
fd -i 'data-source|datasource|ormconfig' .
rg -n -C2 'namingStrategy|SnakeNamingStrategy|snake' .

Repository: Fundable-Protocol/Backend

Length of output: 203


🏁 Script executed:

cat -n src/config/persistence/data-source.ts

Repository: Fundable-Protocol/Backend

Length of output: 3452


Critical mismatch between TypeORM config and database schema: Missing SnakeNamingStrategy

The DistributionBatch entity defines properties in camelCase (e.g., contractId, totalAmount, created_at), while the migration 0002_create_distributions_schema.sql creates columns in snake_case (e.g., contract_id, total_amount, created_at).

The datasource configuration in src/config/persistence/data-source.ts (lines 39-64) does not define a namingStrategy. Without explicitly setting namingStrategy: new SnakeNamingStrategy(), TypeORM defaults to camelCase column names. This will cause runtime errors (column "contractid" does not exist) whenever the application attempts to query or persist DistributionBatch records.

Change required in src/config/persistence/data-source.ts:

import { SnakeNamingStrategy } from 'typeorm';
// ... inside AppDataSource options:
namingStrategy: new SnakeNamingStrategy(),
🤖 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/distributions/src/db/entity/DistributionBatch.ts` around lines 27 -
96, The DistributionBatch entity uses camelCase property names, but the database
schema is snake_case, so TypeORM will generate mismatched column names unless
the datasource is configured correctly. Update the AppDataSource setup in
data-source.ts to use SnakeNamingStrategy so entities like DistributionBatch,
contractId, totalAmount, and createdAt map to the existing snake_case columns in
the migration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Define Distributions database schema

1 participant