@antelopejs/interface-data-api 0.5.0 → 0.6.1

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 (139) 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 +57 -37
  5. package/dist/components.js +36 -68
  6. package/dist/components.js.map +1 -1
  7. package/dist/index.d.ts +44 -60
  8. package/dist/index.js +22 -10
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal/computed.js +12 -4
  11. package/dist/internal/computed.js.map +1 -1
  12. package/dist/internal/controller-types.d.ts +19 -4
  13. package/dist/internal/default-filter.js +11 -1
  14. package/dist/internal/default-filter.js.map +1 -1
  15. package/dist/internal/deferred-joined-stream.js +40 -27
  16. package/dist/internal/deferred-joined-stream.js.map +1 -1
  17. package/dist/internal/extractors.d.ts +25 -0
  18. package/dist/internal/extractors.js +31 -0
  19. package/dist/internal/extractors.js.map +1 -0
  20. package/dist/internal/field-types.d.ts +22 -6
  21. package/dist/internal/filters.d.ts +3 -1
  22. package/dist/internal/filters.js +9 -2
  23. package/dist/internal/filters.js.map +1 -1
  24. package/dist/internal/foreign.js +1 -1
  25. package/dist/internal/foreign.js.map +1 -1
  26. package/dist/internal/guards.d.ts +20 -1
  27. package/dist/internal/guards.js +42 -0
  28. package/dist/internal/guards.js.map +1 -1
  29. package/dist/internal/joined.js +1 -1
  30. package/dist/internal/joined.js.map +1 -1
  31. package/dist/internal/list.d.ts +2 -2
  32. package/dist/internal/list.js +10 -6
  33. package/dist/internal/list.js.map +1 -1
  34. package/dist/internal/meta-records.d.ts +2 -0
  35. package/dist/internal/meta-records.js +10 -5
  36. package/dist/internal/meta-records.js.map +1 -1
  37. package/dist/internal/properties.d.ts +2 -2
  38. package/dist/internal/properties.js +6 -4
  39. package/dist/internal/properties.js.map +1 -1
  40. package/dist/internal/prototypes.d.ts +14 -0
  41. package/dist/internal/prototypes.js +38 -0
  42. package/dist/internal/prototypes.js.map +1 -0
  43. package/dist/internal/read-properties.d.ts +3 -0
  44. package/dist/internal/read-properties.js +29 -0
  45. package/dist/internal/read-properties.js.map +1 -0
  46. package/dist/internal/tables.d.ts +12 -2
  47. package/dist/internal/tables.js +11 -0
  48. package/dist/internal/tables.js.map +1 -1
  49. package/dist/internal/validation.d.ts +4 -3
  50. package/dist/internal/validation.js +18 -6
  51. package/dist/internal/validation.js.map +1 -1
  52. package/dist/metadata.d.ts +13 -13
  53. package/dist/metadata.js +29 -31
  54. package/dist/metadata.js.map +1 -1
  55. package/dist/tests/components/access_control.test.js +11 -3
  56. package/dist/tests/components/access_control.test.js.map +1 -1
  57. package/dist/tests/components/computed.test.js +14 -3
  58. package/dist/tests/components/computed.test.js.map +1 -1
  59. package/dist/tests/components/cross_instance.test.js +2 -2
  60. package/dist/tests/components/cross_instance.test.js.map +1 -1
  61. package/dist/tests/components/deferred_joined.test.js +1 -1
  62. package/dist/tests/components/deferred_joined.test.js.map +1 -1
  63. package/dist/tests/components/extract_generic.test.d.ts +1 -0
  64. package/dist/tests/components/extract_generic.test.js +64 -0
  65. package/dist/tests/components/extract_generic.test.js.map +1 -0
  66. package/dist/tests/components/filter_comparisons.test.d.ts +1 -0
  67. package/dist/tests/components/filter_comparisons.test.js +34 -0
  68. package/dist/tests/components/filter_comparisons.test.js.map +1 -0
  69. package/dist/tests/components/filter_list_route.test.d.ts +1 -0
  70. package/dist/tests/components/filter_list_route.test.js +182 -0
  71. package/dist/tests/components/filter_list_route.test.js.map +1 -0
  72. package/dist/tests/components/filter_options.test.js +3 -3
  73. package/dist/tests/components/filter_options.test.js.map +1 -1
  74. package/dist/tests/components/foreign_computed.test.js +15 -4
  75. package/dist/tests/components/foreign_computed.test.js.map +1 -1
  76. package/dist/tests/components/foreign_joined.test.js +16 -4
  77. package/dist/tests/components/foreign_joined.test.js.map +1 -1
  78. package/dist/tests/components/joined.test.js +26 -8
  79. package/dist/tests/components/joined.test.js.map +1 -1
  80. package/dist/tests/components/listable.test.js +31 -12
  81. package/dist/tests/components/listable.test.js.map +1 -1
  82. package/dist/tests/components/localized_sort.test.js +5 -1
  83. package/dist/tests/components/localized_sort.test.js.map +1 -1
  84. package/dist/tests/components/mandatory.test.js +1 -1
  85. package/dist/tests/components/mandatory.test.js.map +1 -1
  86. package/dist/tests/components/modifier_key.test.js +12 -4
  87. package/dist/tests/components/modifier_key.test.js.map +1 -1
  88. package/dist/tests/components/parse_body.test.js +17 -16
  89. package/dist/tests/components/parse_body.test.js.map +1 -1
  90. package/dist/tests/components/parsed_value.test.js +2 -2
  91. package/dist/tests/components/parsed_value.test.js.map +1 -1
  92. package/dist/tests/components/partial_edit.test.js +8 -3
  93. package/dist/tests/components/partial_edit.test.js.map +1 -1
  94. package/dist/tests/components/per_action_access.test.js +11 -3
  95. package/dist/tests/components/per_action_access.test.js.map +1 -1
  96. package/dist/tests/components/prototype_named_filter.test.d.ts +1 -0
  97. package/dist/tests/components/prototype_named_filter.test.js +87 -0
  98. package/dist/tests/components/prototype_named_filter.test.js.map +1 -0
  99. package/dist/tests/components/validator.test.js +1 -1
  100. package/dist/tests/components/validator.test.js.map +1 -1
  101. package/dist/tests/components/written_row.test.d.ts +1 -0
  102. package/dist/tests/components/written_row.test.js +99 -0
  103. package/dist/tests/components/written_row.test.js.map +1 -0
  104. package/dist/tests/helpers/other-instance.js +8 -4
  105. package/dist/tests/helpers/other-instance.js.map +1 -1
  106. package/dist/tests/index/field_decorator_target.test.d.ts +1 -0
  107. package/dist/tests/index/field_decorator_target.test.js +20 -0
  108. package/dist/tests/index/field_decorator_target.test.js.map +1 -0
  109. package/dist/tests/index/inheritance.test.js +6 -2
  110. package/dist/tests/index/inheritance.test.js.map +1 -1
  111. package/dist/tests/index/metadata_names.test.d.ts +1 -0
  112. package/dist/tests/index/metadata_names.test.js +91 -0
  113. package/dist/tests/index/metadata_names.test.js.map +1 -0
  114. package/dist/tests/index/routes.test.js +14 -5
  115. package/dist/tests/index/routes.test.js.map +1 -1
  116. package/dist/tests/integration.test.js +24 -11
  117. package/dist/tests/integration.test.js.map +1 -1
  118. package/dist/tests/internal/controller-fields.test.d.ts +1 -0
  119. package/dist/tests/internal/controller-fields.test.js +25 -0
  120. package/dist/tests/internal/controller-fields.test.js.map +1 -0
  121. package/dist/tests/internal/http.d.ts +21 -9
  122. package/dist/tests/internal/http.js +24 -3
  123. package/dist/tests/internal/http.js.map +1 -1
  124. package/dist/tests/internal/shop-schema.d.ts +28 -20
  125. package/dist/tests/utils.d.ts +5 -7
  126. package/dist/tests/utils.js.map +1 -1
  127. package/docs/1.introduction.md +38 -35
  128. package/docs/10.joined.md +51 -40
  129. package/docs/11.computed.md +91 -47
  130. package/docs/2.data-controllers.md +50 -42
  131. package/docs/3.routes.md +150 -133
  132. package/docs/4.access-rights.md +48 -44
  133. package/docs/5.validators.md +54 -45
  134. package/docs/6.listable.md +53 -49
  135. package/docs/7.foreign-keys.md +55 -48
  136. package/docs/8.filters.md +64 -57
  137. package/docs/9.modifiers.md +29 -25
  138. package/package.json +17 -14
  139. package/skills/data-api-interface/SKILL.md +9 -3
@@ -1,19 +1,45 @@
1
- # Computed Fields
1
+ ---
2
+ title: Computed fields
3
+ description: Compute a field in the database with @Computed, from the row or from a per-row subquery, and sort, filter, and page on it.
4
+ mode: reference
5
+ ---
2
6
 
3
7
  ## Overview
4
8
 
5
- The `@Computed` decorator declares a field whose value is computed in the database from an expression, then merged onto each row at query time. The expression can be a row-local computation (derived from other fields of the same row) or a per-row subquery, such as an aggregate over another table. Because the merge happens before sorting and filtering, computed fields are natively sortable, filterable, and pageable in DB — no fetch-all or in-memory sorting required.
9
+ The `@Computed` decorator declares a field whose value the database computes from an expression and merges onto each row at query time. The expression can compute from other fields of the same row, or run a per-row subquery such as an aggregate over another table. The merge happens before the list query sorts and filters, so the database sorts, filters, and pages computed fields. The API never fetches every row or sorts in memory.
6
10
 
7
- Computed fields are read-only: the value is produced by the query and is never persisted on the controller's table. Declaring a field as computed automatically sets its access mode to read-only and forces non-indexed sorting, regardless of decorator order.
11
+ Computed fields are meant to be read-only. The query produces the value, and the controller's table never stores it. `@Computed` sets the field's access mode to read-only when it runs. An `@Access` written above it runs later and replaces that mode, so do not combine the two. `@Computed` also makes the field's sort non-indexed, whatever the decorator order.
8
12
 
9
- ## Basic Usage
13
+ ## Basic usage
10
14
 
11
- The expression receives the row proxy and the schema instance of the controller's table. It must return a value built from the query builder (`ValueProxy` operations or a subquery), not plain JavaScript computed at request time.
15
+ The expression receives the row proxy and the schema instance of the controller's table. It must return a value built with the query builder (`ValueProxy` operations or a subquery), not plain JavaScript computed at request time.
12
16
 
13
- The snippets extend the [quick-start controller pattern](./1.introduction.md#quick-start); define `GroupModel` with `BasicDataModel(Group)` and register the `myapp` schema before serving requests. Check [driver support](#driver-support) before using aggregate subqueries.
17
+ The example follows the [quick-start controller pattern](./1.introduction.md#quick-start). Register the `myapp` schema before the module serves requests. Check [driver support](#driver-support) before you use aggregate subqueries.
14
18
 
15
19
  ```typescript
16
- import { Computed } from "@antelopejs/interface-data-api/metadata";
20
+ import { Controller } from "@antelopejs/interface-api";
21
+ import {
22
+ DataController,
23
+ DefaultRoutes,
24
+ RegisterDataController,
25
+ } from "@antelopejs/interface-data-api";
26
+ import {
27
+ Access,
28
+ AccessMode,
29
+ Computed,
30
+ Listable,
31
+ ModelReference,
32
+ Sortable,
33
+ } from "@antelopejs/interface-data-api/metadata";
34
+ import {
35
+ BasicDataModel,
36
+ Field,
37
+ Index,
38
+ Model,
39
+ RegisterTable,
40
+ Relation,
41
+ Table,
42
+ } from "@antelopejs/interface-database-decorators";
17
43
 
18
44
  @RegisterTable("groups", "myapp")
19
45
  class Group extends Table {
@@ -35,6 +61,8 @@ class Device extends Table {
35
61
  declare group_id: string;
36
62
  }
37
63
 
64
+ class GroupModel extends BasicDataModel(Group, "groups") {}
65
+
38
66
  @RegisterDataController()
39
67
  class GroupAPI extends DataController(
40
68
  Group,
@@ -56,7 +84,11 @@ class GroupAPI extends DataController(
56
84
  // Aggregate: number of devices referencing this group, computed in DB
57
85
  @Listable()
58
86
  @Computed(
59
- (row, db) => db.table("devices").getAll(row.key("_id"), "group_id").count(),
87
+ (row, db) =>
88
+ db
89
+ .table("devices")
90
+ .filter((device) => device.key("group_id").eq(row.key("_id")))
91
+ .count(),
60
92
  { default: 0 },
61
93
  )
62
94
  @Sortable()
@@ -64,11 +96,13 @@ class GroupAPI extends DataController(
64
96
 
65
97
  // Row-local expression
66
98
  @Listable()
67
- @Computed((row) => (row.key("name") as ValueProxy<string>).concat("!"))
99
+ @Computed((row) => row.key("name").concat("!"))
68
100
  declare display_name: string;
69
101
  }
70
102
  ```
71
103
 
104
+ The row proxy types every field as a `FieldValue`. The subquery compares each device's `group_id` with the row's `_id` through `eq`, which accepts that type. This filter checks every device for each row and does not use the `group_id` index. `getAll(row.key("_id"), "group_id")` would use the index, but `getAll` does not accept a row key typed as a `FieldValue`, so it compiles only with a cast.
105
+
72
106
  A GET request to `/groups/get?id=group-123` returns the computed values at the top level:
73
107
 
74
108
  ```json
@@ -80,69 +114,79 @@ A GET request to `/groups/get?id=group-123` returns the computed values at the t
80
114
  }
81
115
  ```
82
116
 
83
- ## Decorator Parameters
117
+ ## Decorator parameters
84
118
 
85
- ```typescript
119
+ ```typescript fragment
86
120
  @Computed(expr, options?)
87
121
  ```
88
122
 
89
- | Parameter | Type | Required | Description |
90
- | ----------------- | ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
91
- | `expr` | `(row, db) => expression` | Yes | Expression receiving the row proxy and the schema instance; returns a `ValueProxy` expression or a subquery (`Datum`) |
92
- | `options.default` | `unknown` | No | Fallback merged over the computed value when the expression yields `null` |
123
+ | Parameter | Type | Required | Description |
124
+ | ----------------- | ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
125
+ | `expr` | `(row, db) => expression` | Yes | Expression receiving the row proxy and the schema instance. Returns a `ValueProxy` expression, a subquery (`Query`), or a constant |
126
+ | `options.default` | `FieldValue` | No | Fallback merged over the computed value when the expression yields `null` |
93
127
 
94
128
  ### The `default` option
95
129
 
96
- Aggregate subqueries yield `null` instead of their natural zero value when the underlying set is empty (a `count()` over a group with no devices returns `null`, not `0`). Calling `.default(0)` inside the subquery expression does not help: the aggregation produces no document at all, so there is nothing to apply the default to. The `options.default` value solves this by being applied at the row level, after the merge:
97
-
98
- ```typescript
99
- @Computed((row, db) => db.table("devices").getAll(row.key("_id"), "group_id").count(), {
100
- default: 0,
101
- })
130
+ An aggregate subquery over an empty set yields `null` instead of its zero value. A `count()` over a group with no devices returns `null`, not `0`. Calling `.default(0)` inside the subquery expression does not help, because the aggregation produces no document to apply the default to. The `options.default` value works because the Data API applies it to the row, after the merge:
131
+
132
+ ```typescript fragment
133
+ @Computed(
134
+ (row, db) =>
135
+ db
136
+ .table("devices")
137
+ .filter((device) => device.key("group_id").eq(row.key("_id")))
138
+ .count(),
139
+ { default: 0 },
140
+ )
102
141
  declare devices_count: number;
103
142
  ```
104
143
 
105
- Without it, rows with an empty aggregate carry `null`, which breaks equality filters (`filter_devices_count=eq:0`) and makes sort ordering depend on BSON type ordering.
144
+ Without it, a row with an empty aggregate holds `null`. An equality filter such as `filter_devices_count=eq:0` then misses the row, and the sort order depends on BSON type ordering.
106
145
 
107
- ## Sorting, Filtering, and Pagination
146
+ ## Sorting, filtering, and pagination
108
147
 
109
- Computed fields combine with `@Sortable` and `@Filter` like native columns. The framework injects the computation lazily, only when and where the value is needed:
148
+ Computed fields combine with `@Sortable` and `@Filter` like stored columns. The list query merges each computed field only where a step needs its value:
110
149
 
111
- - A computed field referenced by an active filter is merged before filters run.
112
- - A computed field used as the sort key is merged after the total count is captured and before the non-indexed `orderBy`, so the aggregate is not paid twice.
113
- - A computed field requested only for display (in the pluck, neither sorted nor filtered) is merged after pagination (`slice`), so it is computed only for the returned page, not the whole collection.
150
+ - A computed field that an active filter reads is merged before the filters run.
151
+ - A computed field used as the sort key is merged after the total count and before the non-indexed `orderBy`, so the count does not compute the aggregate.
152
+ - A computed field requested only for display, in the pluck and neither sorted nor filtered, is merged after pagination (`slice`). The database computes it for the returned page only.
114
153
 
115
- Sorting and pagination both stay in the database: sorting by a computed aggregate with `limit`/`offset` produces correct pages and a constant `total`.
154
+ Sorting and pagination both stay in the database. Sorting by a computed aggregate with `limit` and `offset` gives correct pages and the same `total` on every page.
116
155
 
117
- Computed fields are always sorted without a database index (`@Sortable` is forced to non-indexed), since the value does not exist as a stored column.
156
+ A computed field always sorts without a database index, because its value is not a stored column. `@Sortable` on a computed field is always non-indexed.
118
157
 
119
158
  ### Filtering on numeric computed fields
120
159
 
121
- Filter values arrive as strings from the query string. Use the validated `numberFilter` from [Numeric and Boolean Filters](./8.filters.md#numeric-and-boolean-filters), imported into the controller file. It rejects invalid numbers and honors the requested comparison operator; `parseFloat` alone accepts partial input such as `"3devices"`.
122
-
123
- ```typescript
124
- @Computed((row, db) => db.table("devices").getAll(row.key("_id"), "group_id").count(), {
125
- default: 0,
126
- })
160
+ Filter values arrive as strings from the query string. Import the validated `numberFilter` from [Numeric and boolean filters](./8.filters.md#numeric-and-boolean-filters) into the controller file. It rejects invalid numbers and applies the requested comparison operator. `parseFloat` alone accepts partial input such as `"3devices"`.
161
+
162
+ ```typescript fragment
163
+ @Computed(
164
+ (row, db) =>
165
+ db
166
+ .table("devices")
167
+ .filter((device) => device.key("group_id").eq(row.key("_id")))
168
+ .count(),
169
+ { default: 0 },
170
+ )
127
171
  @Filter(numberFilter, false)
128
172
  declare devices_count: number;
129
173
  ```
130
174
 
131
- ## Computed vs. Joined
175
+ ## `@Computed` compared with `@Joined`
132
176
 
133
- | Aspect | `@Computed` | `@Joined` |
134
- | ------------- | --------------------------------------------------------------- | ------------------------------------------------------------ |
135
- | Source | Arbitrary expression: row-local computation or per-row subquery | One scalar field from another table matched by a foreign key |
136
- | Typical use | Aggregates (`count`, `sum`), derived values | Flattening a referenced record's field |
137
- | Access | Read-only (enforced) | Read-only (enforced) |
138
- | Sort / filter | Native, lazily injected only where needed | Native, always applied before sort/filter |
177
+ | Aspect | `@Computed` | `@Joined` |
178
+ | --------------- | ---------------------------------------------------------------- | ------------------------------------------------------------ |
179
+ | Source | Any expression: a computation over the row or a per-row subquery | One scalar field from another table matched by a foreign key |
180
+ | Typical use | Aggregates (`count`, `sum`), derived values | Flattening a field of a referenced record |
181
+ | Access | Read-only, unless a later `@Access` replaces it | Read-only, unless a later `@Access` replaces it |
182
+ | Sort and filter | In the database, merged only where a step needs the value | In the database, join applied before sort and filter |
139
183
 
140
- `@Joined` remains the simpler choice for the 1:1 flattening case; `@Computed` generalizes the same merge mechanism to arbitrary expressions.
184
+ For one field copied from one referenced record, `@Joined` is simpler. `@Computed` applies the same merge to any expression.
141
185
 
142
- ## Driver Support
186
+ ## Driver support
143
187
 
144
- The aggregate form relies on the database driver translating a per-row subquery inside a merge into a correlated lookup (`$lookup` with `let`/`pipeline` on MongoDB). This is supported by `@antelopejs/mongodb` >= 1.2.1. Row-local expressions only use standard expression compilation and work on any driver.
188
+ The aggregate form needs a database driver that turns a per-row subquery inside a merge into a correlated lookup. On MongoDB, the lookup is a `$lookup` with `let` and `pipeline`. The `@antelopejs/mongodb` changelog names the release that added this translation. Row-local expressions use standard expression compilation and work on any driver.
145
189
 
146
- ## Next Steps
190
+ ## Next steps
147
191
 
148
- See the [joined fields documentation](./10.joined.md) for the dedicated 1:1 flattening decorator.
192
+ For the decorator that copies one field from a referenced record, see [Joined fields](./10.joined.md).
@@ -1,14 +1,18 @@
1
- # Data Controllers
1
+ ---
2
+ title: Data controllers
3
+ description: The DataController function, the RegisterDataController decorator, and the parts every data controller declares.
4
+ mode: reference
5
+ ---
2
6
 
3
7
  ## Overview
4
8
 
5
- A data controller is the central building block of the Data API. It connects a database table to a set of API endpoints, exposing the table fields you declare on the class. The `DataController` function creates a base class that combines your table definition, route definitions, and controller path into a single cohesive unit.
9
+ A data controller connects a database table to a set of API endpoints and exposes the table fields you declare on the class. The `DataController` function returns a base class built from three parts: the table class, the route definitions and the controller base that holds the endpoint path.
6
10
 
7
- ## Create a Data Controller
11
+ ## Create a data controller
8
12
 
9
- Use the `DataController` function to define a new controller. It accepts three arguments: the table class, a route definition object, and a controller base with the endpoint path.
13
+ `DataController` takes three arguments: the table class, a route definition object, and a controller base with the endpoint path.
10
14
 
11
- This example uses the `myapp` schema and the `my-database` instance. Import the table declaration and await `RegisterSchema("myapp")` in the declaring module before serving requests; provision the named instance through your database setup.
15
+ The example uses the `myapp` schema and the `my-database` instance. Import the table declaration and await `RegisterSchema("myapp")` in the declaring module before it serves requests. Your database setup provisions the named instance.
12
16
 
13
17
  ```ts docs-check
14
18
  import { Controller } from "@antelopejs/interface-api";
@@ -31,7 +35,6 @@ import {
31
35
  Table,
32
36
  } from "@antelopejs/interface-database-decorators";
33
37
 
34
- // Define the database table
35
38
  @RegisterTable("users", "myapp")
36
39
  class User extends Table {
37
40
  @Field("string")
@@ -44,10 +47,8 @@ class User extends Table {
44
47
  declare age: number;
45
48
  }
46
49
 
47
- // Create the database model
48
50
  class UserModel extends BasicDataModel(User, "users") {}
49
51
 
50
- // Create the data controller
51
52
  @RegisterDataController()
52
53
  class UserAPI extends DataController(
53
54
  User,
@@ -71,50 +72,57 @@ class UserAPI extends DataController(
71
72
  }
72
73
  ```
73
74
 
74
- ## `DataController` Signature
75
-
76
- ```typescript
77
- DataController(
78
- tableClass: typeof Table, // Database table class
79
- routes: DataControllerDef, // Route definitions (e.g., DefaultRoutes.All)
80
- base: typeof Controller, // Controller base with path
81
- )
75
+ ## `DataController` signature
76
+
77
+ ```typescript fragment
78
+ function DataController<
79
+ C extends Class,
80
+ P extends DataControllerDef = DataControllerDef,
81
+ Base extends ControllerClass = ControllerClass,
82
+ >(
83
+ tableClass: C,
84
+ def: P,
85
+ base: Base,
86
+ ): Class<ExtractDefCallbacks<P> & { table: InstanceType<C> }> & Base;
82
87
  ```
83
88
 
84
- | Parameter | Description |
85
- | ------------ | --------------------------------------------------------------------------------------------------------------------------------- |
86
- | `tableClass` | The database table class decorated with `@RegisterTable`. The controller validates that this table exists in the database schema. |
87
- | `routes` | An object mapping endpoint names to route callbacks. Use `DefaultRoutes.All` for standard CRUD or pass a custom subset. |
88
- | `base` | The controller base, typically created with `Controller("/path")`, which sets the URL prefix for all endpoints. |
89
+ | Parameter | Description |
90
+ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `tableClass` | The table class decorated with `@RegisterTable`. `DataController` throws `Non-existent Database Schema` when the table's schema is not registered, and `Unregistered Database Table` when the schema does not hold the table. |
92
+ | `def` | An object that maps endpoint names to route callbacks. Use `DefaultRoutes.All` for the five CRUD routes, or pass a subset or custom routes. |
93
+ | `base` | The controller base, usually `Controller("/path")` from `@antelopejs/interface-api`. Its location is the URL prefix of every endpoint. |
94
+
95
+ The returned class extends `base`. Its prototype holds one method per key of `def`, and its instances declare a `table` field typed by the table class.
89
96
 
90
- ## Required Components
97
+ ## Required components
91
98
 
92
- Every data controller requires the following elements:
99
+ Every data controller declares these four parts.
93
100
 
94
- 1. **Table Class** - A database table decorated with `@RegisterTable`, defining the schema.
95
- 2. **Route Definitions** - Which operations to expose. Use `DefaultRoutes.All` or a custom selection.
96
- 3. **Controller Base** - The URL path prefix, created with `Controller("/path")`.
97
- 4. **Model Reference** - A property decorated with `@ModelReference()` that holds a database model instance. This model provides the actual database operations (insert, get, update, delete). A controller derived from another data controller (for example `Controller("/path", ParentController)`) inherits its model reference unless it declares its own.
101
+ 1. **Table class.** A database table decorated with `@RegisterTable`, which defines the schema.
102
+ 2. **Route definitions.** The operations to expose. Use `DefaultRoutes.All` or a custom selection.
103
+ 3. **Controller base.** The URL path prefix, created with `Controller("/path")`.
104
+ 4. **Model reference.** A property decorated with `@ModelReference()` that holds a database model instance. The model runs the database operations (insert, get, update and delete). A controller derived from another data controller (for example `Controller("/path", ParentController)`) inherits its model reference unless it declares its own.
98
105
 
99
- ## The `@RegisterDataController` Decorator
106
+ ## The `@RegisterDataController` decorator
100
107
 
101
- Apply `@RegisterDataController()` to the class to finalize the controller setup. This decorator processes all field decorators, registers the defined routes, and connects parameter providers to each endpoint.
108
+ Apply `@RegisterDataController()` to the class to finish the controller setup. For each route of the definition, the decorator applies the route's parameter decorators, registers a prefix route that records the route entry on the request context, and registers the route method as the handler at `<controller location>/<endpoint>`.
102
109
 
103
- ## Field Declarations
110
+ ## Field declarations
104
111
 
105
- Fields declared on the data controller class determine which database columns are exposed through the API. Each field can be annotated with decorators to control its behavior:
112
+ The fields you declare on the data controller class decide which table fields the API exposes. Each field takes decorators that set its behaviour:
106
113
 
107
- - `@Access` - Set read/write permissions.
108
- - `@Listable` - Include the field in list responses.
109
- - `@Mandatory` - Require the field on specific operations.
110
- - `@Validator` - Attach a validation function.
111
- - `@Foreign` - Establish a foreign key relationship.
112
- - `@Joined` - Flatten a scalar field from another table onto the row.
113
- - `@Filter` - Enable filtering on the field.
114
- - `@Sortable` - Allow sorting by the field.
114
+ - `@Access` sets read and write permissions.
115
+ - `@Listable` includes the field in list responses.
116
+ - `@Mandatory` requires the field on specific operations.
117
+ - `@Validator` attaches a validation function.
118
+ - `@Foreign` declares a foreign key relationship.
119
+ - `@Joined` flattens a scalar field from another table onto the row.
120
+ - `@Computed` adds a value the database computes for each row.
121
+ - `@Filter` enables filtering on the field.
122
+ - `@Sortable` allows sorting by the field.
115
123
 
116
- Fields without any decorator are ignored by the Data API and do not appear in API responses or accept input.
124
+ The Data API ignores a field without an access mode. Such a field never appears in responses and never accepts input. `@Access` gives a field its mode, and `@Joined` and `@Computed` make their field read-only.
117
125
 
118
- ## Next Steps
126
+ ## Next steps
119
127
 
120
- See the [routes documentation](./3.routes.md) to learn about available routes and how to customize them.
128
+ The [routes page](./3.routes.md) lists the available routes and how to customize them.