Skip to content

Commit 3c02011

Browse files
committed
Address review: move WhereOperator/GetWhereFilter to index.d.ts, add JSDoc, runtime index check
- Move WhereOperator<T> and GetWhereFilter<E> from Rust-generated envio.d.ts to static index.d.ts in envio package as TS generics - Add JSDoc comments for all WhereOperator fields (_eq, _gt, _lt) - Generate getWhereFilter for ALL non-derived entity fields (not just indexed) - Add runtime isIndex check in UserContext.res with user-friendly errors: - Derived fields: tells user to use source entity's indexed field - Non-indexed fields: tells user to add @index directive with example - Update Types.ts.hbs to import GetWhereFilter from "envio" package https://claude.ai/code/session_01FmZW8U8ymXd46zRzJZDhNh
1 parent 59bf8de commit 3c02011

File tree

6 files changed

+42
-77
lines changed

6 files changed

+42
-77
lines changed

codegenerator/cli/npm/envio/index.d.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,30 @@ export type { EffectCaller, Address } from "./src/Types.ts";
1818
/** Utility type to expand/flatten complex types for better IDE display. */
1919
export type Prettify<T> = { [K in keyof T]: T[K] } & {};
2020

21+
/**
22+
* Operator for filtering entity fields in getWhere queries.
23+
* Only fields with `@index` in the schema can be queried at runtime.
24+
*/
25+
export type WhereOperator<T> = {
26+
/** Matches entities where the field equals the given value. */
27+
readonly _eq?: T;
28+
/** Matches entities where the field is greater than the given value. */
29+
readonly _gt?: T;
30+
/** Matches entities where the field is less than the given value. */
31+
readonly _lt?: T;
32+
};
33+
34+
/**
35+
* Constructs a getWhere filter type from an entity type.
36+
* Each field can be filtered using {@link WhereOperator} (`_eq`, `_gt`, `_lt`).
37+
*
38+
* Note: only fields with `@index` in the schema can be queried at runtime.
39+
* Attempting to filter on a non-indexed field will throw a descriptive error.
40+
*/
41+
export type GetWhereFilter<E> = {
42+
[K in keyof E]?: WhereOperator<E[K]>;
43+
};
44+
2145
import type {
2246
effect as Effect,
2347
effectArgs as EffectArgs,

codegenerator/cli/npm/envio/src/UserContext.res

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,18 +122,22 @@ let getWhereHandler = (params: entityContextParams, filter: Js.Dict.t<Js.Dict.t<
122122
Js.Exn.raiseError(
123123
`Invalid field "${dbFieldName}" in context.${entityConfig.name}.getWhere(). The field doesn't exist. ${codegenHelpMessage}`,
124124
)
125-
| Some(field) =>
126-
let fieldValueSchema = switch field {
127-
| Field({fieldSchema}) => fieldSchema
128-
| DerivedFrom(_) => S.string->S.toUnknown
129-
}
125+
| Some(DerivedFrom(_)) =>
126+
Js.Exn.raiseError(
127+
`The field "${dbFieldName}" on entity "${entityConfig.name}" is a derived field and cannot be used in getWhere(). Use the source entity's indexed field instead.`,
128+
)
129+
| Some(Field({isIndex: false})) =>
130+
Js.Exn.raiseError(
131+
`The field "${dbFieldName}" on entity "${entityConfig.name}" does not have an index. To use it in getWhere(), add the @index directive in your schema.graphql:\n\n ${dbFieldName}: ... @index\n\nThen run 'pnpm envio codegen' to regenerate.`,
132+
)
133+
| Some(Field({fieldSchema, isIndex: true})) =>
130134
LoadLayer.loadByField(
131135
~loadManager=params.loadManager,
132136
~persistence=params.persistence,
133137
~operator,
134138
~entityConfig,
135139
~fieldName=dbFieldName,
136-
~fieldValueSchema,
140+
~fieldValueSchema=fieldSchema,
137141
~inMemoryStore=params.inMemoryStore,
138142
~shouldGroup=params.isPreload,
139143
~item=params.item,

codegenerator/cli/src/hbs_templating/codegen_templates.rs

Lines changed: 3 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -306,10 +306,11 @@ impl EntityRecordTypeTemplate {
306306

307307
let composite_indices = entity.get_composite_indices();
308308

309-
// Generate getWhereFilter type code for ReScript
309+
// Generate getWhereFilter type code for ReScript (all non-derived fields)
310+
// Non-indexed fields will throw a user-friendly error at runtime
310311
let get_where_filter_fields: Vec<String> = params
311312
.iter()
312-
.filter(|p| p.is_queryable_field)
313+
.filter(|p| !p.is_derived_field)
313314
.map(|p| {
314315
let (field_name, as_name) = if p.is_entity_field {
315316
(
@@ -1810,53 +1811,6 @@ let createTestIndexer: unit => TestIndexer.t<testIndexerProcessConfig> = TestInd
18101811
)
18111812
});
18121813

1813-
// Generate WhereOperator generic type
1814-
parts.push(
1815-
"export type WhereOperator<T> = {\n readonly _eq?: T;\n readonly _gt?: T;\n readonly _lt?: T;\n};"
1816-
.to_string(),
1817-
);
1818-
1819-
// Generate GetWhereFilter type with per-entity filter definitions
1820-
let get_where_filter_entries: Vec<String> = entities
1821-
.iter()
1822-
.map(|entity| {
1823-
let filter_fields: Vec<String> = entity
1824-
.params
1825-
.iter()
1826-
.filter(|param| param.is_queryable_field)
1827-
.map(|param| {
1828-
let ts_type = param.field_type.to_ts_type_string();
1829-
let (field_name, field_type) = if param.is_entity_field {
1830-
(
1831-
format!("{}_id", param.field_name.original),
1832-
"string".to_string(),
1833-
)
1834-
} else {
1835-
(param.field_name.original.clone(), ts_type)
1836-
};
1837-
format!(" {}?: WhereOperator<{}>;", field_name, field_type)
1838-
})
1839-
.collect();
1840-
if filter_fields.is_empty() {
1841-
format!(" \"{}\": {{}};", entity.name.capitalized)
1842-
} else {
1843-
format!(
1844-
" \"{}\": {{\n{}\n }};",
1845-
entity.name.capitalized,
1846-
filter_fields.join("\n")
1847-
)
1848-
}
1849-
})
1850-
.collect();
1851-
parts.push(if get_where_filter_entries.is_empty() {
1852-
"export type GetWhereFilter = {};".to_string()
1853-
} else {
1854-
format!(
1855-
"export type GetWhereFilter = {{\n{}\n}};",
1856-
get_where_filter_entries.join("\n")
1857-
)
1858-
});
1859-
18601814
parts.join("\n")
18611815
};
18621816

codegenerator/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__envio_dts_code_generated_for_evm.snap

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
source: cli/src/hbs_templating/codegen_templates.rs
3-
assertion_line: 2471
3+
assertion_line: 2425
44
expression: project_template.envio_dts_code
55
---
66
export type EvmChains = {
@@ -31,12 +31,3 @@ export type Entities = {
3131
readonly "name": string;
3232
};
3333
};
34-
export type WhereOperator<T> = {
35-
readonly _eq?: T;
36-
readonly _gt?: T;
37-
readonly _lt?: T;
38-
};
39-
export type GetWhereFilter = {
40-
"EmptyEntity": {};
41-
"RelatedEntity": {};
42-
};

codegenerator/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__envio_dts_code_generated_for_fuel.snap

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
source: cli/src/hbs_templating/codegen_templates.rs
3-
assertion_line: 2477
3+
assertion_line: 2431
44
expression: project_template.envio_dts_code
55
---
66
export type EvmChains = {};
@@ -19,11 +19,3 @@ export type Entities = {
1919
readonly "emptyField": string;
2020
};
2121
};
22-
export type WhereOperator<T> = {
23-
readonly _eq?: T;
24-
readonly _gt?: T;
25-
readonly _lt?: T;
26-
};
27-
export type GetWhereFilter = {
28-
"EmptyEntity": {};
29-
};

codegenerator/cli/templates/dynamic/codegen/src/Types.ts.hbs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// which we can't get using GenType
33
// Use @genType.import to link the types back to ReScript code
44

5-
import type { Logger, IndexerFromConfig, EffectCaller } from "envio";
6-
import type { EvmChains, EvmContracts, FuelChains, FuelContracts, SvmChains, Entities, GetWhereFilter } from "../envio.d.ts";
5+
import type { Logger, IndexerFromConfig, EffectCaller, GetWhereFilter } from "envio";
6+
import type { EvmChains, EvmContracts, FuelChains, FuelContracts, SvmChains, Entities } from "../envio.d.ts";
77

88
// Utility type to check if an object type is empty
99
type IsEmptyObject<T> = keyof T extends never ? true : false;
@@ -131,7 +131,7 @@ export type HandlerContext = {
131131
* Supports Hasura-style operators: _eq, _gt, _lt.
132132
* Currently only one filter field with one operator is supported per call.
133133
*/
134-
readonly getWhere: (filter: GetWhereFilter["{{entity.name.capitalized}}"]) => Promise<Entities["{{entity.name.capitalized}}"][]>,
134+
readonly getWhere: (filter: GetWhereFilter<Entities["{{entity.name.capitalized}}"]>) => Promise<Entities["{{entity.name.capitalized}}"][]>,
135135
/**
136136
* Returns the entity {{entity.name.original}} from the storage by ID.
137137
* If the entity is not found, creates it using provided parameters and returns it.

0 commit comments

Comments
 (0)