@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,43 +1,47 @@
1
- # Access Rights
1
+ ---
2
+ title: Access rights
3
+ description: The @Access decorator, the three AccessMode values and per-action overrides that decide which fields each route reads and writes.
4
+ mode: reference
5
+ ---
2
6
 
3
7
  ## Overview
4
8
 
5
- The `@Access` decorator controls how fields are read and written through the API. Each field can be set to read-only, write-only, or read-write, providing fine-grained control over data exposure. Access modes can also be overridden on a per-action basis, allowing different permissions for `get`, `list`, `new`, and `edit` operations.
9
+ The `@Access` decorator sets how the API reads and writes a field. A field is read-only, write-only or read-write. An override can give one action (`get`, `list`, `new` or `edit`) a different mode.
6
10
 
7
11
  ## Separate field policy from caller permissions
8
12
 
9
- `@Access` answers “may this action read or write this field?”, not “may this user access this record?”. Authenticate the caller and apply record-level policy separately. A write-only password is hidden from responses, but this decorator does not hash it.
13
+ `@Access` answers "may this action read or write this field?". It does not answer "may this user access this record?". Authenticate the caller and apply record-level policy separately. A write-only password stays out of responses, but `@Access` does not hash it.
10
14
 
11
- Build the field policy in this order:
15
+ Build the field policy in this order.
12
16
 
13
- 1. Set `@Access` for each public field. Undecorated fields remain private.
14
- 2. Add action overrides for differences between creation and editing.
15
- 3. Add `@Listable` for the fields selected by list responses; readable alone does not define a list projection.
17
+ 1. Set `@Access` on each public field. A field without it stays private.
18
+ 2. Add action overrides where creation and editing differ.
19
+ 3. Add `@Listable` to the fields list responses select. Read access alone does not put a field in a list response.
16
20
  4. Add validators and mandatory-field rules for writable inputs.
17
21
 
18
- For example, a display name can be readable and writable, an ID readable only, and a password writable only. Test each direction: attempting to write a read-only field is ignored, while omitting a mandatory writable field is a validation concern.
22
+ For example, a display name is readable and writable, an ID is read-only, and a password is write-only. Test each direction. The routes ignore a value sent for a read-only field. A missing mandatory field is a validation error, covered on the [validators page](./5.validators.md).
19
23
 
20
- ## Access Modes
24
+ ## Access modes
21
25
 
22
- The `AccessMode` enum defines three permission levels:
26
+ The `AccessMode` enum defines three modes.
23
27
 
24
28
  ```typescript
25
29
  import { Access, AccessMode } from "@antelopejs/interface-data-api/metadata";
26
30
  ```
27
31
 
28
- | Mode | Value | Description |
29
- | ---------------------- | ----- | --------------------------------------------------------------------------------------------------- |
30
- | `AccessMode.ReadOnly` | `1` | Field is readable; list responses also require selection by the pluck mode. POST and PUT ignore it. |
31
- | `AccessMode.WriteOnly` | `2` | Field is accepted in POST and PUT bodies but never appears in responses |
32
- | `AccessMode.ReadWrite` | `3` | Field is both readable and writable |
32
+ | Mode | Value | Description |
33
+ | ---------------------- | ----- | ------------------------------------------------------------------------------------------------------ |
34
+ | `AccessMode.ReadOnly` | `1` | The field is readable. A list response also needs the pluck mode to select it. POST and PUT ignore it. |
35
+ | `AccessMode.WriteOnly` | `2` | POST and PUT bodies accept the field, and no response holds it |
36
+ | `AccessMode.ReadWrite` | `3` | The field is readable and writable |
33
37
 
34
- ## Basic Usage
38
+ ## Basic usage
35
39
 
36
- ### Read-Only Fields
40
+ ### Read-only fields
37
41
 
38
- Read-only fields appear in API responses but cannot be modified through create or update requests. Values sent for these fields in request bodies are silently ignored.
42
+ A read-only field appears in responses, and create and update requests cannot change it. The routes ignore a value sent for it in a request body and report nothing.
39
43
 
40
- ```typescript
44
+ ```typescript fragment
41
45
  @RegisterDataController()
42
46
  class UserAPI extends DataController(
43
47
  User,
@@ -59,13 +63,13 @@ class UserAPI extends DataController(
59
63
  }
60
64
  ```
61
65
 
62
- A PUT request to `/users/edit?id=user-123` with `{ "createdAt": "2025-01-01", "name": "Alice" }` updates only `name`. The `createdAt` field is ignored.
66
+ A PUT request to `/users/edit?id=user-123` with `{ "createdAt": "2025-01-01", "name": "Alice" }` updates only `name`. The route ignores `createdAt`.
63
67
 
64
- ### Write-Only Fields
68
+ ### Write-only fields
65
69
 
66
- Write-only fields accept input during create and update operations but are never included in API responses. This is useful for sensitive data like passwords.
70
+ A write-only field accepts input on create and update, and no response holds it. Passwords and other secrets use this mode.
67
71
 
68
- ```typescript
72
+ ```typescript fragment
69
73
  @RegisterDataController()
70
74
  class UserAPI extends DataController(
71
75
  User,
@@ -87,7 +91,7 @@ class UserAPI extends DataController(
87
91
  }
88
92
  ```
89
93
 
90
- A GET request to `/users/get?id=user-123` returns `name` and `email` but never includes `password`:
94
+ A GET request to `/users/get?id=user-123` returns `name` and `email`, and never `password`.
91
95
 
92
96
  ```json
93
97
  {
@@ -96,11 +100,11 @@ A GET request to `/users/get?id=user-123` returns `name` and `email` but never i
96
100
  }
97
101
  ```
98
102
 
99
- ### Read-Write Fields
103
+ ### Read-write fields
100
104
 
101
- Read-write fields are fully accessible: they appear in responses and accept input in request bodies.
105
+ A read-write field appears in responses and accepts input in request bodies.
102
106
 
103
- ```typescript
107
+ ```typescript fragment
104
108
  @Access(AccessMode.ReadWrite)
105
109
  declare email: string;
106
110
 
@@ -108,21 +112,21 @@ declare email: string;
108
112
  declare name: string;
109
113
  ```
110
114
 
111
- ## Per-Action Overrides
115
+ ## Per-action overrides
112
116
 
113
- The `@Access` decorator accepts an optional second argument to override the access mode for specific actions. This allows a field to behave differently depending on the operation being performed.
117
+ `@Access` takes an optional second argument that overrides the mode for named actions.
114
118
 
115
- ```typescript
116
- @Access(defaultMode: AccessMode, overrides?: Record<string, AccessMode>)
119
+ ```typescript fragment
120
+ @Access(mode: AccessMode, overrides?: Record<string, AccessMode>)
117
121
  ```
118
122
 
119
- The `overrides` object maps action names (`"get"`, `"list"`, `"new"`, `"edit"`) to an `AccessMode` value. Actions not listed in the overrides use the default mode.
123
+ The `overrides` object maps action names (`"get"`, `"list"`, `"new"` and `"edit"`) to an `AccessMode`. An action that `overrides` does not name uses `mode`.
120
124
 
121
- To read the access a field has in one action, outside the routes (to render a form read-only on edit, for example), call `GetEffectiveMode(field, action)` from `@antelopejs/interface-data-api/metadata` with the field's metadata (`meta.fields[name]`). It is the mode the routes apply.
125
+ To read the mode a field has in one action outside the routes (to render a form read-only on edit, for example), call `GetEffectiveMode(field, action)` from `@antelopejs/interface-data-api/metadata` with the field's metadata (`meta.fields[name]`). It returns the mode the routes apply.
122
126
 
123
- ### Example: Writable on Create, Read-Only on Edit
127
+ ### Example: writable on create, read-only on edit
124
128
 
125
- ```typescript
129
+ ```typescript fragment
126
130
  @RegisterDataController()
127
131
  class UserAPI extends DataController(
128
132
  User,
@@ -139,25 +143,25 @@ class UserAPI extends DataController(
139
143
  @Access(AccessMode.ReadWrite)
140
144
  declare name: string;
141
145
 
142
- // role can be set when creating, but cannot be changed during edit
146
+ // Writable on new, read-only on edit
143
147
  @Access(AccessMode.ReadWrite, { edit: AccessMode.ReadOnly })
144
148
  declare role: string;
145
149
 
146
- // age is read-only by default, but writable during creation
150
+ // Read-only, except on new
147
151
  @Access(AccessMode.ReadOnly, { new: AccessMode.ReadWrite })
148
152
  declare age: number;
149
153
  }
150
154
  ```
151
155
 
152
- In this configuration:
156
+ With this configuration:
153
157
 
154
- - `role` is readable and writable in all operations except `edit`, where it becomes read-only.
155
- - `age` is read-only by default but can be set during `new` (create) operations.
158
+ - `role` is readable and writable in every action except `edit`, where it is read-only.
159
+ - `age` is read-only, except in `new`, where a create request can set it.
156
160
 
157
- ## Fields Without `@Access`
161
+ ## Fields without `@Access`
158
162
 
159
- Ordinary fields need `@Access` to appear in responses or accept input; `@Listable` alone does not grant access. The derived-field decorators [`@Joined`](./10.joined.md) and [`@Computed`](./11.computed.md) are exceptions: they set read-only access themselves.
163
+ An ordinary field needs `@Access` to appear in responses or accept input. `@Listable` alone grants no access. The derived-field decorators [`@Joined`](./10.joined.md) and [`@Computed`](./11.computed.md) are the exceptions. They set read-only access themselves.
160
164
 
161
- ## Next Steps
165
+ ## Next steps
162
166
 
163
- See the [validators documentation](./5.validators.md) to learn how to enforce validation rules on field values.
167
+ The [validators page](./5.validators.md) covers validation rules on field values.
@@ -1,16 +1,22 @@
1
- # Validators
1
+ ---
2
+ title: Validators
3
+ description: The @Validator, @Mandatory and @Optional decorators, the checks they run on create and update, and the 400 bodies they answer.
4
+ mode: reference
5
+ ---
2
6
 
3
7
  ## Overview
4
8
 
5
- Validators provide field-level data integrity checks that run automatically during create and update operations. The `@Validator` decorator attaches a custom validation function to a field, while `@Mandatory` ensures that specific fields are present in request bodies for given operations. The `@Optional` decorator explicitly marks a field as not mandatory.
9
+ Validators check field values on the `new` and `edit` routes. `@Validator` attaches a validation function to a field. `@Mandatory` requires a field in the request body for the named operations. `@Optional` marks a field as mandatory for no operation.
6
10
 
7
- ## The `@Validator` Decorator
11
+ The routes parse the body, then run the mandatory check, then the validators. A failed mandatory check answers `400` before any validator runs.
8
12
 
9
- ### Basic Usage
13
+ ## The `@Validator` decorator
10
14
 
11
- The `@Validator` decorator accepts a function that receives the field value and returns `true` if valid or `false` if invalid. It can instead return a parse result (see [Parsed Values](#parsed-values)) so that the parsed value is written. The function can also return a `Promise` of either for asynchronous validation.
15
+ ### Basic usage
12
16
 
13
- ```typescript
17
+ `@Validator` takes a function that receives the field value and returns `true` for a valid value or `false` for an invalid one. It can return a parse result instead (see [parsed values](#parsed-values)), and the route then writes the parsed value. For asynchronous validation, the function returns a `Promise` of either.
18
+
19
+ ```typescript fragment
14
20
  import { Validator } from "@antelopejs/interface-data-api/metadata";
15
21
 
16
22
  @RegisterDataController()
@@ -33,30 +39,40 @@ class UserAPI extends DataController(
33
39
  }
34
40
  ```
35
41
 
36
- ### Validation Function Signature
42
+ ### Validation function signature
37
43
 
38
44
  ```typescript
45
+ type FieldValue =
46
+ string | number | boolean | null | Date | FieldValue[] | FieldValues;
47
+
48
+ interface FieldValues {
49
+ [field: string]: FieldValue | undefined;
50
+ }
51
+
39
52
  type FieldValidator = (
40
- value: unknown,
53
+ value: FieldValue | undefined,
41
54
  ) => FieldValidationResult | Promise<FieldValidationResult>;
42
55
 
43
56
  type FieldValidationResult =
44
- boolean | { success: true; data: unknown } | { success: false };
57
+ | boolean
58
+ | { success: true; data: FieldValue | undefined }
59
+ | { success: false };
45
60
  ```
46
61
 
47
- The function:
62
+ The function behaves as follows.
48
63
 
49
- - Receives the raw field value from the request body.
50
- - Returns `true` when the value is valid: the raw value is written.
51
- - Returns `{ success: true, data }` when the value is valid: `data` is written instead of the raw value.
52
- - Returns `false` or `{ success: false }` when the value is invalid, which triggers a `400 Bad Request` response.
53
- - Should handle unexpected types gracefully (e.g., check `typeof` before accessing properties).
64
+ - It runs only for a field present in the request body.
65
+ - It receives the raw field value from the request body. The value is JSON, or a `Date` or `undefined` when a route that wraps `new` or `edit` amended the parsed body. Narrow `FieldValue | undefined` before you use it.
66
+ - It returns `true` for a valid value, and the route writes the raw value.
67
+ - It returns `{ success: true, data }` for a valid value, and the route writes `data` instead of the raw value.
68
+ - It returns `false` or `{ success: false }` for an invalid value, and the route answers `400 Bad Request`.
69
+ - It receives any JSON type, so check the type with `typeof` before you read a property.
54
70
 
55
- ### Parsed Values
71
+ ### Parsed values
56
72
 
57
- A validator returning a parse result writes the parsed value on both `new` and `edit`. The result has the shape of zod's `safeParse`, so a zod schema can be used directly:
73
+ When a validator returns a parse result, the route writes the parsed value on both `new` and `edit`. The result has the shape of the result of zod's `safeParse`, so a zod schema works as a validator.
58
74
 
59
- ```typescript
75
+ ```typescript fragment
60
76
  const startsAtSchema = z.coerce.date();
61
77
 
62
78
  @Access(AccessMode.ReadWrite)
@@ -64,11 +80,11 @@ const startsAtSchema = z.coerce.date();
64
80
  declare startsAt: Date; // "2026-01-15" is stored as a Date
65
81
  ```
66
82
 
67
- ## Common Validation Patterns
83
+ ## Common validation patterns
68
84
 
69
- ### String Validation
85
+ ### String validation
70
86
 
71
- ```typescript
87
+ ```typescript fragment
72
88
  // Minimum length
73
89
  @Validator((value) => typeof value === "string" && value.length >= 2)
74
90
  declare firstName: string;
@@ -84,9 +100,9 @@ const VALID_ROLES = ["admin", "editor", "viewer"];
84
100
  declare role: string;
85
101
  ```
86
102
 
87
- ### Number Validation
103
+ ### Number validation
88
104
 
89
- ```typescript
105
+ ```typescript fragment
90
106
  // Range
91
107
  @Validator((value) => typeof value === "number" && value >= 0 && value <= 120)
92
108
  declare age: number;
@@ -100,9 +116,9 @@ declare quantity: number;
100
116
  declare price: number;
101
117
  ```
102
118
 
103
- ### Date Validation
119
+ ### Date validation
104
120
 
105
- ```typescript
121
+ ```typescript fragment
106
122
  // Valid date string
107
123
  @Validator((value) => typeof value === "string" && !isNaN(Date.parse(value)))
108
124
  declare birthDate: string;
@@ -116,11 +132,11 @@ declare birthDate: string;
116
132
  declare appointmentDate: string;
117
133
  ```
118
134
 
119
- ### Async Validation
135
+ ### Async validation
120
136
 
121
- An asynchronous lookup can give early feedback, but a check followed by an insert is not a uniqueness guarantee: concurrent requests can both pass. Enforce uniqueness in the database where supported, and handle write conflicts. `lookupByEmail` below is an application-supplied function.
137
+ An asynchronous lookup gives early feedback. A check followed by an insert does not guarantee uniqueness, because two concurrent requests can both pass the check. Enforce uniqueness in the database where the database supports it, and handle write conflicts. `lookupByEmail` below is a function your application supplies.
122
138
 
123
- ```typescript
139
+ ```typescript fragment
124
140
  // Check uniqueness against the database
125
141
  @Validator(async (value) => {
126
142
  if (typeof value !== "string") return false;
@@ -130,12 +146,12 @@ An asynchronous lookup can give early feedback, but a check followed by an inser
130
146
  declare email: string;
131
147
  ```
132
148
 
133
- ## The `@Mandatory` Decorator
149
+ ## The `@Mandatory` decorator
134
150
 
135
- The `@Mandatory` decorator specifies which operations require a field in the request body. Unlike `@Validator`, which checks field content, `@Mandatory` only verifies that the field is provided:
151
+ `@Mandatory` names the operations that require a field in the request body. `@Validator` checks the content of a field. `@Mandatory` checks only that the body provides it.
136
152
 
137
- - On `"edit"`, the body only carries the fields to update: a mandatory field absent from the body keeps its stored value and is accepted, while a mandatory field cleared by the body (`null`, `""` or `[]`) is rejected.
138
- - On any other operation (such as `"new"`), a mandatory field must be present in the body.
153
+ - On `"edit"`, the body carries only the fields to update. The route accepts a mandatory field absent from the body, which keeps its stored value. It rejects a mandatory field that the body clears with `null`, `""` or `[]`.
154
+ - On any other operation, such as `"new"`, the body must hold every mandatory field.
139
155
 
140
156
  ```typescript
141
157
  import { Mandatory } from "@antelopejs/interface-data-api/metadata";
@@ -143,9 +159,9 @@ import { Mandatory } from "@antelopejs/interface-data-api/metadata";
143
159
 
144
160
  ### Usage
145
161
 
146
- Pass one or more operation names as arguments. Common operation names are `"new"` (create) and `"edit"` (update).
162
+ Pass one or more operation names as arguments. The default routes check `"new"` (create) and `"edit"` (update).
147
163
 
148
- ```typescript
164
+ ```typescript fragment
149
165
  @RegisterDataController()
150
166
  class UserAPI extends DataController(
151
167
  User,
@@ -180,11 +196,11 @@ In this example:
180
196
  - `status` cannot be cleared when editing an existing record.
181
197
  - `notes` is always optional.
182
198
 
183
- ### Skip Mandatory Validation
199
+ ### Skip mandatory validation
184
200
 
185
- Use `DefaultRoutes.WithOptions` with the `noMandatory` option to create an endpoint that skips mandatory field checks.
201
+ `DefaultRoutes.WithOptions` with the `noMandatory` option creates an endpoint that skips the mandatory field check. Any non-empty string turns the option on. The validators still run.
186
202
 
187
- ```typescript
203
+ ```typescript fragment
188
204
  const routes = {
189
205
  edit: DefaultRoutes.Edit,
190
206
  patchEdit: DefaultRoutes.WithOptions(DefaultRoutes.Edit, {
@@ -202,26 +218,28 @@ class UserAPI extends DataController(User, routes, Controller("/users")) {
202
218
 
203
219
  Requests to `/users/edit` cannot clear `status`, while requests to `/users/patchEdit` can.
204
220
 
205
- ## The `@Optional` Decorator
221
+ ## The `@Optional` decorator
206
222
 
207
- The `@Optional` decorator explicitly marks a field as not mandatory for any operation. Use it on fields that have no other decorators but still need to be recognized by the Data API metadata system.
223
+ `@Optional` marks a field as mandatory for no operation, and records the field in the Data API metadata. Use it only on a field with no other decorator, since it clears what `@Mandatory` set.
208
224
 
209
- ```typescript
225
+ ```typescript fragment
210
226
  import { Optional } from "@antelopejs/interface-data-api/metadata";
211
227
 
212
228
  @Optional()
213
229
  declare notes: string;
214
230
  ```
215
231
 
216
- ## Error Handling
232
+ ## Error handling
217
233
 
218
- When validation or mandatory checks fail, the API returns a `400 Bad Request` response whose JSON body names what went wrong and the fields concerned, for the client to word it and show it under each field:
234
+ When a mandatory check or a validator fails, the route answers `400 Bad Request`. The JSON body holds a `code` for what went wrong and the `fields` concerned, so the client can word the error and show it under each field.
219
235
 
220
- - **Mandatory failure:** `{ "code": "missing_mandatory_fields", "fields": ["email", "password"] }`
221
- - **Validator failure:** `{ "code": "invalid_field_types", "fields": ["age", "email"] }`
236
+ | Failure | Body |
237
+ | --------- | ------------------------------------------------------------------------- |
238
+ | Mandatory | `{ "code": "missing_mandatory_fields", "fields": ["email", "password"] }` |
239
+ | Validator | `{ "code": "invalid_field_types", "fields": ["age", "email"] }` |
222
240
 
223
- The codes are exported as `Validation.MISSING_MANDATORY_FIELDS` and `Validation.INVALID_FIELD_TYPES` (`@antelopejs/interface-data-api/components`), the body's type as `Validation.FieldsErrorBody`. All fields are checked: `fields` lists every one that failed.
241
+ `@antelopejs/interface-data-api/components` exports the codes as `Validation.MISSING_MANDATORY_FIELDS` and `Validation.INVALID_FIELD_TYPES`, and the body type as `Validation.FieldsErrorBody`. Each check covers every field, and `fields` lists every field that failed it.
224
242
 
225
- ## Next Steps
243
+ ## Next steps
226
244
 
227
- See the [listable fields documentation](./6.listable.md) to learn how to control field visibility in list operations and configure pagination.
245
+ The [listable fields page](./6.listable.md) covers which fields list responses hold and how pagination works.
@@ -1,16 +1,20 @@
1
- # Listable Fields
1
+ ---
2
+ title: Listable fields
3
+ description: Choose the fields a list route returns with @Listable, serve several field sets with pluck modes, page the results, and sort them with @Sortable.
4
+ mode: reference
5
+ ---
2
6
 
3
7
  ## Overview
4
8
 
5
- The `@Listable` decorator controls which fields are included in list endpoint responses. By default, list operations only return fields explicitly marked as listable, keeping payloads lightweight. The decorator supports multiple pluck modes for different levels of detail, and list responses include built-in pagination metadata.
9
+ The `@Listable` decorator selects the fields that list responses include. A list route returns only the fields marked as listable, so list payloads stay small. Pluck modes let one controller serve several field sets, and every list response carries pagination metadata.
6
10
 
7
- The `@Sortable` decorator marks fields that can be used as a sorting key in list queries.
11
+ The `@Sortable` decorator marks the fields that a list request can sort on.
8
12
 
9
- ## Basic Usage
13
+ ## Basic usage
10
14
 
11
- Fields decorated with `@Listable()` are included in list responses. Fields without this decorator are excluded from list results but still appear in single-record `get` responses (provided they have read access).
15
+ A list response includes the fields decorated with `@Listable()`. A field without the decorator is left out of list results, but a single-record `get` response still includes it when the field has read access.
12
16
 
13
- ```typescript
17
+ ```typescript fragment
14
18
  import {
15
19
  Access,
16
20
  AccessMode,
@@ -43,24 +47,24 @@ class UserAPI extends DataController(
43
47
 
44
48
  A request to `GET /users/list` returns `_id` and `email` for each record. A request to `GET /users/get?id=user-123` returns all three fields.
45
49
 
46
- ## Pluck Modes
50
+ ## Pluck modes
47
51
 
48
- Pluck modes allow a single controller to serve multiple list views with different field selections. Each `@Listable` call can target a named mode.
52
+ A pluck mode is a named field set. With pluck modes, one controller serves several list views, each with its own fields. Each `@Listable` call adds the field to one mode.
49
53
 
50
- ### Default Mode
54
+ ### Default mode
51
55
 
52
- Calling `@Listable()` without a mode name registers the field under the default `"list"` mode.
56
+ `@Listable()` without a mode name adds the field to the default `"list"` mode.
53
57
 
54
- ```typescript
58
+ ```typescript fragment
55
59
  @Listable()
56
60
  declare name: string;
57
61
  ```
58
62
 
59
- ### Custom Modes
63
+ ### Custom modes
60
64
 
61
- Pass `true` and a mode name to register a field under a specific pluck mode.
65
+ `@Listable(true, "detailed")` adds the field to the `"detailed"` mode.
62
66
 
63
- ```typescript
67
+ ```typescript fragment
64
68
  @Listable() // Included in default "list" mode
65
69
  @Listable(true, "detailed") // Also included in "detailed" mode
66
70
  declare name: string;
@@ -72,21 +76,21 @@ declare internalNotes: string;
72
76
  declare email: string;
73
77
  ```
74
78
 
75
- ### Exclude from a Mode
79
+ ### Exclude from a mode
76
80
 
77
- Pass `false` to explicitly exclude a field from a mode.
81
+ `@Listable(false, "minimal")` leaves the field out of the `"minimal"` mode.
78
82
 
79
- ```typescript
83
+ ```typescript fragment
80
84
  @Listable()
81
85
  @Listable(false, "minimal") // Excluded from "minimal" mode
82
86
  declare description: string;
83
87
  ```
84
88
 
85
- ### Access Pluck Modes via Routes
89
+ ### Access pluck modes via routes
86
90
 
87
- Define separate routes for each pluck mode using `DefaultRoutes.WithOptions` with the `pluckMode` option.
91
+ Each pluck mode needs its own route. `DefaultRoutes.WithOptions` sets the route's `pluckMode` option.
88
92
 
89
- ```typescript
93
+ ```typescript fragment
90
94
  const routes = {
91
95
  list: DefaultRoutes.List,
92
96
  detailed: DefaultRoutes.WithOptions(DefaultRoutes.List, {
@@ -114,13 +118,13 @@ class UserAPI extends DataController(User, routes, Controller("/users")) {
114
118
  - `GET /users/list` returns only `name`.
115
119
  - `GET /users/detailed` returns both `name` and `internalNotes`.
116
120
 
117
- Modes are independent field sets: `"detailed"` does not inherit `"list"`. The example includes `name` in both modes explicitly. A registered list route whose `pluckMode` has never been declared returns `400 Bad Request` unless `noPluck` is enabled; an unregistered URL instead returns `404`.
121
+ Modes are independent field sets. The `"detailed"` mode does not inherit the fields of `"list"`, so the example adds `name` to both modes. A registered list route whose `pluckMode` no field declares returns `400 Bad Request`, unless the route sets `noPluck`. A URL that matches no route returns `404`.
118
122
 
119
- ### Listable with Getters
123
+ ### Listable with getters
120
124
 
121
- When a field uses a getter (computed property), the `@Listable` decorator must specify which database fields the getter depends on. This ensures those fields are fetched from the database.
125
+ A getter (computed property) reads fields that the database must return. Its `@Listable` decorator lists those database fields, so the list query fetches them.
122
126
 
123
- ```typescript
127
+ ```typescript fragment
124
128
  @Listable(["firstName", "lastName"])
125
129
  @Access(AccessMode.ReadOnly)
126
130
  get fullName() {
@@ -130,9 +134,9 @@ get fullName() {
130
134
 
131
135
  ## Pagination
132
136
 
133
- List responses include pagination metadata. The response structure is:
137
+ Every list response carries pagination metadata. The response has this shape:
134
138
 
135
- ```typescript
139
+ ```typescript fragment
136
140
  {
137
141
  results: T[]; // Records for the current page
138
142
  total: number; // Total number of matching records
@@ -141,12 +145,12 @@ List responses include pagination metadata. The response structure is:
141
145
  }
142
146
  ```
143
147
 
144
- ### Query Parameters
148
+ ### Query parameters
145
149
 
146
- | Parameter | Default | Description |
147
- | --------- | ------- | ----------------------------------- |
148
- | `limit` | `10` | Maximum number of records to return |
149
- | `offset` | `0` | Number of records to skip |
150
+ | Parameter | Default | Description |
151
+ | --------- | ------------------------------------------------ | ----------------------------------- |
152
+ | `limit` | The route's `maxPage`, or `10` when it sets none | Maximum number of records to return |
153
+ | `offset` | `0` | Number of records to skip |
150
154
 
151
155
  ### Example
152
156
 
@@ -166,11 +170,11 @@ GET /users/list?limit=2&offset=4
166
170
  }
167
171
  ```
168
172
 
169
- ### Limit Maximum Page Size
173
+ ### Limit maximum page size
170
174
 
171
- Use the `maxPage` route option to cap the `limit` parameter. If a request specifies a `limit` larger than `maxPage`, the value is clamped to `maxPage`.
175
+ The `maxPage` route option caps the `limit` parameter. A request whose `limit` is larger than `maxPage` gets `maxPage` records at most. A request without `limit` also gets `maxPage` records. A route without `maxPage` caps `limit` at `100`.
172
176
 
173
- ```typescript
177
+ ```typescript fragment
174
178
  const routes = {
175
179
  list: DefaultRoutes.WithOptions(DefaultRoutes.List, { maxPage: 25 }),
176
180
  };
@@ -183,9 +187,9 @@ class UserAPI extends DataController(User, routes, Controller("/users")) {
183
187
 
184
188
  ## Sorting with `@Sortable`
185
189
 
186
- The `@Sortable` decorator marks a field as eligible for use as a sorting key in list operations.
190
+ The `@Sortable` decorator lets a list request sort on the field.
187
191
 
188
- ```typescript
192
+ ```typescript fragment
189
193
  import { Sortable } from "@antelopejs/interface-data-api/metadata";
190
194
 
191
195
  @RegisterDataController()
@@ -215,18 +219,18 @@ class UserAPI extends DataController(
215
219
  }
216
220
  ```
217
221
 
218
- ### Sort Query Parameters
222
+ ### Sort query parameters
219
223
 
220
- | Parameter | Description |
221
- | --------------- | ------------------------------------------------------------------ |
222
- | `sortKey` | The field name to sort by. Must be decorated with `@Sortable`. |
223
- | `sortDirection` | `"asc"` (ascending) or `"desc"` (descending). Defaults to `"asc"`. |
224
+ | Parameter | Description |
225
+ | --------------- | --------------------------------------------------------------------------------------- |
226
+ | `sortKey` | The field name to sort by. The field must have `@Sortable`. |
227
+ | `sortDirection` | `"asc"` (ascending) or `"desc"` (descending). A missing or other value sorts ascending. |
224
228
 
225
- ### The `noIndex` Option
229
+ ### The `noIndex` option
226
230
 
227
- By default, `@Sortable()` marks the field for indexed ordering. Set `noIndex: true` when it is not indexed. Both paths build an AQL `orderBy` operation; this option changes where sorting is staged, not whether rows are sorted in application memory. The provider determines how the database executes that ordering.
231
+ By default, `@Sortable()` marks the field for indexed ordering. Set `noIndex: true` when the field has no index. Both settings build a database `orderBy` operation, and neither sorts rows in application memory. The option changes where the list query places the sort. The database provider decides how it runs the ordering.
228
232
 
229
- ```typescript
233
+ ```typescript fragment
230
234
  @Sortable() // Uses database index for sorting
231
235
  declare _id: string;
232
236
 
@@ -234,9 +238,9 @@ declare _id: string;
234
238
  declare name: string;
235
239
  ```
236
240
 
237
- ### Localized Fields
241
+ ### Localized fields
238
242
 
239
- A `@Localized` field is sorted on its value in the request's locale, the one the controller's `@ModifierKey(LocalizationModifier)` field holds. A row with no value in that locale, or an empty one, sorts on its `fallbackLocale` value. This ordering is computed per row, so it never uses an index, whatever `noIndex` says.
243
+ A `@Localized` field sorts on its value in the request's locale, which the controller's `@ModifierKey(LocalizationModifier)` field holds. A row with no value in that locale, or an empty one, sorts on its `fallbackLocale` value. The list query computes this value for each row, so the sort never uses an index, whatever `noIndex` says.
240
244
 
241
245
  ### Example
242
246
 
@@ -257,8 +261,8 @@ GET /users/list?sortKey=age&sortDirection=desc&limit=3
257
261
  }
258
262
  ```
259
263
 
260
- If `sortKey` refers to a field that is not marked with `@Sortable`, the API returns a `400 Bad Request` error.
264
+ If `sortKey` names a field without `@Sortable`, the API returns `400 Bad Request` with the message `Field is not sortable.`
261
265
 
262
- ## Next Steps
266
+ ## Next steps
263
267
 
264
- See the [foreign keys documentation](./7.foreign-keys.md) to learn how to establish relationships between tables.
268
+ To link records across tables, see [Foreign keys](./7.foreign-keys.md).