@antelopejs/interface-data-api 0.1.6 → 0.1.8
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/dist/components.d.ts +20 -1
- package/dist/components.js +121 -2
- package/dist/components.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tests/components/deferred_joined.test.d.ts +1 -0
- package/dist/tests/components/deferred_joined.test.js +315 -0
- package/dist/tests/components/deferred_joined.test.js.map +1 -0
- package/docs/1.introduction.md +128 -0
- package/docs/10.joined.md +157 -0
- package/docs/11.computed.md +152 -0
- package/docs/2.data-controllers.md +120 -0
- package/docs/3.routes.md +320 -0
- package/docs/4.access-rights.md +148 -0
- package/docs/5.validators.md +204 -0
- package/docs/6.listable.md +257 -0
- package/docs/7.foreign-keys.md +285 -0
- package/docs/8.filters.md +175 -0
- package/docs/9.modifiers.md +137 -0
- package/package.json +8 -3
- package/skills/data-api-interface/SKILL.md +91 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Access Rights
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Access Modes
|
|
8
|
+
|
|
9
|
+
The `AccessMode` enum defines three permission levels:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { Access, AccessMode } from "@antelopejs/interface-data-api/metadata";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Mode | Value | Description |
|
|
16
|
+
| ---- | ----- | ----------- |
|
|
17
|
+
| `AccessMode.ReadOnly` | `1` | Field appears in GET and LIST responses but is ignored in POST and PUT bodies |
|
|
18
|
+
| `AccessMode.WriteOnly` | `2` | Field is accepted in POST and PUT bodies but never appears in responses |
|
|
19
|
+
| `AccessMode.ReadWrite` | `3` | Field is both readable and writable |
|
|
20
|
+
|
|
21
|
+
## Basic Usage
|
|
22
|
+
|
|
23
|
+
### Read-Only Fields
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
@RegisterDataController()
|
|
29
|
+
class UserAPI extends DataController(
|
|
30
|
+
User,
|
|
31
|
+
DefaultRoutes.All,
|
|
32
|
+
Controller("/users"),
|
|
33
|
+
) {
|
|
34
|
+
@ModelReference()
|
|
35
|
+
@Model(UserModel, "my-database")
|
|
36
|
+
declare userModel: UserModel;
|
|
37
|
+
|
|
38
|
+
@Access(AccessMode.ReadOnly)
|
|
39
|
+
declare _id: string;
|
|
40
|
+
|
|
41
|
+
@Access(AccessMode.ReadOnly)
|
|
42
|
+
declare createdAt: string;
|
|
43
|
+
|
|
44
|
+
@Access(AccessMode.ReadWrite)
|
|
45
|
+
declare name: string;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A PUT request to `/users/edit?id=user-123` with `{ "createdAt": "2025-01-01", "name": "Alice" }` updates only `name`. The `createdAt` field is ignored.
|
|
50
|
+
|
|
51
|
+
### Write-Only Fields
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
@RegisterDataController()
|
|
57
|
+
class UserAPI extends DataController(
|
|
58
|
+
User,
|
|
59
|
+
DefaultRoutes.All,
|
|
60
|
+
Controller("/users"),
|
|
61
|
+
) {
|
|
62
|
+
@ModelReference()
|
|
63
|
+
@Model(UserModel, "my-database")
|
|
64
|
+
declare userModel: UserModel;
|
|
65
|
+
|
|
66
|
+
@Access(AccessMode.ReadWrite)
|
|
67
|
+
declare name: string;
|
|
68
|
+
|
|
69
|
+
@Access(AccessMode.ReadWrite)
|
|
70
|
+
declare email: string;
|
|
71
|
+
|
|
72
|
+
@Access(AccessMode.WriteOnly)
|
|
73
|
+
declare password: string;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A GET request to `/users/get?id=user-123` returns `name` and `email` but never includes `password`:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"name": "Bob",
|
|
82
|
+
"email": "bob@example.com"
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Read-Write Fields
|
|
87
|
+
|
|
88
|
+
Read-write fields are fully accessible: they appear in responses and accept input in request bodies.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
@Access(AccessMode.ReadWrite)
|
|
92
|
+
declare email: string;
|
|
93
|
+
|
|
94
|
+
@Access(AccessMode.ReadWrite)
|
|
95
|
+
declare name: string;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Per-Action Overrides
|
|
99
|
+
|
|
100
|
+
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.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
@Access(defaultMode: AccessMode, overrides?: Record<string, AccessMode>)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The `overrides` object maps action names (`"get"`, `"list"`, `"new"`, `"edit"`) to an `AccessMode` value. Actions not listed in the overrides use the default mode.
|
|
107
|
+
|
|
108
|
+
### Example: Writable on Create, Read-Only on Edit
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
@RegisterDataController()
|
|
112
|
+
class UserAPI extends DataController(
|
|
113
|
+
User,
|
|
114
|
+
DefaultRoutes.All,
|
|
115
|
+
Controller("/users"),
|
|
116
|
+
) {
|
|
117
|
+
@ModelReference()
|
|
118
|
+
@Model(UserModel, "my-database")
|
|
119
|
+
declare userModel: UserModel;
|
|
120
|
+
|
|
121
|
+
@Access(AccessMode.ReadOnly)
|
|
122
|
+
declare _id: string;
|
|
123
|
+
|
|
124
|
+
@Access(AccessMode.ReadWrite)
|
|
125
|
+
declare name: string;
|
|
126
|
+
|
|
127
|
+
// role can be set when creating, but cannot be changed during edit
|
|
128
|
+
@Access(AccessMode.ReadWrite, { edit: AccessMode.ReadOnly })
|
|
129
|
+
declare role: string;
|
|
130
|
+
|
|
131
|
+
// age is read-only by default, but writable during creation
|
|
132
|
+
@Access(AccessMode.ReadOnly, { new: AccessMode.ReadWrite })
|
|
133
|
+
declare age: number;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
In this configuration:
|
|
138
|
+
|
|
139
|
+
- `role` is readable and writable in all operations except `edit`, where it becomes read-only.
|
|
140
|
+
- `age` is read-only by default but can be set during `new` (create) operations.
|
|
141
|
+
|
|
142
|
+
## Fields Without `@Access`
|
|
143
|
+
|
|
144
|
+
Fields declared without the `@Access` decorator are not exposed through the API at all. They do not appear in responses and are not accepted in request bodies. To include a field in the API, you must explicitly set its access mode.
|
|
145
|
+
|
|
146
|
+
## Next Steps
|
|
147
|
+
|
|
148
|
+
See the [validators documentation](./5.validators.md) to learn how to enforce validation rules on field values.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Validators
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
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.
|
|
6
|
+
|
|
7
|
+
## The `@Validator` Decorator
|
|
8
|
+
|
|
9
|
+
### Basic Usage
|
|
10
|
+
|
|
11
|
+
The `@Validator` decorator accepts a function that receives the field value and returns `true` if valid or `false` if invalid. The function can also return a `Promise<boolean>` for asynchronous validation.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Validator } from "@antelopejs/interface-data-api/metadata";
|
|
15
|
+
|
|
16
|
+
@RegisterDataController()
|
|
17
|
+
class UserAPI extends DataController(
|
|
18
|
+
User,
|
|
19
|
+
DefaultRoutes.All,
|
|
20
|
+
Controller("/users"),
|
|
21
|
+
) {
|
|
22
|
+
@ModelReference()
|
|
23
|
+
@Model(UserModel, "my-database")
|
|
24
|
+
declare userModel: UserModel;
|
|
25
|
+
|
|
26
|
+
@Access(AccessMode.ReadWrite)
|
|
27
|
+
@Validator((value) => typeof value === "string" && value.trim().length > 0)
|
|
28
|
+
declare name: string;
|
|
29
|
+
|
|
30
|
+
@Access(AccessMode.ReadWrite)
|
|
31
|
+
@Validator((value) => typeof value === "number" && value >= 0 && value <= 120)
|
|
32
|
+
declare age: number;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Validation Function Signature
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
type ValidatorFunction = (value: unknown) => boolean | Promise<boolean>;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The function:
|
|
43
|
+
|
|
44
|
+
- Receives the raw field value from the request body.
|
|
45
|
+
- Returns `true` when the value is valid.
|
|
46
|
+
- Returns `false` when the value is invalid, which triggers a `400 Bad Request` response.
|
|
47
|
+
- Should handle unexpected types gracefully (e.g., check `typeof` before accessing properties).
|
|
48
|
+
|
|
49
|
+
## Common Validation Patterns
|
|
50
|
+
|
|
51
|
+
### String Validation
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// Minimum length
|
|
55
|
+
@Validator((value) => typeof value === "string" && value.length >= 2)
|
|
56
|
+
declare firstName: string;
|
|
57
|
+
|
|
58
|
+
// Email format
|
|
59
|
+
const emailRegex = /^[\w](\.?[\w-]+)*@(?:[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,}$/;
|
|
60
|
+
@Validator((value) => typeof value === "string" && emailRegex.test(value))
|
|
61
|
+
declare email: string;
|
|
62
|
+
|
|
63
|
+
// Enum-like constraint
|
|
64
|
+
const VALID_ROLES = ["admin", "editor", "viewer"];
|
|
65
|
+
@Validator((value) => typeof value === "string" && VALID_ROLES.includes(value))
|
|
66
|
+
declare role: string;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Number Validation
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Range
|
|
73
|
+
@Validator((value) => typeof value === "number" && value >= 0 && value <= 120)
|
|
74
|
+
declare age: number;
|
|
75
|
+
|
|
76
|
+
// Integer only
|
|
77
|
+
@Validator((value) => typeof value === "number" && Number.isInteger(value))
|
|
78
|
+
declare quantity: number;
|
|
79
|
+
|
|
80
|
+
// Positive
|
|
81
|
+
@Validator((value) => typeof value === "number" && value > 0)
|
|
82
|
+
declare price: number;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Date Validation
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Valid date string
|
|
89
|
+
@Validator((value) => typeof value === "string" && !isNaN(Date.parse(value)))
|
|
90
|
+
declare birthDate: string;
|
|
91
|
+
|
|
92
|
+
// Future date only
|
|
93
|
+
@Validator((value) =>
|
|
94
|
+
typeof value === "string" &&
|
|
95
|
+
!isNaN(Date.parse(value)) &&
|
|
96
|
+
new Date(value) > new Date()
|
|
97
|
+
)
|
|
98
|
+
declare appointmentDate: string;
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Async Validation
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Check uniqueness against the database
|
|
105
|
+
@Validator(async (value) => {
|
|
106
|
+
if (typeof value !== "string") return false;
|
|
107
|
+
const existing = await lookupByEmail(value);
|
|
108
|
+
return !existing;
|
|
109
|
+
})
|
|
110
|
+
declare email: string;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## The `@Mandatory` Decorator
|
|
114
|
+
|
|
115
|
+
The `@Mandatory` decorator specifies which operations require a field to be present in the request body. Unlike `@Validator`, which checks field content, `@Mandatory` only verifies that the field exists in the payload.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { Mandatory } from "@antelopejs/interface-data-api/metadata";
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Usage
|
|
122
|
+
|
|
123
|
+
Pass one or more operation names as arguments. Common operation names are `"new"` (create) and `"edit"` (update).
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
@RegisterDataController()
|
|
127
|
+
class UserAPI extends DataController(
|
|
128
|
+
User,
|
|
129
|
+
DefaultRoutes.All,
|
|
130
|
+
Controller("/users"),
|
|
131
|
+
) {
|
|
132
|
+
@ModelReference()
|
|
133
|
+
@Model(UserModel, "my-database")
|
|
134
|
+
declare userModel: UserModel;
|
|
135
|
+
|
|
136
|
+
@Access(AccessMode.ReadWrite)
|
|
137
|
+
@Mandatory("new", "edit")
|
|
138
|
+
declare email: string;
|
|
139
|
+
|
|
140
|
+
@Access(AccessMode.WriteOnly)
|
|
141
|
+
@Mandatory("new")
|
|
142
|
+
declare password: string;
|
|
143
|
+
|
|
144
|
+
@Access(AccessMode.ReadWrite)
|
|
145
|
+
@Mandatory("edit")
|
|
146
|
+
declare status: string;
|
|
147
|
+
|
|
148
|
+
@Access(AccessMode.ReadWrite)
|
|
149
|
+
declare notes: string; // Not mandatory for any operation
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
In this example:
|
|
154
|
+
|
|
155
|
+
- `email` must be provided in both create and update requests.
|
|
156
|
+
- `password` is required only when creating a new record.
|
|
157
|
+
- `status` is required only when editing an existing record.
|
|
158
|
+
- `notes` is always optional.
|
|
159
|
+
|
|
160
|
+
### Skip Mandatory Validation
|
|
161
|
+
|
|
162
|
+
Use `DefaultRoutes.WithOptions` with the `noMandatory` option to create an endpoint that skips mandatory field checks.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const routes = {
|
|
166
|
+
edit: DefaultRoutes.Edit,
|
|
167
|
+
patchEdit: DefaultRoutes.WithOptions(DefaultRoutes.Edit, {
|
|
168
|
+
noMandatory: "true",
|
|
169
|
+
}),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
@RegisterDataController()
|
|
173
|
+
class UserAPI extends DataController(User, routes, Controller("/users")) {
|
|
174
|
+
@Access(AccessMode.ReadWrite)
|
|
175
|
+
@Mandatory("edit")
|
|
176
|
+
declare status: string;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Requests to `/users/edit` require `status`, while requests to `/users/patchEdit` do not.
|
|
181
|
+
|
|
182
|
+
## The `@Optional` Decorator
|
|
183
|
+
|
|
184
|
+
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.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { Optional } from "@antelopejs/interface-data-api/metadata";
|
|
188
|
+
|
|
189
|
+
@Optional()
|
|
190
|
+
declare notes: string;
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Error Handling
|
|
194
|
+
|
|
195
|
+
When validation or mandatory checks fail, the API returns a `400 Bad Request` response with a plain text message describing the issue.
|
|
196
|
+
|
|
197
|
+
- **Mandatory failure:** `"Missing mandatory fields: email, password"`
|
|
198
|
+
- **Validator failure:** `"Invalid field type(s): age, email"`
|
|
199
|
+
|
|
200
|
+
All fields are checked, and the error message lists every field that failed validation.
|
|
201
|
+
|
|
202
|
+
## Next Steps
|
|
203
|
+
|
|
204
|
+
See the [listable fields documentation](./6.listable.md) to learn how to control field visibility in list operations and configure pagination.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Listable Fields
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
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.
|
|
6
|
+
|
|
7
|
+
The `@Sortable` decorator marks fields that can be used as a sorting key in list queries.
|
|
8
|
+
|
|
9
|
+
## Basic Usage
|
|
10
|
+
|
|
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).
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import {
|
|
15
|
+
Access,
|
|
16
|
+
AccessMode,
|
|
17
|
+
Listable,
|
|
18
|
+
ModelReference,
|
|
19
|
+
} from "@antelopejs/interface-data-api/metadata";
|
|
20
|
+
|
|
21
|
+
@RegisterDataController()
|
|
22
|
+
class UserAPI extends DataController(
|
|
23
|
+
User,
|
|
24
|
+
DefaultRoutes.All,
|
|
25
|
+
Controller("/users"),
|
|
26
|
+
) {
|
|
27
|
+
@ModelReference()
|
|
28
|
+
@Model(UserModel, "my-database")
|
|
29
|
+
declare userModel: UserModel;
|
|
30
|
+
|
|
31
|
+
@Listable()
|
|
32
|
+
@Access(AccessMode.ReadOnly)
|
|
33
|
+
declare _id: string;
|
|
34
|
+
|
|
35
|
+
@Listable()
|
|
36
|
+
@Access(AccessMode.ReadOnly)
|
|
37
|
+
declare email: string;
|
|
38
|
+
|
|
39
|
+
@Access(AccessMode.ReadOnly)
|
|
40
|
+
declare biography: string; // Excluded from list, included in get
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
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
|
+
|
|
46
|
+
## Pluck Modes
|
|
47
|
+
|
|
48
|
+
Pluck modes allow a single controller to serve multiple list views with different field selections. Each `@Listable` call can target a named mode.
|
|
49
|
+
|
|
50
|
+
### Default Mode
|
|
51
|
+
|
|
52
|
+
Calling `@Listable()` without a mode name registers the field under the default `"list"` mode.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
@Listable()
|
|
56
|
+
declare name: string;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Custom Modes
|
|
60
|
+
|
|
61
|
+
Pass `true` and a mode name to register a field under a specific pluck mode.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
@Listable() // Included in default "list" mode
|
|
65
|
+
@Listable(true, "detailed") // Also included in "detailed" mode
|
|
66
|
+
declare name: string;
|
|
67
|
+
|
|
68
|
+
@Listable(true, "detailed") // Only in "detailed" mode
|
|
69
|
+
declare internalNotes: string;
|
|
70
|
+
|
|
71
|
+
@Listable() // Only in default "list" mode
|
|
72
|
+
declare email: string;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Exclude from a Mode
|
|
76
|
+
|
|
77
|
+
Pass `false` to explicitly exclude a field from a mode.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
@Listable()
|
|
81
|
+
@Listable(false, "minimal") // Excluded from "minimal" mode
|
|
82
|
+
declare description: string;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Access Pluck Modes via Routes
|
|
86
|
+
|
|
87
|
+
Define separate routes for each pluck mode using `DefaultRoutes.WithOptions` with the `pluckMode` option.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const routes = {
|
|
91
|
+
list: DefaultRoutes.List,
|
|
92
|
+
detailed: DefaultRoutes.WithOptions(DefaultRoutes.List, {
|
|
93
|
+
pluckMode: "detailed",
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
@RegisterDataController()
|
|
98
|
+
class UserAPI extends DataController(User, routes, Controller("/users")) {
|
|
99
|
+
@ModelReference()
|
|
100
|
+
@Model(UserModel, "my-database")
|
|
101
|
+
declare userModel: UserModel;
|
|
102
|
+
|
|
103
|
+
@Listable()
|
|
104
|
+
@Access(AccessMode.ReadOnly)
|
|
105
|
+
declare name: string;
|
|
106
|
+
|
|
107
|
+
@Listable(true, "detailed")
|
|
108
|
+
@Access(AccessMode.ReadOnly)
|
|
109
|
+
declare internalNotes: string;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- `GET /users/list` returns only `name`.
|
|
114
|
+
- `GET /users/detailed` returns both `name` and `internalNotes`.
|
|
115
|
+
|
|
116
|
+
### Listable with Getters
|
|
117
|
+
|
|
118
|
+
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.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
@Listable(["firstName", "lastName"])
|
|
122
|
+
@Access(AccessMode.ReadOnly)
|
|
123
|
+
get fullName() {
|
|
124
|
+
return `${this.firstName} ${this.lastName}`;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Pagination
|
|
129
|
+
|
|
130
|
+
List responses include pagination metadata. The response structure is:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
{
|
|
134
|
+
results: T[]; // Records for the current page
|
|
135
|
+
total: number; // Total number of matching records
|
|
136
|
+
offset: number; // Number of records skipped
|
|
137
|
+
limit: number; // Maximum records per page
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Query Parameters
|
|
142
|
+
|
|
143
|
+
| Parameter | Default | Description |
|
|
144
|
+
| --------- | ------- | ----------- |
|
|
145
|
+
| `limit` | `10` | Maximum number of records to return |
|
|
146
|
+
| `offset` | `0` | Number of records to skip |
|
|
147
|
+
|
|
148
|
+
### Example
|
|
149
|
+
|
|
150
|
+
```http
|
|
151
|
+
GET /users/list?limit=2&offset=4
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"results": [
|
|
157
|
+
{ "_id": "user-5", "name": "Eve" },
|
|
158
|
+
{ "_id": "user-6", "name": "Frank" }
|
|
159
|
+
],
|
|
160
|
+
"total": 100,
|
|
161
|
+
"offset": 4,
|
|
162
|
+
"limit": 2
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Limit Maximum Page Size
|
|
167
|
+
|
|
168
|
+
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`.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const routes = {
|
|
172
|
+
list: DefaultRoutes.WithOptions(DefaultRoutes.List, { maxPage: 25 }),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
@RegisterDataController()
|
|
176
|
+
class UserAPI extends DataController(User, routes, Controller("/users")) {
|
|
177
|
+
// A request with ?limit=100 returns at most 25 records
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Sorting with `@Sortable`
|
|
182
|
+
|
|
183
|
+
The `@Sortable` decorator marks a field as eligible for use as a sorting key in list operations.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { Sortable } from "@antelopejs/interface-data-api/metadata";
|
|
187
|
+
|
|
188
|
+
@RegisterDataController()
|
|
189
|
+
class UserAPI extends DataController(
|
|
190
|
+
User,
|
|
191
|
+
DefaultRoutes.All,
|
|
192
|
+
Controller("/users"),
|
|
193
|
+
) {
|
|
194
|
+
@ModelReference()
|
|
195
|
+
@Model(UserModel, "my-database")
|
|
196
|
+
declare userModel: UserModel;
|
|
197
|
+
|
|
198
|
+
@Listable()
|
|
199
|
+
@Access(AccessMode.ReadOnly)
|
|
200
|
+
@Sortable()
|
|
201
|
+
declare _id: string;
|
|
202
|
+
|
|
203
|
+
@Listable()
|
|
204
|
+
@Access(AccessMode.ReadOnly)
|
|
205
|
+
@Sortable({ noIndex: true })
|
|
206
|
+
declare name: string;
|
|
207
|
+
|
|
208
|
+
@Listable()
|
|
209
|
+
@Access(AccessMode.ReadOnly)
|
|
210
|
+
@Sortable({ noIndex: true })
|
|
211
|
+
declare age: number;
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Sort Query Parameters
|
|
216
|
+
|
|
217
|
+
| Parameter | Description |
|
|
218
|
+
| --------- | ----------- |
|
|
219
|
+
| `sortKey` | The field name to sort by. Must be decorated with `@Sortable`. |
|
|
220
|
+
| `sortDirection` | `"asc"` (ascending) or `"desc"` (descending). Defaults to `"asc"`. |
|
|
221
|
+
|
|
222
|
+
### The `noIndex` Option
|
|
223
|
+
|
|
224
|
+
By default, `@Sortable()` assumes the field has a database index and uses indexed ordering. Set `noIndex: true` when the field is not indexed, so the framework performs an in-memory sort instead.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
@Sortable() // Uses database index for sorting
|
|
228
|
+
declare _id: string;
|
|
229
|
+
|
|
230
|
+
@Sortable({ noIndex: true }) // In-memory sort, no index required
|
|
231
|
+
declare name: string;
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Example
|
|
235
|
+
|
|
236
|
+
```http
|
|
237
|
+
GET /users/list?sortKey=age&sortDirection=desc&limit=3
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"results": [
|
|
243
|
+
{ "_id": "user-1", "name": "Senior", "age": 65 },
|
|
244
|
+
{ "_id": "user-2", "name": "Adult", "age": 30 },
|
|
245
|
+
{ "_id": "user-3", "name": "Young", "age": 18 }
|
|
246
|
+
],
|
|
247
|
+
"total": 3,
|
|
248
|
+
"offset": 0,
|
|
249
|
+
"limit": 3
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
If `sortKey` refers to a field that is not marked with `@Sortable`, the API returns a `400 Bad Request` error.
|
|
254
|
+
|
|
255
|
+
## Next Steps
|
|
256
|
+
|
|
257
|
+
See the [foreign keys documentation](./7.foreign-keys.md) to learn how to establish relationships between tables.
|