@antelopejs/interface-data-api 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +26 -22
  2. package/dist/antelope.test.js +1 -5
  3. package/dist/antelope.test.js.map +1 -1
  4. package/dist/components.d.ts +67 -39
  5. package/dist/components.js +106 -685
  6. package/dist/components.js.map +1 -1
  7. package/dist/index.d.ts +57 -68
  8. package/dist/index.js +114 -100
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal/computed.d.ts +6 -0
  11. package/dist/internal/computed.js +51 -0
  12. package/dist/internal/computed.js.map +1 -0
  13. package/dist/internal/controller-types.d.ts +34 -0
  14. package/dist/internal/controller-types.js +4 -0
  15. package/dist/internal/controller-types.js.map +1 -0
  16. package/dist/internal/default-filter.d.ts +4 -0
  17. package/dist/internal/default-filter.js +29 -0
  18. package/dist/internal/default-filter.js.map +1 -0
  19. package/dist/internal/deferred-joined-stream.d.ts +11 -0
  20. package/dist/internal/deferred-joined-stream.js +125 -0
  21. package/dist/internal/deferred-joined-stream.js.map +1 -0
  22. package/dist/internal/extractors.d.ts +25 -0
  23. package/dist/internal/extractors.js +31 -0
  24. package/dist/internal/extractors.js.map +1 -0
  25. package/dist/internal/field-types.d.ts +199 -0
  26. package/dist/internal/field-types.js +4 -0
  27. package/dist/internal/field-types.js.map +1 -0
  28. package/dist/internal/filters.d.ts +6 -0
  29. package/dist/internal/filters.js +22 -0
  30. package/dist/internal/filters.js.map +1 -0
  31. package/dist/internal/foreign.d.ts +6 -0
  32. package/dist/internal/foreign.js +116 -0
  33. package/dist/internal/foreign.js.map +1 -0
  34. package/dist/internal/guards.d.ts +39 -0
  35. package/dist/internal/guards.js +116 -0
  36. package/dist/internal/guards.js.map +1 -0
  37. package/dist/internal/joined.d.ts +28 -0
  38. package/dist/internal/joined.js +127 -0
  39. package/dist/internal/joined.js.map +1 -0
  40. package/dist/internal/list.d.ts +16 -0
  41. package/dist/internal/list.js +182 -0
  42. package/dist/internal/list.js.map +1 -0
  43. package/dist/internal/meta-records.d.ts +19 -0
  44. package/dist/internal/meta-records.js +78 -0
  45. package/dist/internal/meta-records.js.map +1 -0
  46. package/dist/internal/properties.d.ts +4 -0
  47. package/dist/internal/properties.js +45 -0
  48. package/dist/internal/properties.js.map +1 -0
  49. package/dist/internal/prototypes.d.ts +14 -0
  50. package/dist/internal/prototypes.js +38 -0
  51. package/dist/internal/prototypes.js.map +1 -0
  52. package/dist/internal/read-properties.d.ts +3 -0
  53. package/dist/internal/read-properties.js +29 -0
  54. package/dist/internal/read-properties.js.map +1 -0
  55. package/dist/internal/table-reference.d.ts +11 -0
  56. package/dist/internal/table-reference.js +46 -0
  57. package/dist/internal/table-reference.js.map +1 -0
  58. package/dist/internal/tables.d.ts +28 -0
  59. package/dist/internal/tables.js +48 -0
  60. package/dist/internal/tables.js.map +1 -0
  61. package/dist/internal/validation.d.ts +12 -0
  62. package/dist/internal/validation.js +100 -0
  63. package/dist/internal/validation.js.map +1 -0
  64. package/dist/metadata.d.ts +27 -228
  65. package/dist/metadata.js +84 -213
  66. package/dist/metadata.js.map +1 -1
  67. package/dist/tests/components/access_control.test.js +28 -20
  68. package/dist/tests/components/access_control.test.js.map +1 -1
  69. package/dist/tests/components/computed.test.js +36 -27
  70. package/dist/tests/components/computed.test.js.map +1 -1
  71. package/dist/tests/components/cross_instance.test.js +8 -6
  72. package/dist/tests/components/cross_instance.test.js.map +1 -1
  73. package/dist/tests/components/deferred_joined.test.js +57 -54
  74. package/dist/tests/components/deferred_joined.test.js.map +1 -1
  75. package/dist/tests/components/extract_generic.test.d.ts +1 -0
  76. package/dist/tests/components/extract_generic.test.js +64 -0
  77. package/dist/tests/components/extract_generic.test.js.map +1 -0
  78. package/dist/tests/components/filter_comparisons.test.d.ts +1 -0
  79. package/dist/tests/components/filter_comparisons.test.js +34 -0
  80. package/dist/tests/components/filter_comparisons.test.js.map +1 -0
  81. package/dist/tests/components/filter_list_route.test.d.ts +1 -0
  82. package/dist/tests/components/filter_list_route.test.js +182 -0
  83. package/dist/tests/components/filter_list_route.test.js.map +1 -0
  84. package/dist/tests/components/filter_options.test.js +11 -10
  85. package/dist/tests/components/filter_options.test.js.map +1 -1
  86. package/dist/tests/components/foreign_computed.test.js +23 -14
  87. package/dist/tests/components/foreign_computed.test.js.map +1 -1
  88. package/dist/tests/components/foreign_joined.test.js +31 -27
  89. package/dist/tests/components/foreign_joined.test.js.map +1 -1
  90. package/dist/tests/components/joined.test.js +59 -45
  91. package/dist/tests/components/joined.test.js.map +1 -1
  92. package/dist/tests/components/listable.test.js +78 -58
  93. package/dist/tests/components/listable.test.js.map +1 -1
  94. package/dist/tests/components/localized_sort.test.js +14 -11
  95. package/dist/tests/components/localized_sort.test.js.map +1 -1
  96. package/dist/tests/components/mandatory.test.js +25 -25
  97. package/dist/tests/components/mandatory.test.js.map +1 -1
  98. package/dist/tests/components/modifier_key.test.js +27 -18
  99. package/dist/tests/components/modifier_key.test.js.map +1 -1
  100. package/dist/tests/components/parse_body.test.d.ts +1 -0
  101. package/dist/tests/components/parse_body.test.js +76 -0
  102. package/dist/tests/components/parse_body.test.js.map +1 -0
  103. package/dist/tests/components/parsed_value.test.js +23 -21
  104. package/dist/tests/components/parsed_value.test.js.map +1 -1
  105. package/dist/tests/components/partial_edit.test.js +38 -32
  106. package/dist/tests/components/partial_edit.test.js.map +1 -1
  107. package/dist/tests/components/per_action_access.test.js +20 -12
  108. package/dist/tests/components/per_action_access.test.js.map +1 -1
  109. package/dist/tests/components/prototype_named_filter.test.d.ts +1 -0
  110. package/dist/tests/components/prototype_named_filter.test.js +87 -0
  111. package/dist/tests/components/prototype_named_filter.test.js.map +1 -0
  112. package/dist/tests/components/validator.test.js +45 -37
  113. package/dist/tests/components/validator.test.js.map +1 -1
  114. package/dist/tests/components/written_row.test.d.ts +1 -0
  115. package/dist/tests/components/written_row.test.js +99 -0
  116. package/dist/tests/components/written_row.test.js.map +1 -0
  117. package/dist/tests/helpers/other-instance.d.ts +11 -1
  118. package/dist/tests/helpers/other-instance.js +35 -9
  119. package/dist/tests/helpers/other-instance.js.map +1 -1
  120. package/dist/tests/index/field_decorator_target.test.d.ts +1 -0
  121. package/dist/tests/index/field_decorator_target.test.js +20 -0
  122. package/dist/tests/index/field_decorator_target.test.js.map +1 -0
  123. package/dist/tests/index/get_guard.test.js +6 -6
  124. package/dist/tests/index/get_guard.test.js.map +1 -1
  125. package/dist/tests/index/inheritance.test.js +14 -9
  126. package/dist/tests/index/inheritance.test.js.map +1 -1
  127. package/dist/tests/index/metadata_names.test.d.ts +1 -0
  128. package/dist/tests/index/metadata_names.test.js +91 -0
  129. package/dist/tests/index/metadata_names.test.js.map +1 -0
  130. package/dist/tests/index/routes.test.js +49 -44
  131. package/dist/tests/index/routes.test.js.map +1 -1
  132. package/dist/tests/integration.test.d.ts +1 -1
  133. package/dist/tests/integration.test.js +66 -729
  134. package/dist/tests/integration.test.js.map +1 -1
  135. package/dist/tests/internal/controller-fields.test.d.ts +1 -0
  136. package/dist/tests/internal/controller-fields.test.js +25 -0
  137. package/dist/tests/internal/controller-fields.test.js.map +1 -0
  138. package/dist/tests/internal/http.d.ts +35 -0
  139. package/dist/tests/internal/http.js +56 -0
  140. package/dist/tests/internal/http.js.map +1 -0
  141. package/dist/tests/internal/shop-api.d.ts +1 -0
  142. package/dist/tests/internal/shop-api.js +331 -0
  143. package/dist/tests/internal/shop-api.js.map +1 -0
  144. package/dist/tests/internal/shop-schema.d.ts +162 -0
  145. package/dist/tests/internal/shop-schema.js +387 -0
  146. package/dist/tests/internal/shop-schema.js.map +1 -0
  147. package/dist/tests/utils.d.ts +8 -10
  148. package/dist/tests/utils.js +1 -3
  149. package/dist/tests/utils.js.map +1 -1
  150. package/dist/tests/utils.test.js +10 -11
  151. package/dist/tests/utils.test.js.map +1 -1
  152. package/docs/1.introduction.md +38 -35
  153. package/docs/10.joined.md +51 -40
  154. package/docs/11.computed.md +91 -47
  155. package/docs/2.data-controllers.md +50 -42
  156. package/docs/3.routes.md +156 -134
  157. package/docs/4.access-rights.md +48 -44
  158. package/docs/5.validators.md +65 -47
  159. package/docs/6.listable.md +53 -49
  160. package/docs/7.foreign-keys.md +55 -48
  161. package/docs/8.filters.md +65 -54
  162. package/docs/9.modifiers.md +29 -25
  163. package/package.json +34 -13
  164. package/skills/data-api-interface/SKILL.md +13 -3
  165. package/skills/data-api-interface/cases.json +99 -0
@@ -1,18 +1,22 @@
1
- # Foreign Keys
1
+ ---
2
+ title: Foreign keys
3
+ description: Resolve a foreign key field into the referenced record with @Foreign, for single and array references, across schemas, and per route.
4
+ mode: reference
5
+ ---
2
6
 
3
7
  ## Overview
4
8
 
5
- The `@Foreign` decorator establishes relationships between database tables in your data controller. When a field holds a foreign key (an ID referencing another table), the Data API automatically resolves the reference and includes the related record in API responses. This works for both single references and arrays of references.
9
+ The `@Foreign` decorator links a field of your data controller to another table. When a field holds a foreign key (an ID of a record in another table), the Data API looks up the referenced record and puts it in the response. The field can hold one key or an array of keys.
6
10
 
7
- ## Basic Usage
11
+ ## Basic usage
8
12
 
9
- Apply `@Foreign` to a field that stores a foreign key value. The decorator replaces the raw ID in the response with the full referenced record.
13
+ Apply `@Foreign` to a field that stores a foreign key value. The response then holds the referenced record in place of the raw ID.
10
14
 
11
- Choose the response fields explicitly with `pluck`. A foreign lookup reads the target table, not its API controller: do not rely on the target controller's `@Access` policy to hide passwords or internal fields. The example below selects only the user's ID, name, and email.
15
+ Choose the response fields with `pluck`. A foreign lookup reads the target table, not its API controller, so the target controller's `@Access` policy does not hide passwords or internal fields. The example below selects only the user's ID, name, and email.
12
16
 
13
- The snippets use the `myapp` schema. Register it in the declaring module before serving requests, as in the [quick start](./1.introduction.md#quick-start). Controller snippets assume the corresponding model and shared imports are already declared.
17
+ The snippets use the `myapp` schema. Register it in the declaring module before serving requests, as in the [quick start](./1.introduction.md#quick-start). The controller snippets assume that the matching model and the shared imports exist.
14
18
 
15
- ```typescript
19
+ ```typescript fragment
16
20
  import { Foreign } from "@antelopejs/interface-data-api/metadata";
17
21
  import {
18
22
  Field,
@@ -71,7 +75,7 @@ class OrderAPI extends DataController(
71
75
  }
72
76
  ```
73
77
 
74
- A GET request to `/orders/get?id=order-123` returns the full user object instead of a raw ID:
78
+ A GET request to `/orders/get?id=order-123` returns the user object in place of the raw ID:
75
79
 
76
80
  ```json
77
81
  {
@@ -85,47 +89,48 @@ A GET request to `/orders/get?id=order-123` returns the full user object instead
85
89
  }
86
90
  ```
87
91
 
88
- ## Decorator Parameters
92
+ ## Decorator parameters
89
93
 
90
- ```typescript
91
- @Foreign(table, index?, multi?, pluck?, schema?)
94
+ ```typescript fragment
95
+ @Foreign(table, index?, multi?, pluck?, schema?, targetMeta?)
92
96
  ```
93
97
 
94
- | Parameter | Type | Required | Default | Description |
95
- | --------- | -------------------------- | -------- | ----------------- | -------------------------------------------------------------------------------------------------- |
96
- | `table` | `Class<Table>` or `string` | Yes | - | The target table class or table name string |
97
- | `index` | `string` | No | `"_id"` | The index on the target table to match against |
98
- | `multi` | `boolean` | No | `false` | Set to `true` when the field holds an array of foreign keys |
99
- | `pluck` | `string[]` | No | All fields | List of fields to include from the referenced record |
100
- | `schema` | `string` | No | Controller schema | Name of the schema the target table is registered in, when it differs from the controller's schema |
98
+ | Parameter | Type | Required | Default | Description |
99
+ | ------------ | -------------------------- | -------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
+ | `table` | `Class<Table>` or `string` | Yes | None | The target table class or table name string |
101
+ | `index` | `string` | No | `"_id"` | The index on the target table to match against |
102
+ | `multi` | `boolean` | No | `false` | Set to `true` when the field holds an array of foreign keys |
103
+ | `pluck` | `string[]` | No | All fields | List of fields to include from the referenced record |
104
+ | `schema` | `string` | No | Controller schema | Name of the schema the target table is registered in, when it differs from the controller's schema |
105
+ | `targetMeta` | `DataAPIMeta` | No | None | Metadata of the target's data controller. Its `@Joined` fields that `pluck` names, or all of them when `pluck` is unset, are resolved on a relation that is not `multi` |
101
106
 
102
- ## Table Reference Methods
107
+ ## Table reference methods
103
108
 
104
- ### Class Reference
109
+ ### Class reference
105
110
 
106
- Pass the table class directly. This provides IDE autocompletion, refactoring support, and compile-time validation.
111
+ Pass the table class. Your editor can then complete and rename the reference, and the compiler checks it.
107
112
 
108
- ```typescript
113
+ ```typescript fragment
109
114
  @Foreign(User)
110
115
  declare userId: User;
111
116
  ```
112
117
 
113
- ### String Reference
118
+ ### String reference
114
119
 
115
- Pass the registered table name as a string. Use this when importing the class would create circular dependencies.
120
+ Pass the registered table name as a string. Use a string when importing the class would create a circular import.
116
121
 
117
- ```typescript
122
+ ```typescript fragment
118
123
  @Foreign("users")
119
124
  declare userId: User;
120
125
  ```
121
126
 
122
- ## Multi-Record References
127
+ ## Multi-record references
123
128
 
124
- When a field stores an array of foreign key IDs, set the `multi` parameter to `true`. The API resolves each ID and returns an array of full records.
129
+ When a field stores an array of foreign key IDs, set the `multi` parameter to `true`. The API looks up each ID and returns an array of records.
125
130
 
126
- The table-level `@Relation({ many: true })` is declarative metadata recording that the field holds multiple target keys; API-side resolution is controlled solely by the `multi` parameter of `@Foreign`.
131
+ The table-level `@Relation({ many: true })` is metadata only. It records that the field holds several target keys. Only the `multi` parameter of `@Foreign` controls how the API resolves the field.
127
132
 
128
- ```typescript
133
+ ```typescript fragment
129
134
  @RegisterTable("carts", "myapp")
130
135
  class Cart extends Table {
131
136
  @Field("string")
@@ -181,11 +186,11 @@ Response:
181
186
  }
182
187
  ```
183
188
 
184
- ## Selective Field Retrieval with Pluck
189
+ ## Selective field retrieval with pluck
185
190
 
186
- The `pluck` parameter limits which fields are included from the referenced record. This reduces response payload size when you only need specific fields.
191
+ The `pluck` parameter limits the fields taken from the referenced record. Use it to keep the response small when you need only some fields.
187
192
 
188
- ```typescript
193
+ ```typescript fragment
189
194
  @RegisterDataController()
190
195
  class CartAPI extends DataController(
191
196
  Cart,
@@ -239,46 +244,48 @@ Response with `pluck: ["name", "email"]`:
239
244
  }
240
245
  ```
241
246
 
242
- ## Custom Index
247
+ ## Custom index
243
248
 
244
- Use the `index` parameter to match against a secondary index instead of the primary key.
249
+ The `index` parameter matches the field against a secondary index of the target table, in place of the primary key.
245
250
 
246
- ```typescript
247
- // Match userId against the "email" index on the User table
251
+ ```typescript fragment
252
+ // Match userEmail against the "email" index on the User table
248
253
  @Foreign(User, "email")
249
254
  declare userEmail: User;
250
255
  ```
251
256
 
252
- ## Cross-Schema References
257
+ ## Cross-schema references
253
258
 
254
- By default, the target table is resolved in the same schema as the controller's table. Use the `schema` parameter to reference a table registered in a different schema. At runtime the foreign lookup is resolved through the target's table class, against the default instance of the schema `RegisterSchema` bound it to, so it follows the database of the module that declared the table. The target's class must be registered with `@RegisterTable` before the field is declared; a string table name with no registered class in that schema throws at declaration.
259
+ By default, the Data API looks for the target table in the schema of the controller's table. The `schema` parameter names a different schema. At runtime, the foreign lookup goes through the target's table class, against the default instance of the schema that `RegisterSchema` bound it to. The lookup therefore uses the database of the module that declared the table. Register the target's class with `@RegisterTable` before you declare the field. A string table name with no registered class in that schema throws at declaration.
255
260
 
256
- ```typescript
261
+ ```typescript fragment
257
262
  // Resolve userId against the User table registered in the "auth" schema
258
263
  @Access(AccessMode.ReadOnly)
259
264
  @Foreign(User, undefined, false, undefined, "auth")
260
265
  declare userId: User;
261
266
  ```
262
267
 
263
- When a table class is passed (instead of a string), its schema is inferred from its `@RegisterTable` metadata, so the `schema` parameter is only needed to override that or when passing a string table name. If both the class metadata and the explicit `schema` option specify a schema and they disagree, an error is thrown.
268
+ When you pass a table class, the Data API reads its schema from its `@RegisterTable` metadata. The `schema` parameter is then needed only with a string table name, or when the class metadata names no schema. If the class metadata and the `schema` parameter name different schemas, the declaration throws.
264
269
 
265
- ## Disable Foreign Key Resolution
270
+ ## Disable foreign key resolution
266
271
 
267
- Use the `noForeign` route option to skip foreign key resolution for specific endpoints. This returns raw IDs instead of resolved objects.
272
+ The `noForeign` route option skips foreign key resolution on one route. The route then returns raw IDs in place of records. On a `get` route the option is a string, and on a `list` route it is a boolean.
268
273
 
269
274
  ```typescript
275
+ import { DefaultRoutes } from "@antelopejs/interface-data-api";
276
+
270
277
  const routes = {
271
278
  get: DefaultRoutes.Get,
272
279
  getRaw: DefaultRoutes.WithOptions(DefaultRoutes.Get, { noForeign: "true" }),
273
280
  };
274
281
  ```
275
282
 
276
- ## Error Handling
283
+ ## Error handling
277
284
 
278
- - **Missing References** - If a referenced record does not exist, the field value is `null`.
279
- - **Invalid IDs** - Invalid foreign key values result in `null` for the resolved field.
280
- - **Arrays with Missing Items** - In multi-record references, missing items appear as `null` in the array.
285
+ - If a referenced record does not exist, the field value is `null`.
286
+ - An invalid foreign key value resolves to `null`.
287
+ - In a multi-record reference, each missing item is `null` in the array.
281
288
 
282
- ## Next Steps
289
+ ## Next steps
283
290
 
284
- See the [filters documentation](./8.filters.md) to learn how to add filtering capabilities to list endpoints.
291
+ To filter list results, see [Filters](./8.filters.md).
package/docs/8.filters.md CHANGED
@@ -1,16 +1,20 @@
1
- # Filters
1
+ ---
2
+ title: Filters
3
+ description: Let list requests filter rows with @Filter, through the default comparisons, custom filter functions, and mandatory route filters.
4
+ mode: reference
5
+ ---
2
6
 
3
7
  ## Overview
4
8
 
5
- The `@Filter` decorator adds filtering capabilities to list endpoints. Filters allow clients to narrow down results using query parameters. The module provides a default filter with standard comparison operators and supports custom filter functions for advanced logic.
9
+ The `@Filter` decorator lets a list request narrow its results through query parameters. The default filter compares the field with six comparison operators. A custom filter function can hold any other logic.
6
10
 
7
- ## Basic Usage
11
+ ## Basic usage
8
12
 
9
- ### Default Filter
13
+ ### Default filter
10
14
 
11
- Apply `@Filter()` without arguments to enable standard comparison filtering on a string field. Query filter values remain strings: TypeScript annotations, `@Field`, and write validators do not parse them into numbers or booleans.
15
+ `@Filter()` without arguments enables the default comparison filter on a string field. Filter values stay strings. TypeScript annotations, `@Field`, and write validators do not parse them into numbers or booleans.
12
16
 
13
- ```typescript
17
+ ```typescript fragment
14
18
  import { Filter } from "@antelopejs/interface-data-api/metadata";
15
19
 
16
20
  @RegisterDataController()
@@ -39,18 +43,18 @@ class UserAPI extends DataController(
39
43
  }
40
44
  ```
41
45
 
42
- ### Query Parameter Format
46
+ ### Query parameter format
43
47
 
44
- Filters are applied through query parameters using the following format:
48
+ A filter is a query parameter in one of these forms:
45
49
 
46
50
  ```
47
51
  filter_<fieldName>=<value>
48
52
  filter_<fieldName>=<operator>:<value>
49
53
  ```
50
54
 
51
- When no operator is specified, `eq` (equal) is used by default.
55
+ Without an operator, the filter uses `eq` (equal). The text before the first colon is the operator, so a value that contains a colon needs the `eq:` prefix. The default filter answers `400` to an operator that is not in the table below.
52
56
 
53
- ### Comparison Operators
57
+ ### Comparison operators
54
58
 
55
59
  | Operator | Description |
56
60
  | -------- | ------------------------ |
@@ -75,7 +79,7 @@ Retrieve users whose role is not admin:
75
79
  GET /users/list?filter_role=ne:admin
76
80
  ```
77
81
 
78
- Combine multiple filters:
82
+ Combine filters on several fields:
79
83
 
80
84
  ```http
81
85
  GET /users/list?filter_status=active&filter_role=ne:admin
@@ -87,16 +91,24 @@ Filter one field several times to keep the rows that pass every filter (AND):
87
91
  GET /users/list?filter_age=ge:18&filter_age=lt:65
88
92
  ```
89
93
 
90
- The route receives that field's filters as a list of `FilterValue` tuples (`FieldFilter`); a field filtered once keeps a single tuple. Read either shape with `FilterValuesOf(filter)` from `@antelopejs/interface-data-api/metadata`. An indexed equality filter only uses its index when it is the field's only filter.
94
+ The route receives the filters of that field as a list of `FilterValue` tuples (`FieldFilter`). A field filtered once keeps a single tuple. `FilterValuesOf(filter)` from `@antelopejs/interface-data-api/metadata` reads either shape as a list. An indexed equality filter uses its index only when it is the only filter on the field.
91
95
 
92
- ### Numeric and Boolean Filters
96
+ ### Numeric and boolean filters
93
97
 
94
- Parse and validate values explicitly in a custom filter. These filters support the six numeric comparisons and boolean equality/inequality; unsupported input throws an HTTP 400 response.
98
+ Parse and validate numbers and booleans in a custom filter. The filters below support the six numeric comparisons and boolean `eq` and `ne`. Any other input fails with an HTTP 400 response.
95
99
 
96
100
  ```ts docs-check
97
101
  import { assert } from "@antelopejs/interface-api-util";
98
102
  import type { FilterFunction } from "@antelopejs/interface-data-api/metadata";
99
103
 
104
+ type Comparison = "eq" | "ne" | "gt" | "ge" | "lt" | "le";
105
+
106
+ const COMPARISONS: readonly Comparison[] = ["eq", "ne", "gt", "ge", "lt", "le"];
107
+
108
+ function isComparison(mode: string): mode is Comparison {
109
+ return COMPARISONS.some((comparison) => comparison === mode);
110
+ }
111
+
100
112
  export const numberFilter: FilterFunction<Record<string, unknown>> = (
101
113
  _context,
102
114
  proxy,
@@ -106,11 +118,7 @@ export const numberFilter: FilterFunction<Record<string, unknown>> = (
106
118
  ) => {
107
119
  const value = Number(raw);
108
120
  assert(raw.trim() !== "" && Number.isFinite(value), 400, "Invalid number");
109
- assert(
110
- ["eq", "ne", "gt", "ge", "lt", "le"].includes(mode),
111
- 400,
112
- "Invalid comparison",
113
- );
121
+ assert(isComparison(mode), 400, "Invalid comparison");
114
122
  return proxy[mode](value);
115
123
  };
116
124
 
@@ -127,9 +135,9 @@ export const booleanFilter: FilterFunction<Record<string, unknown>> = (
127
135
  };
128
136
  ```
129
137
 
130
- Apply the functions to the corresponding fields in your controller:
138
+ Apply the functions to the matching fields of your controller:
131
139
 
132
- ```typescript
140
+ ```typescript fragment
133
141
  @Filter(numberFilter, false)
134
142
  declare age: number;
135
143
 
@@ -137,37 +145,40 @@ declare age: number;
137
145
  declare isActive: boolean;
138
146
  ```
139
147
 
140
- With these custom filters, `?filter_age=gt:30&filter_isActive=true` compares against `30` and `true`, not `"30"` and `"true"`. Keep `useIndex` false for callbacks that parse values: indexed equality uses the raw string directly and bypasses the filter callback.
148
+ With these custom filters, `?filter_age=gt:30&filter_isActive=true` compares against `30` and `true`, not `"30"` and `"true"`. Keep `useIndex` false for a filter that parses its value. An indexed equality lookup uses the raw string and skips the filter function.
141
149
 
142
- ## Custom Filters
150
+ ## Custom filters
143
151
 
144
- For filtering logic beyond simple comparisons, pass a custom filter function to `@Filter`.
152
+ For logic beyond the comparison operators, pass a custom filter function to `@Filter`.
145
153
 
146
- ### Filter Function Signature
154
+ ### Filter function signature
147
155
 
148
- ```typescript
149
- type FilterFunction<T, U> = (
156
+ ```typescript fragment
157
+ type FilterFunction<
158
+ T extends Record<string, any>,
159
+ U extends Record<string, any> = FieldValues,
160
+ > = (
150
161
  context: RequestContext & { this: T },
151
162
  proxy: ValueProxy<any>,
152
163
  key: string,
153
- value: string,
154
- mode: "eq" | "ne" | "gt" | "ge" | "lt" | "le",
155
- row: ValueProxy<U>,
164
+ ...rest: [value: FilterValue[0], mode: FilterValue[1], row: ValueProxy<U>]
156
165
  ) => ValueProxyOrValue<boolean>;
157
166
  ```
158
167
 
159
- | Parameter | Description |
160
- | --------- | ------------------------------------------------------------------------------------------------------- |
161
- | `context` | The request context, extended with `this` referencing the controller instance |
162
- | `proxy` | A query expression produced by the field modifier's `unlockrequest()` method; see the limitations below |
163
- | `key` | The field name being filtered |
164
- | `value` | The filter value from the query parameter |
165
- | `mode` | The comparison operator from the query parameter |
166
- | `row` | The full database row as a `ValueProxy` |
168
+ | Parameter | Description |
169
+ | --------- | -------------------------------------------------------------------------------------------------------- |
170
+ | `context` | The request context, with `this` set to the controller instance |
171
+ | `proxy` | A query expression that the field modifier's `unlockrequest()` method builds. See the limitations below. |
172
+ | `key` | The name of the filtered field |
173
+ | `value` | The filter value from the query parameter, a string |
174
+ | `mode` | The text before the first colon of the query parameter, unchecked. The filter defines its own modes. |
175
+ | `row` | The full database row as a `ValueProxy` |
176
+
177
+ `value`, `mode`, and `row` form one rest tuple, so the function type stays within five parameters. A function that declares them as separate parameters is assignable to the type, and `Parameters<FilterFunction<T, U>>` lists all six.
167
178
 
168
- ### Example: Pattern Matching Filter
179
+ ### Example: pattern matching filter
169
180
 
170
- ```typescript
181
+ ```typescript fragment
171
182
  function filterByEmailDomain(
172
183
  context: RequestContext,
173
184
  proxy: ValueProxy<any>,
@@ -209,13 +220,13 @@ Query for users with Gmail addresses:
209
220
  GET /users/list?filter_email=gmail.com
210
221
  ```
211
222
 
212
- Import `assert` from `@antelopejs/interface-api-util`. Escaping the domain makes the dot literal rather than a regular-expression wildcard. This example deliberately rejects operators other than `eq` instead of silently treating every request as equality.
223
+ Import `assert` from `@antelopejs/interface-api-util`. The function escapes the domain, so the dot matches a literal dot and not any character. The function rejects operators other than `eq`, so a request with another operator fails instead of running as an equality filter.
213
224
 
214
- ### The `useIndex` Parameter
225
+ ### The `useIndex` parameter
215
226
 
216
- The `@Filter` decorator accepts an optional second parameter, `useIndex`, that controls whether the filter can use a database index for optimized lookups.
227
+ The second parameter of `@Filter`, `useIndex`, allows the filter to use a database index.
217
228
 
218
- ```typescript
229
+ ```typescript fragment
219
230
  @Filter(undefined, true) // Default filter with index optimization
220
231
  declare status: string;
221
232
 
@@ -223,16 +234,16 @@ declare status: string;
223
234
  declare name: string;
224
235
  ```
225
236
 
226
- When `useIndex` is not specified:
237
+ When `useIndex` is not set:
227
238
 
228
- - Default filters (no custom function) automatically use the index if the field has one.
229
- - Custom filters default to no index usage.
239
+ - The default filter (no custom function) uses the index when the field has a single-field index.
240
+ - A custom filter uses no index.
230
241
 
231
- Index-optimized filters with an `eq` operator perform an indexed lookup instead of a full table scan, which significantly improves performance on large datasets.
242
+ An indexed filter with the `eq` operator reads the matching rows through the index instead of scanning the whole table.
232
243
 
233
- ## Mandatory Route Filters
244
+ ## Mandatory route filters
234
245
 
235
- Use `DefaultRoutes.WithOptions` to constrain a list route. The `filters` map contains `[stringValue, comparison]` tuples for fields declared with `@Filter`. Request filters for other fields are retained, but a route filter wins when both specify the same field.
246
+ `DefaultRoutes.WithOptions` can constrain a list route. Its `filters` map holds `[stringValue, comparison]` tuples for fields declared with `@Filter`. The route keeps request filters on other fields. When the route and the request filter the same field, the route filter wins.
236
247
 
237
248
  ```ts docs-check
238
249
  import { DefaultRoutes } from "@antelopejs/interface-data-api";
@@ -242,10 +253,10 @@ const activeUsers = DefaultRoutes.WithOptions(DefaultRoutes.List, {
242
253
  });
243
254
  ```
244
255
 
245
- On a controller with a `status` filter, `?filter_status=inactive&filter_role=admin` still restricts `status` to `"active"` while retaining the requested `role`. Route filters are list-query constraints, not authorization for separate get/edit/delete routes.
256
+ On a controller with a `status` filter, `?filter_status=inactive&filter_role=admin` still restricts `status` to `"active"` and keeps the requested `role`. Route filters constrain list queries only. They do not authorize or restrict the `get`, `edit`, or `delete` routes.
246
257
 
247
- ## Modifier Integration
258
+ ## Modifier integration
248
259
 
249
- The `proxy` parameter is a database expression, not an unlocked model instance. The Data API calls the modifier's `unlockrequest()` method to build that expression. Localization can select a locale at query time, but the built-in `EncryptionModifier.unlockrequest()` returns the stored expression unchanged: it does not decrypt ciphertext in the database. A custom modifier must explicitly support query-time access before a filter can operate on its logical value.
260
+ The `proxy` parameter is a database expression, not an unlocked model instance. The Data API calls the modifier's `unlockrequest()` method to build that expression. Localization can select a locale at query time. The built-in `EncryptionModifier.unlockrequest()` returns the stored expression unchanged, so it does not decrypt ciphertext in the database. A custom modifier must support query-time access before a filter can compare its plain value.
250
261
 
251
- For raw database values, use the `row` parameter and access the field directly.
262
+ For raw database values, read the field from the `row` parameter.
@@ -1,14 +1,18 @@
1
- # Modifiers
1
+ ---
2
+ title: Modifiers
3
+ description: Use database modifiers such as @Hashed, @Encrypted, and @Localized with a data controller, and pass a per-request modifier key with @ModifierKey.
4
+ mode: reference
5
+ ---
2
6
 
3
7
  ## Overview
4
8
 
5
- Modifiers from `@antelopejs/interface-database-decorators` work automatically with the Data API. Modifiers such as `@Hashed` or `@Encrypted` transform data transparently during read and write operations without any additional configuration in the data controller.
9
+ Modifiers from `@antelopejs/interface-database-decorators` work with the Data API without extra configuration in the data controller. Modifiers such as `@Hashed` or `@Encrypted` transform data on every read and write.
6
10
 
7
- Some modifiers require a runtime parameter (for example, a locale key for localization). The `@ModifierKey` decorator bridges this gap by mapping a controller field to a modifier's runtime key.
11
+ Some modifiers need a value at runtime, such as the locale key of the localization modifier. The `@ModifierKey` decorator maps a controller field to the runtime key of one modifier.
8
12
 
9
- ## The `@ModifierKey` Decorator
13
+ ## The `@ModifierKey` decorator
10
14
 
11
- Use `@ModifierKey` when a modifier needs a runtime value that varies per request. The decorator associates a controller field with a specific modifier class. The field's value at request time is passed to the modifier during lock and unlock operations.
15
+ Use `@ModifierKey` when a modifier needs a value that changes per request. The decorator links a controller field to a modifier class. At request time, the Data API passes the value of the field to the modifier when it locks and unlocks data.
12
16
 
13
17
  ```typescript
14
18
  import { ModifierKey } from "@antelopejs/interface-data-api/metadata";
@@ -16,17 +20,17 @@ import { ModifierKey } from "@antelopejs/interface-data-api/metadata";
16
20
 
17
21
  ### Signature
18
22
 
19
- ```typescript
23
+ ```typescript fragment
20
24
  @ModifierKey(modifierClass: typeof ContainerModifier<any>)
21
25
  ```
22
26
 
23
- | Parameter | Description |
24
- | --------------- | ----------------------------------------------------------------------------- |
25
- | `modifierClass` | The modifier class that requires a runtime key (e.g., `LocalizationModifier`) |
27
+ | Parameter | Description |
28
+ | --------------- | ------------------------------------------------------------------------------- |
29
+ | `modifierClass` | The modifier class that needs a runtime key, for example `LocalizationModifier` |
26
30
 
27
- ### Example: Localized Content
31
+ ### Example: localized content
28
32
 
29
- The `LocalizationModifier` requires a locale identifier to determine which translation to store or retrieve. Use `@ModifierKey` to supply this value from an HTTP header.
33
+ The `LocalizationModifier` needs a locale identifier to choose which translation to store or read. In this example, `@ModifierKey` takes that value from an HTTP header.
30
34
 
31
35
  ```typescript
32
36
  import { Controller, Parameter } from "@antelopejs/interface-api";
@@ -92,14 +96,14 @@ class ContentAPI extends DataController(
92
96
  }
93
97
  ```
94
98
 
95
- ### How It Works
99
+ ### How it works
96
100
 
97
101
  1. The HTTP request includes the header `x-content-language: fr`.
98
102
  2. The `language` property receives the value `"fr"`.
99
- 3. During write operations, the modifier uses `"fr"` as the key to store the localized value.
100
- 4. During read operations, the modifier uses `"fr"` to retrieve the correct localized value.
103
+ 3. On a write, the modifier stores the localized value under the key `"fr"`.
104
+ 4. On a read, the modifier reads the localized value stored under `"fr"`.
101
105
 
102
- **Example request:**
106
+ This request writes French values:
103
107
 
104
108
  ```http
105
109
  POST /content/new
@@ -112,24 +116,24 @@ Content-Type: application/json
112
116
  }
113
117
  ```
114
118
 
115
- The data is stored under the `"fr"` locale key. A subsequent request with `x-content-language: en` accesses different localized values for the same record.
119
+ The record stores the data under the `"fr"` locale key. A later request with `x-content-language: en` reads and writes other localized values of the same record.
116
120
 
117
- > **Note:** Most modifiers (such as `@Hashed` and `@Encrypted`) do not require `@ModifierKey`. Only modifiers that need a runtime parameter per request require this decorator.
121
+ Most modifiers, such as `@Hashed` and `@Encrypted`, do not need `@ModifierKey`. Only a modifier that needs a runtime value per request needs this decorator.
118
122
 
119
- ## Integration with Other Features
123
+ ## Integration with other features
120
124
 
121
- ### Foreign Keys
125
+ ### Foreign keys
122
126
 
123
- Modifiers work automatically with foreign key relationships. When the Data API resolves foreign records, modifier-protected fields on those records are properly unlocked during retrieval. See the [foreign keys documentation](./7.foreign-keys.md) for details.
127
+ Modifiers work with foreign key relationships. When the Data API resolves foreign records, it unlocks the modifier fields of those records. See [Foreign keys](./7.foreign-keys.md) for details.
124
128
 
125
129
  ### Filters
126
130
 
127
- Custom filters receive a query expression produced by the modifier's `unlockrequest()` method, not a decrypted JavaScript value. Localization supports query-time access; the built-in encryption modifier leaves its stored representation unchanged in query expressions. Do not use it to filter by plaintext. See the [filters documentation](./8.filters.md#modifier-integration) for details.
131
+ A custom filter receives a query expression that the modifier's `unlockrequest()` method builds, not a decrypted JavaScript value. Localization supports access at query time. The built-in encryption modifier leaves its stored value unchanged in query expressions, so do not filter on the plain text of an encrypted field. See [Modifier integration](./8.filters.md#modifier-integration) in the filters page for details.
128
132
 
129
- ## Learn More
133
+ ## Learn more
130
134
 
131
- For the full list of available modifiers and instructions on creating custom ones, see the [@antelopejs/interface-database-decorators documentation](https://github.com/AntelopeJS/interface-database-decorators).
135
+ For the full list of modifiers and how to write your own, see the [`@antelopejs/interface-database-decorators` repository](https://github.com/AntelopeJS/interface-database-decorators).
132
136
 
133
- ## Next Steps
137
+ ## Next steps
134
138
 
135
- See the [joined fields documentation](./10.joined.md) to learn how to flatten a remote field onto rows for native sorting and filtering.
139
+ To flatten a field of another table onto each row for sorting and filtering in the database, see [Joined fields](./10.joined.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/interface-data-api",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "AntelopeJS interface that generates CRUD REST endpoints from database tables, with access rules, validation and pagination",
5
5
  "keywords": [
6
6
  "antelopejs",
@@ -60,16 +60,25 @@
60
60
  "release": "pnpm run lint && pnpm run prepack && release-it",
61
61
  "test": "pnpm run build && ajs module test .",
62
62
  "format:check": "oxfmt --check .",
63
- "knip": "knip"
63
+ "knip": "knip",
64
+ "prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi",
65
+ "checks": "antelopejs-checks",
66
+ "size": "antelopejs-check-size",
67
+ "sync": "antelopejs-sync",
68
+ "sync:check": "antelopejs-sync --check",
69
+ "typecheck": "tsc --noEmit",
70
+ "suppressions": "antelopejs-suppressions",
71
+ "suppressions:update": "antelopejs-suppressions --update"
64
72
  },
65
73
  "devDependencies": {
66
- "@antelopejs/core": "^1.16.0",
67
- "@antelopejs/interface-api": "^0.0.15",
68
- "@antelopejs/interface-api-util": "^0.1.3",
69
- "@antelopejs/interface-core": "^0.2.4",
70
- "@antelopejs/interface-database": "^0.1.11",
71
- "@antelopejs/interface-database-decorators": "^0.1.10",
72
- "@antelopejs/tooling-configs": "^0.0.9",
74
+ "@antelopejs/api": "file:.campaign/tgz/antelopejs-api-6afe3e2.tgz",
75
+ "@antelopejs/core": "~1.20.0",
76
+ "@antelopejs/interface-api": "^0.2.0",
77
+ "@antelopejs/interface-api-util": "^0.3.0",
78
+ "@antelopejs/interface-core": "^0.4.0",
79
+ "@antelopejs/interface-database": "^0.3.0",
80
+ "@antelopejs/interface-database-decorators": "^0.2.0",
81
+ "@antelopejs/tooling-configs": "^0.2.17",
73
82
  "@types/chai": "^4.3.20",
74
83
  "@types/mocha": "^10.0.10",
75
84
  "@types/node": "^22.19.15",
@@ -77,6 +86,7 @@
77
86
  "chai": "^4.5.0",
78
87
  "eslint-plugin-perfectionist": "^5.11.0",
79
88
  "knip": "^6.34.0",
89
+ "lefthook": "^2.2.2",
80
90
  "mongodb-memory-server-core": "^10.4.3",
81
91
  "oxfmt": "^0.66.0",
82
92
  "oxlint": "1.81.0",
@@ -84,21 +94,32 @@
84
94
  "release-it": "^19.2.4",
85
95
  "release-it-changelogen": "^0.1.0",
86
96
  "rimraf": "^6.1.3",
87
- "typescript": "^5.9.3"
97
+ "typescript": "^5.9.3",
98
+ "zod": "4.5.4"
88
99
  },
89
100
  "peerDependencies": {
90
- "@antelopejs/interface-api": ">=0.0.15 <1.0.0",
101
+ "@antelopejs/interface-api": ">=0.3.0 <1.0.0",
91
102
  "@antelopejs/interface-api-util": ">=0.1.3 <1.0.0",
92
103
  "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
93
104
  "@antelopejs/interface-database": ">=0.1.10 <1.0.0",
94
105
  "@antelopejs/interface-database-decorators": ">=0.1.10 <1.0.0"
95
106
  },
96
- "packageManager": "pnpm@10.6.5",
107
+ "packageManager": "pnpm@10.33.0",
108
+ "pnpm": {
109
+ "overrides": {
110
+ "@antelopejs/interface-api": "0.2.0",
111
+ "@antelopejs/interface-api-util": "0.3.0",
112
+ "@antelopejs/api": "file:.campaign/tgz/antelopejs-api-6afe3e2.tgz"
113
+ }
114
+ },
97
115
  "antelopeJs": {
98
116
  "standalone": true,
99
117
  "test": "src/antelope.test.ts",
100
118
  "skills": [
101
119
  "./skills"
102
- ]
120
+ ],
121
+ "docs": {
122
+ "standard": true
123
+ }
103
124
  }
104
125
  }