Generate Enums and Entities modules inline in indexer code - #948
Conversation
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
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughMoves 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
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
…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
There was a problem hiding this comment.
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, unlikegenerate_entities_code.
generate_entities_codeinsertswriteln!(code).unwrap()(line 286) before each entity module for readability, butgenerate_enums_codedoesn'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.getExncalls produce cryptic errors on misconfigured input.Lines 237, 241, and 245 use
Option.getExnwithout context. Ifprop["enum"], the enum config lookup, orprop["entity"]is missing, the error will be an unhelpful "Not found" exception. Consider usingOption.getWithDefaultwithJs.Exn.raiseErrorfor 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:getFieldTypeAndSchemais called twice per property — once for table fields, once for schema construction.Lines 279-280 call
getFieldTypeAndSchemato 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 + })
… 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
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
…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
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
indent()helper function to properly indent generated code blocksgenerate_enums_code()function to generate the Enums module with all enum definitions and configurationgenerate_entities_code()function to generate the Entities module with entity type definitions, schemas, and table configurationsEnums.res.hbsandEntities.res.hbstemplate files as they are no longer neededIndexer.EntitiesandIndexer.Enumsinstead of standaloneEntitiesandEnumsmodulesImplementation Details
@genTypeannotations and a consolidatedallEnumsarrayhttps://claude.ai/code/session_01XURj7rvuXp9X1gj1vY5UbC
Summary by CodeRabbit
New Features
Refactor