Skip to content

Generate Enums and Entities modules inline in indexer code - #948

Merged
DZakh merged 26 commits into
mainfrom
claude/move-entities-enums-rust-f51l7
Feb 18, 2026
Merged

Generate Enums and Entities modules inline in indexer code#948
DZakh merged 26 commits into
mainfrom
claude/move-entities-enums-rust-f51l7

Conversation

@DZakh

@DZakh DZakh commented Feb 12, 2026

Copy link
Copy Markdown
Member

Summary

Gradually getting rid of handlebars.


This PR refactors the code generation to inline the Enums and Entities modules directly into the generated indexer code, rather than generating them as separate template files. This simplifies the build process and improves code organization by keeping related entity and enum definitions together.

Key Changes

  • Added indent() helper function to properly indent generated code blocks
  • Implemented generate_enums_code() function to generate the Enums module with all enum definitions and configuration
  • Implemented generate_entities_code() function to generate the Entities module with entity type definitions, schemas, and table configurations
  • Modified the indexer code generation to wrap Enums and Entities as nested modules within the main indexer output
  • Removed Enums.res.hbs and Entities.res.hbs template files as they are no longer needed
  • Updated all test files and handler code to reference entities and enums via Indexer.Entities and Indexer.Enums instead of standalone Entities and Enums modules

Implementation Details

  • The generated Enums module contains individual enum type definitions with @genType annotations and a consolidated allEnums array
  • The generated Entities module includes the Entity module type signature, helper functions, and individual entity module definitions with their schemas and table configurations
  • Both modules are indented by 2 spaces when inserted into the main indexer code to maintain proper ReScript formatting
  • The order of module generation ensures Enums are defined before Entities since entities may reference enum types
  • All references throughout the codebase have been updated to use the new module path structure

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC

Summary by CodeRabbit

  • New Features

    • Public config now accepts and serializes enums and entities (including detailed entity JSON) and exposes userEntities, allEntities, allEnums, plus a config-without-registrations view.
    • Project templates now surface generated indexer bindings and inserted entities/enums code into outputs.
  • Refactor

    • Public access paths reorganized to use Generated/Indexer/Mock-driven entity descriptors; templates, tests and helpers updated to the new config-driven access patterns.

Generate Entities and Enums ReScript code in Rust and emit them as
modules inside Indexer.res instead of separate Handlebars-templated
files. This eliminates Entities.res.hbs and Enums.res.hbs, moving
their logic into generate_entities_code() and generate_enums_code()
functions in codegen_templates.rs.

All references updated from Entities.X to Indexer.Entities.X and
Enums.X to Indexer.Enums.X across templates and scenario tests.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
Add indent() helper to properly indent generated code inside
module Enums and module Entities wrappers. Ensure closing braces
are on their own lines.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
@coderabbitai

coderabbitai Bot commented Feb 12, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Moves per-entity and per-enum generation from Handlebars into the Rust codegen, emits JSON representations for enums/entities, exposes generated Enums/Entities under Indexer/Generated, and updates runtime templates, config parsing, InMemoryStore, and tests to consume Internal.entityConfig / Generated.configWithoutRegistrations.

Changes

Cohort / File(s) Summary
Code generator core
codegenerator/cli/src/hbs_templating/codegen_templates.rs
Adds generators for enums/entities, new Internal*Json structs, ProjectTemplate fields for entities_enums_code and generated_indexer_bindings, and serializes enums_json/entities_json.
Removed Handlebars templates
codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs, codegenerator/cli/templates/dynamic/codegen/src/db/Enums.res.hbs
Deletes per-entity and per-enum Handlebars templates; generation is migrated into Rust codegen.
Generated templates & types
codegenerator/cli/templates/dynamic/codegen/src/Generated.res.hbs, codegenerator/cli/templates/dynamic/codegen/src/Types.res.hbs
Integrates generated entities/enums blocks into Generated/Types; updates Generated to expose configWithoutRegistrations/allEntities and inserts generated_indexer_bindings into output.
Test helpers & mocks
codegenerator/cli/templates/dynamic/codegen/src/TestHelpers_MockDb.res.hbs, scenarios/test_codegen/test/helpers/Mock.res, scenarios/test_codegen/test/__mocks__/MockEntities.res, .../TestIndexerWorker.res
Switches runtime/test code to use Indexer.Generated bindings, Generated.allEntities, and entityConfig-based APIs; adapts setEntity signatures and mock helpers to Internal.entityConfig/Internal.entity.
Runtime config & parsing
codegenerator/cli/npm/envio/src/Config.res, codegenerator/cli/npm/envio/src/db/InternalTable.res
Extends public config schema to include enums/entities, adds parse helpers, returns userEntities/allEntities/allEnums from fromPublic, and aliases DynamicContractRegistry to Config.DynamicContractRegistry.
Persistence & InMemoryStore wiring
codegenerator/cli/npm/envio/src/Persistence.res, codegenerator/cli/npm/envio/src/InMemoryStore.res
Switches DynamicContractRegistry references from .config to .entityConfig and concatenates entityConfig into lists used by persistence.
Tests & scenarios
scenarios/... (many files, e.g. test/*, rollback/*, lib_tests/*)
Wide symbol updates: replace Entities.*/Enums.* with Indexer.Entities.*/Indexer.Enums.*, use Mock.entityConfig("Name") or Generated.* for entity sources, update casts/table/schema/id usages to entityConfig-based forms. Areas to review: many test expectations and mocks.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant PublicConfig as "Public JSON\n(config.json)"
  participant Envio as "Envio.fromPublic\n(codegenerator/npm/envio/Config.res)"
  participant RustCodegen as "Rust Codegen\n(codegen_templates.rs)"
  participant Generated as "Generated runtime\n(Generated.res)"
  participant Runtime as "Runtime & Tests\n(InMemoryStore, Persistence)"

  PublicConfig->>Envio: parse enums & entities JSON
  Envio->>Envio: build userEntities/allEnums/allEntities
  Envio-->>RustCodegen: provide parsed entities/enums
  RustCodegen->>RustCodegen: generate Enums/Entities code + Internal JSON
  RustCodegen-->>Generated: emit entities_enums_code & generated_indexer_bindings
  Generated->>Runtime: expose configWithoutRegistrations, allEntities
  Runtime->>Runtime: use Internal.entityConfig for stores, queries, casts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • JonoPrest
  • moose-code

Poem

🐇 I hopped through templates, bytes, and threads of code,
Moved enums and entities onto a brighter road.
Generated bindings now lead the test parade,
Mock stores learned new names, JSON proudly displayed.
~A cheerful CodeRabbit 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Generate Enums and Entities modules inline in indexer code' is highly specific and clearly describes the primary refactoring objective: moving Enums and Entities module generation from separate Handlebars templates into inline code generation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/move-entities-enums-rust-f51l7

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 and usage tips.

@DZakh
DZakh requested a review from JonoPrest February 12, 2026 09:52
Db.cmj -> Indexer.cmj -> Generated.cmj -> Db.cmj cycle caused by
Db.res referencing Indexer.Entities after the module move. Inline
the schema construction directly in Generated.res.hbs.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
@DZakh
DZakh removed the request for review from JonoPrest February 12, 2026 11:18
…json

Break the Generated.cmj -> Indexer.cmj -> Generated.cmj dependency cycle by:
- Keeping only type definitions in Indexer.res Entities/Enums modules
- Adding entities and enums data to internal.config.json via Rust codegen
- Parsing entities/enums at runtime in Config.res to create entityConfig/enumConfig
- Updating Generated.res.hbs, TestHelpers_MockDb, TestIndexerWorker to use
  config-based entities/enums instead of Indexer module runtime values
- Updating all scenario test files to use Mock.entityConfig() helper

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC

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

🤖 Fix all issues with AI agents
In `@codegenerator/cli/npm/envio/src/Config.res`:
- Around line 251-257: fieldSchema currently uses an if/else-if so
S.array(baseSchema) is chosen when isArray is true and the isNullable branch is
skipped; instead build the schema by composing wrappers: start with baseSchema,
if isArray wrap it with S.array(...)->S.toUnknown, and then if isNullable wrap
that result with S.null(...)->S.toUnknown (or apply S.toUnknown at the end), so
that when both isArray and isNullable are true you produce
S.null(S.array(baseSchema)) rather than just S.array(baseSchema); update the
logic around the fieldSchema binding (references: fieldSchema, isArray,
isNullable, baseSchema, S.array, S.null, S.toUnknown) to perform sequential
wrapping rather than a mutually exclusive if/else-if.
🧹 Nitpick comments (3)
codegenerator/cli/src/hbs_templating/codegen_templates.rs (1)

264-278: Missing blank-line separator between enum modules, unlike generate_entities_code.

generate_entities_code inserts writeln!(code).unwrap() (line 286) before each entity module for readability, but generate_enums_code doesn't. This means consecutive enum modules will be rendered without a blank line between them, which is a minor formatting inconsistency.

Add a blank line between enum modules for consistency
 fn generate_enums_code(gql_enums: &[GraphQlEnumTypeTemplate]) -> String {
     let mut code = String::new();
 
-    for gql_enum in gql_enums {
+    for (i, gql_enum) in gql_enums.iter().enumerate() {
+        if i > 0 {
+            writeln!(code).unwrap();
+        }
         writeln!(code, "module {} = {{", gql_enum.name.capitalized).unwrap();
         writeln!(code, "  `@genType`").unwrap();
         write!(code, "  type t =\n").unwrap();
codegenerator/cli/npm/envio/src/Config.res (2)

211-260: Option.getExn calls produce cryptic errors on misconfigured input.

Lines 237, 241, and 245 use Option.getExn without context. If prop["enum"], the enum config lookup, or prop["entity"] is missing, the error will be an unhelpful "Not found" exception. Consider using Option.getWithDefault with Js.Exn.raiseError for a descriptive message, similar to how you handle unknowns on line 248.

Proposed improvement for line 237-242
   | "enum" => {
-      let enumName = prop["enum"]->Option.getExn
-      let enumConfig =
-        enumConfigsByName
-        ->Js.Dict.get(enumName)
-        ->Option.getExn
+      let enumName = switch prop["enum"] {
+      | Some(n) => n
+      | None => Js.Exn.raiseError("Field of type 'enum' is missing 'enum' property: " ++ prop["name"])
+      }
+      let enumConfig = switch enumConfigsByName->Js.Dict.get(enumName) {
+      | Some(c) => c
+      | None => Js.Exn.raiseError("Enum config not found for: " ++ enumName)
+      }
       (Table.Enum({config: enumConfig}), enumConfig.schema->S.toUnknown)
     }
   | "entity" => {
-      let entityName = prop["entity"]->Option.getExn
+      let entityName = switch prop["entity"] {
+      | Some(n) => n
+      | None => Js.Exn.raiseError("Field of type 'entity' is missing 'entity' property: " ++ prop["name"])
+      }
       (Table.Entity({name: entityName}), S.string->S.toUnknown)
     }

270-338: getFieldTypeAndSchema is called twice per property — once for table fields, once for schema construction.

Lines 279-280 call getFieldTypeAndSchema to build table fields, and lines 323-324 call it again for the same properties to build the runtime schema. Since the function performs pattern matching and option lookups, this is redundant work. Consider computing once and reusing both the field metadata and the schema.

Sketch: compute field info once
     let fields: array<Table.fieldOrDerived> =
-      entityJson["properties"]->Array.map(prop => {
-        let (fieldType, fieldSchema, isPrimaryKey, isNullable, isArray, isIndex) =
+      entityJson["properties"]->Array.map(prop => {
+        let (fieldType, _fieldSchema, isPrimaryKey, isNullable, isArray, isIndex) =
           getFieldTypeAndSchema(prop, ~enumConfigsByName)
         Table.mkField(
           ...
         )
       })
     ...
-    // Build schema dynamically from properties
-    let schema = S.schema(s => {
-      let dict = Js.Dict.empty()
-      entityJson["properties"]->Array.forEach(prop => {
-        let (_, fieldSchema, _, _, _, _) = getFieldTypeAndSchema(prop, ~enumConfigsByName)
-        dict->Js.Dict.set(prop["name"], s.matches(fieldSchema))
-      })
-      dict
-    })
+    // Pre-compute field schemas
+    let fieldSchemas = entityJson["properties"]->Array.map(prop => {
+      let (_, fieldSchema, _, _, _, _) = getFieldTypeAndSchema(prop, ~enumConfigsByName)
+      (prop["name"], fieldSchema)
+    })
+    let schema = S.schema(s => {
+      let dict = Js.Dict.empty()
+      fieldSchemas->Array.forEach(((name, fieldSchema)) => {
+        dict->Js.Dict.set(name, s.matches(fieldSchema))
+      })
+      dict
+    })

Comment thread codegenerator/cli/npm/envio/src/Config.res Outdated
… Generated

Remove allEntities from Config.t since it referenced InternalTable.DynamicContractRegistry,
creating a Config.cmj -> InternalTable.cmj -> Config.cmj cycle. allEntities is now computed
in Generated.res.hbs and referenced directly as Generated.allEntities.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
Breaks Config -> InternalTable cycle by defining DynamicContractRegistry
in Config.res with entityConfig field. InternalTable re-exports it as
module DynamicContractRegistry = Config.DynamicContractRegistry.
Restores allEntities in Config.t computed from userEntities + DynamicContractRegistry.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
Js.Date.schema doesn't exist. Use Utils.Schema.dbDate which matches
the Rust codegen output for Timestamp fields.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
Move Enums and Entities modules from Indexer.res to Types.res (Indexer
re-exports them). Move indexer value and createTestIndexer bindings from
Indexer.res to Generated.res since they reference Generated.config.

Dependency order is now: Types -> Indexer -> Generated (no cycles).

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
Instead of moving Enums/Entities to Types.res, keep them in Indexer.res
and move the contract modules (MakeRegister), onBlock, indexer value,
and createTestIndexer to Generated.res. This breaks the cycle without
changing entity type locations.

Dependency order: Indexer -> Types -> Generated (no cycles).

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
Primary key is always the id field. No need to pass isPrimaryKey
via internal.config.json. Config.res now uses prop["name"] === "id".

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
Move entityHandlerContext, handlerContext types, and onBlock from
Types.res to Indexer.res (Rust codegen). This lets Indexer reference
entity types locally. Types.MakeRegister uses Indexer.handlerContext.

Contract modules (MakeRegister) stay in Generated since they reference
Types. indexer value and createTestIndexer stay in Generated since they
reference Generated.configWithoutRegistrations.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
- Flatten all Types.res.hbs content directly into Indexer.res.hbs
- Wrap Generated.res.hbs content into module Generated inside Indexer.res
- Move indexer and createTestIndexer bindings to top level of Indexer.res
- Delete Types.res.hbs and Generated.res.hbs
- Update all internal refs: Indexer.handlerContext -> handlerContext,
  Types.MakeRegister(Types.X.Y) -> MakeRegister(X.Y)
- Update all external refs: Types. -> Indexer., Generated. -> Indexer.Generated.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
…est mocks

- Generate `type rec name<'entity>` GADT in Entities module with @as annotations
  for type-safe entity name references (replaces string-based lookups)
- Move `~makePersistence` callback into `initTestWorker` to simplify TestIndexerWorker
- Use `include` pattern for contract event modules: raw `{Event}Event` module +
  `{Event}` module that includes both raw and MakeRegister output
- Update Mock.res: entityConfig/query/queryHistory accept GADT, add queryRaw for
  internal tables (DynamicContractRegistry)
- Update all test call sites: Types. -> Indexer., Generated. -> Indexer.Generated.,
  Mock.entityConfig("X") -> Mock.entityConfig(X), query(entityConfig) -> query(Name)

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
Resolve conflicts with TestIndexer chain info changes (#947):
- Keep testIndexer wrapper type in indexer_code (type definitions)
- Move createTestIndexer binding with Utils.magic cast to generated_top_level_bindings
- Regenerate insta snapshots

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
The previous if/else-if made isArray and isNullable mutually exclusive,
so nullable arrays produced S.array(baseSchema) instead of
S.null(S.array(baseSchema)). Now wraps baseSchema with S.array first
if isArray, then with S.null if isNullable.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
@DZakh
DZakh requested a review from JonoPrest February 16, 2026 09:54
claude and others added 10 commits February 16, 2026 10:29
The envio package defines its own NodeJs module which conflicts with
rescript-nodejs's NodeJs, causing "inconsistent assumptions over
interface NodeJs" compilation errors.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
…l.evmEventConfig

The generated Indexer.gen.ts imports these types from the envio
package, but genType deletes .gen.ts files for modules without
@Gentype annotations during build. Adding @genType/@genType.opaque
ensures the .gen.ts files are properly generated and persist.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
In parseEntitiesFromJson, the schema was using user-facing field names
(e.g., "b") but Table.toSqlParams expects db column names (e.g., "b_id").

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
…ucturing

Entities module was moved inside Indexer.res, so `open Entities` no longer
resolves. Update to `open Indexer.Entities`.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
The fuel test was failing because fuelEventConfig was not exported from
Internal.gen.ts. Added @genType.opaque annotation matching evmEventConfig.
Also removed rescript.lock files from git tracking and added to .gitignore.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
- Types.res.hbs: accept deletion (content moved to Indexer.res.hbs),
  port Utils.Array.firstUnsafe change from main
- Mock.res: keep Indexer.Generated.configWithoutRegistrations

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
@DZakh
DZakh enabled auto-merge (squash) February 18, 2026 09:16
…or records

The query() method takes a first-class module already in scope as SimpleEntity,
not module(Entities.SimpleEntity). Also fix record type paths from main's merge.

https://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
@DZakh
DZakh merged commit 3d21998 into main Feb 18, 2026
2 checks passed
@DZakh
DZakh deleted the claude/move-entities-enums-rust-f51l7 branch February 18, 2026 10:31
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.

3 participants