@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.
- package/README.md +26 -22
- package/dist/antelope.test.js +1 -5
- package/dist/antelope.test.js.map +1 -1
- package/dist/components.d.ts +67 -39
- package/dist/components.js +106 -685
- package/dist/components.js.map +1 -1
- package/dist/index.d.ts +57 -68
- package/dist/index.js +114 -100
- package/dist/index.js.map +1 -1
- package/dist/internal/computed.d.ts +6 -0
- package/dist/internal/computed.js +51 -0
- package/dist/internal/computed.js.map +1 -0
- package/dist/internal/controller-types.d.ts +34 -0
- package/dist/internal/controller-types.js +4 -0
- package/dist/internal/controller-types.js.map +1 -0
- package/dist/internal/default-filter.d.ts +4 -0
- package/dist/internal/default-filter.js +29 -0
- package/dist/internal/default-filter.js.map +1 -0
- package/dist/internal/deferred-joined-stream.d.ts +11 -0
- package/dist/internal/deferred-joined-stream.js +125 -0
- package/dist/internal/deferred-joined-stream.js.map +1 -0
- package/dist/internal/extractors.d.ts +25 -0
- package/dist/internal/extractors.js +31 -0
- package/dist/internal/extractors.js.map +1 -0
- package/dist/internal/field-types.d.ts +199 -0
- package/dist/internal/field-types.js +4 -0
- package/dist/internal/field-types.js.map +1 -0
- package/dist/internal/filters.d.ts +6 -0
- package/dist/internal/filters.js +22 -0
- package/dist/internal/filters.js.map +1 -0
- package/dist/internal/foreign.d.ts +6 -0
- package/dist/internal/foreign.js +116 -0
- package/dist/internal/foreign.js.map +1 -0
- package/dist/internal/guards.d.ts +39 -0
- package/dist/internal/guards.js +116 -0
- package/dist/internal/guards.js.map +1 -0
- package/dist/internal/joined.d.ts +28 -0
- package/dist/internal/joined.js +127 -0
- package/dist/internal/joined.js.map +1 -0
- package/dist/internal/list.d.ts +16 -0
- package/dist/internal/list.js +182 -0
- package/dist/internal/list.js.map +1 -0
- package/dist/internal/meta-records.d.ts +19 -0
- package/dist/internal/meta-records.js +78 -0
- package/dist/internal/meta-records.js.map +1 -0
- package/dist/internal/properties.d.ts +4 -0
- package/dist/internal/properties.js +45 -0
- package/dist/internal/properties.js.map +1 -0
- package/dist/internal/prototypes.d.ts +14 -0
- package/dist/internal/prototypes.js +38 -0
- package/dist/internal/prototypes.js.map +1 -0
- package/dist/internal/read-properties.d.ts +3 -0
- package/dist/internal/read-properties.js +29 -0
- package/dist/internal/read-properties.js.map +1 -0
- package/dist/internal/table-reference.d.ts +11 -0
- package/dist/internal/table-reference.js +46 -0
- package/dist/internal/table-reference.js.map +1 -0
- package/dist/internal/tables.d.ts +28 -0
- package/dist/internal/tables.js +48 -0
- package/dist/internal/tables.js.map +1 -0
- package/dist/internal/validation.d.ts +12 -0
- package/dist/internal/validation.js +100 -0
- package/dist/internal/validation.js.map +1 -0
- package/dist/metadata.d.ts +27 -228
- package/dist/metadata.js +84 -213
- package/dist/metadata.js.map +1 -1
- package/dist/tests/components/access_control.test.js +28 -20
- package/dist/tests/components/access_control.test.js.map +1 -1
- package/dist/tests/components/computed.test.js +36 -27
- package/dist/tests/components/computed.test.js.map +1 -1
- package/dist/tests/components/cross_instance.test.js +8 -6
- package/dist/tests/components/cross_instance.test.js.map +1 -1
- package/dist/tests/components/deferred_joined.test.js +57 -54
- package/dist/tests/components/deferred_joined.test.js.map +1 -1
- package/dist/tests/components/extract_generic.test.d.ts +1 -0
- package/dist/tests/components/extract_generic.test.js +64 -0
- package/dist/tests/components/extract_generic.test.js.map +1 -0
- package/dist/tests/components/filter_comparisons.test.d.ts +1 -0
- package/dist/tests/components/filter_comparisons.test.js +34 -0
- package/dist/tests/components/filter_comparisons.test.js.map +1 -0
- package/dist/tests/components/filter_list_route.test.d.ts +1 -0
- package/dist/tests/components/filter_list_route.test.js +182 -0
- package/dist/tests/components/filter_list_route.test.js.map +1 -0
- package/dist/tests/components/filter_options.test.js +11 -10
- package/dist/tests/components/filter_options.test.js.map +1 -1
- package/dist/tests/components/foreign_computed.test.js +23 -14
- package/dist/tests/components/foreign_computed.test.js.map +1 -1
- package/dist/tests/components/foreign_joined.test.js +31 -27
- package/dist/tests/components/foreign_joined.test.js.map +1 -1
- package/dist/tests/components/joined.test.js +59 -45
- package/dist/tests/components/joined.test.js.map +1 -1
- package/dist/tests/components/listable.test.js +78 -58
- package/dist/tests/components/listable.test.js.map +1 -1
- package/dist/tests/components/localized_sort.test.js +14 -11
- package/dist/tests/components/localized_sort.test.js.map +1 -1
- package/dist/tests/components/mandatory.test.js +25 -25
- package/dist/tests/components/mandatory.test.js.map +1 -1
- package/dist/tests/components/modifier_key.test.js +27 -18
- package/dist/tests/components/modifier_key.test.js.map +1 -1
- package/dist/tests/components/parse_body.test.d.ts +1 -0
- package/dist/tests/components/parse_body.test.js +76 -0
- package/dist/tests/components/parse_body.test.js.map +1 -0
- package/dist/tests/components/parsed_value.test.js +23 -21
- package/dist/tests/components/parsed_value.test.js.map +1 -1
- package/dist/tests/components/partial_edit.test.js +38 -32
- package/dist/tests/components/partial_edit.test.js.map +1 -1
- package/dist/tests/components/per_action_access.test.js +20 -12
- package/dist/tests/components/per_action_access.test.js.map +1 -1
- package/dist/tests/components/prototype_named_filter.test.d.ts +1 -0
- package/dist/tests/components/prototype_named_filter.test.js +87 -0
- package/dist/tests/components/prototype_named_filter.test.js.map +1 -0
- package/dist/tests/components/validator.test.js +45 -37
- package/dist/tests/components/validator.test.js.map +1 -1
- package/dist/tests/components/written_row.test.d.ts +1 -0
- package/dist/tests/components/written_row.test.js +99 -0
- package/dist/tests/components/written_row.test.js.map +1 -0
- package/dist/tests/helpers/other-instance.d.ts +11 -1
- package/dist/tests/helpers/other-instance.js +35 -9
- package/dist/tests/helpers/other-instance.js.map +1 -1
- package/dist/tests/index/field_decorator_target.test.d.ts +1 -0
- package/dist/tests/index/field_decorator_target.test.js +20 -0
- package/dist/tests/index/field_decorator_target.test.js.map +1 -0
- package/dist/tests/index/get_guard.test.js +6 -6
- package/dist/tests/index/get_guard.test.js.map +1 -1
- package/dist/tests/index/inheritance.test.js +14 -9
- package/dist/tests/index/inheritance.test.js.map +1 -1
- package/dist/tests/index/metadata_names.test.d.ts +1 -0
- package/dist/tests/index/metadata_names.test.js +91 -0
- package/dist/tests/index/metadata_names.test.js.map +1 -0
- package/dist/tests/index/routes.test.js +49 -44
- package/dist/tests/index/routes.test.js.map +1 -1
- package/dist/tests/integration.test.d.ts +1 -1
- package/dist/tests/integration.test.js +66 -729
- package/dist/tests/integration.test.js.map +1 -1
- package/dist/tests/internal/controller-fields.test.d.ts +1 -0
- package/dist/tests/internal/controller-fields.test.js +25 -0
- package/dist/tests/internal/controller-fields.test.js.map +1 -0
- package/dist/tests/internal/http.d.ts +35 -0
- package/dist/tests/internal/http.js +56 -0
- package/dist/tests/internal/http.js.map +1 -0
- package/dist/tests/internal/shop-api.d.ts +1 -0
- package/dist/tests/internal/shop-api.js +331 -0
- package/dist/tests/internal/shop-api.js.map +1 -0
- package/dist/tests/internal/shop-schema.d.ts +162 -0
- package/dist/tests/internal/shop-schema.js +387 -0
- package/dist/tests/internal/shop-schema.js.map +1 -0
- package/dist/tests/utils.d.ts +8 -10
- package/dist/tests/utils.js +1 -3
- package/dist/tests/utils.js.map +1 -1
- package/dist/tests/utils.test.js +10 -11
- package/dist/tests/utils.test.js.map +1 -1
- package/docs/1.introduction.md +38 -35
- package/docs/10.joined.md +51 -40
- package/docs/11.computed.md +91 -47
- package/docs/2.data-controllers.md +50 -42
- package/docs/3.routes.md +156 -134
- package/docs/4.access-rights.md +48 -44
- package/docs/5.validators.md +65 -47
- package/docs/6.listable.md +53 -49
- package/docs/7.foreign-keys.md +55 -48
- package/docs/8.filters.md +65 -54
- package/docs/9.modifiers.md +29 -25
- package/package.json +34 -13
- package/skills/data-api-interface/SKILL.md +13 -3
- package/skills/data-api-interface/cases.json +99 -0
package/docs/4.access-rights.md
CHANGED
|
@@ -1,43 +1,47 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
|
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`
|
|
14
|
-
2. Add action overrides
|
|
15
|
-
3. Add `@Listable`
|
|
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
|
|
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
|
|
24
|
+
## Access modes
|
|
21
25
|
|
|
22
|
-
The `AccessMode` enum defines three
|
|
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` |
|
|
31
|
-
| `AccessMode.WriteOnly` | `2` |
|
|
32
|
-
| `AccessMode.ReadWrite` | `3` |
|
|
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
|
|
38
|
+
## Basic usage
|
|
35
39
|
|
|
36
|
-
### Read-
|
|
40
|
+
### Read-only fields
|
|
37
41
|
|
|
38
|
-
|
|
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
|
|
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-
|
|
68
|
+
### Write-only fields
|
|
65
69
|
|
|
66
|
-
|
|
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
|
|
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-
|
|
103
|
+
### Read-write fields
|
|
100
104
|
|
|
101
|
-
|
|
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-
|
|
115
|
+
## Per-action overrides
|
|
112
116
|
|
|
113
|
-
|
|
117
|
+
`@Access` takes an optional second argument that overrides the mode for named actions.
|
|
114
118
|
|
|
115
|
-
```typescript
|
|
116
|
-
@Access(
|
|
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"
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
146
|
+
// Writable on new, read-only on edit
|
|
143
147
|
@Access(AccessMode.ReadWrite, { edit: AccessMode.ReadOnly })
|
|
144
148
|
declare role: string;
|
|
145
149
|
|
|
146
|
-
//
|
|
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
|
-
|
|
156
|
+
With this configuration:
|
|
153
157
|
|
|
154
|
-
- `role` is readable and writable in
|
|
155
|
-
- `age` is read-only
|
|
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
|
|
161
|
+
## Fields without `@Access`
|
|
158
162
|
|
|
159
|
-
|
|
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
|
|
165
|
+
## Next steps
|
|
162
166
|
|
|
163
|
-
|
|
167
|
+
The [validators page](./5.validators.md) covers validation rules on field values.
|
package/docs/5.validators.md
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
## The `@Validator` decorator
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
### Basic usage
|
|
12
16
|
|
|
13
|
-
|
|
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
|
|
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:
|
|
53
|
+
value: FieldValue | undefined,
|
|
41
54
|
) => FieldValidationResult | Promise<FieldValidationResult>;
|
|
42
55
|
|
|
43
56
|
type FieldValidationResult =
|
|
44
|
-
|
|
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
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
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
|
|
71
|
+
### Parsed values
|
|
56
72
|
|
|
57
|
-
|
|
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
|
|
83
|
+
## Common validation patterns
|
|
68
84
|
|
|
69
|
-
### String
|
|
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
|
|
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
|
|
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
|
|
135
|
+
### Async validation
|
|
120
136
|
|
|
121
|
-
An asynchronous lookup
|
|
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`
|
|
149
|
+
## The `@Mandatory` decorator
|
|
134
150
|
|
|
135
|
-
|
|
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
|
|
138
|
-
- On any other operation
|
|
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.
|
|
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
|
|
199
|
+
### Skip mandatory validation
|
|
184
200
|
|
|
185
|
-
|
|
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`
|
|
221
|
+
## The `@Optional` decorator
|
|
206
222
|
|
|
207
|
-
|
|
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
|
|
232
|
+
## Error handling
|
|
217
233
|
|
|
218
|
-
When
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
|
243
|
+
## Next steps
|
|
226
244
|
|
|
227
|
-
|
|
245
|
+
The [listable fields page](./6.listable.md) covers which fields list responses hold and how pagination works.
|
package/docs/6.listable.md
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
|
11
|
+
The `@Sortable` decorator marks the fields that a list request can sort on.
|
|
8
12
|
|
|
9
|
-
## Basic
|
|
13
|
+
## Basic usage
|
|
10
14
|
|
|
11
|
-
|
|
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
|
|
50
|
+
## Pluck modes
|
|
47
51
|
|
|
48
|
-
|
|
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
|
|
54
|
+
### Default mode
|
|
51
55
|
|
|
52
|
-
|
|
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
|
|
63
|
+
### Custom modes
|
|
60
64
|
|
|
61
|
-
|
|
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
|
|
79
|
+
### Exclude from a mode
|
|
76
80
|
|
|
77
|
-
|
|
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
|
|
89
|
+
### Access pluck modes via routes
|
|
86
90
|
|
|
87
|
-
|
|
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
|
|
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
|
|
123
|
+
### Listable with getters
|
|
120
124
|
|
|
121
|
-
|
|
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
|
-
|
|
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
|
|
148
|
+
### Query parameters
|
|
145
149
|
|
|
146
|
-
| Parameter | Default
|
|
147
|
-
| --------- |
|
|
148
|
-
| `limit` | `10`
|
|
149
|
-
| `offset` | `0`
|
|
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
|
|
173
|
+
### Limit maximum page size
|
|
170
174
|
|
|
171
|
-
|
|
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
|
|
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
|
|
222
|
+
### Sort query parameters
|
|
219
223
|
|
|
220
|
-
| Parameter | Description
|
|
221
|
-
| --------------- |
|
|
222
|
-
| `sortKey` | The field name to sort by.
|
|
223
|
-
| `sortDirection` | `"asc"` (ascending) or `"desc"` (descending).
|
|
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`
|
|
229
|
+
### The `noIndex` option
|
|
226
230
|
|
|
227
|
-
By default, `@Sortable()` marks the field for indexed ordering. Set `noIndex: true` when
|
|
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
|
|
241
|
+
### Localized fields
|
|
238
242
|
|
|
239
|
-
A `@Localized` field
|
|
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`
|
|
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
|
|
266
|
+
## Next steps
|
|
263
267
|
|
|
264
|
-
|
|
268
|
+
To link records across tables, see [Foreign keys](./7.foreign-keys.md).
|