@antelopejs/interface-data-api 0.1.6 → 0.1.7
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/docs/1.introduction.md +128 -0
- package/docs/10.joined.md +146 -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
package/docs/3.routes.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# Routes
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Routes define which operations are available on a data controller. The module provides predefined route configurations that automatically generate endpoints with the correct HTTP methods, parameter extraction, and response handling.
|
|
6
|
+
|
|
7
|
+
## Default Routes
|
|
8
|
+
|
|
9
|
+
The `DefaultRoutes` namespace provides five built-in routes covering standard CRUD operations.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { DefaultRoutes } from "@antelopejs/interface-data-api";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Route | Object | HTTP Method | Endpoint Pattern |
|
|
16
|
+
| ----- | ------ | ----------- | ---------------- |
|
|
17
|
+
| Get | `DefaultRoutes.Get` | GET | `/resource/get?id=<id>` |
|
|
18
|
+
| List | `DefaultRoutes.List` | GET | `/resource/list` |
|
|
19
|
+
| New | `DefaultRoutes.New` | POST | `/resource/new` |
|
|
20
|
+
| Edit | `DefaultRoutes.Edit` | PUT | `/resource/edit?id=<id>` |
|
|
21
|
+
| Delete | `DefaultRoutes.Delete` | DELETE | `/resource/delete?id=<id>` |
|
|
22
|
+
|
|
23
|
+
Use `DefaultRoutes.All` to include all five routes at once.
|
|
24
|
+
|
|
25
|
+
## Route Configuration
|
|
26
|
+
|
|
27
|
+
Pass a route definition object to `DataController` to control which endpoints are created.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// All CRUD routes
|
|
31
|
+
@RegisterDataController()
|
|
32
|
+
class UserAPI extends DataController(
|
|
33
|
+
User,
|
|
34
|
+
DefaultRoutes.All,
|
|
35
|
+
Controller("/users"),
|
|
36
|
+
) {
|
|
37
|
+
// ...
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Only specific routes
|
|
41
|
+
@RegisterDataController()
|
|
42
|
+
class UserAPI extends DataController(
|
|
43
|
+
User,
|
|
44
|
+
{
|
|
45
|
+
get: DefaultRoutes.Get,
|
|
46
|
+
list: DefaultRoutes.List,
|
|
47
|
+
new: DefaultRoutes.New,
|
|
48
|
+
},
|
|
49
|
+
Controller("/users"),
|
|
50
|
+
) {
|
|
51
|
+
// edit and delete endpoints are not created
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Route Details
|
|
56
|
+
|
|
57
|
+
### Get
|
|
58
|
+
|
|
59
|
+
Retrieves a single record by its identifier.
|
|
60
|
+
|
|
61
|
+
**Endpoint:** `GET /resource/get`
|
|
62
|
+
|
|
63
|
+
**Query Parameters:**
|
|
64
|
+
|
|
65
|
+
| Parameter | Required | Description |
|
|
66
|
+
| --------- | -------- | ----------- |
|
|
67
|
+
| `id` | Yes | Record identifier |
|
|
68
|
+
|
|
69
|
+
**Example:**
|
|
70
|
+
|
|
71
|
+
```http
|
|
72
|
+
GET /users/get?id=user-123
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Response:**
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"_id": "user-123",
|
|
80
|
+
"name": "Bob",
|
|
81
|
+
"email": "bob@example.com"
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### List
|
|
86
|
+
|
|
87
|
+
Retrieves multiple records with pagination, sorting, and filtering support.
|
|
88
|
+
|
|
89
|
+
**Endpoint:** `GET /resource/list`
|
|
90
|
+
|
|
91
|
+
**Query Parameters:**
|
|
92
|
+
|
|
93
|
+
| Parameter | Required | Default | Description |
|
|
94
|
+
| --------- | -------- | ------- | ----------- |
|
|
95
|
+
| `limit` | No | `10` | Maximum number of records to return |
|
|
96
|
+
| `offset` | No | `0` | Number of records to skip |
|
|
97
|
+
| `sortKey` | No | - | Field to sort by (must be marked `@Sortable`) |
|
|
98
|
+
| `sortDirection` | No | `asc` | Sort direction: `asc` or `desc` |
|
|
99
|
+
|
|
100
|
+
**Example:**
|
|
101
|
+
|
|
102
|
+
```http
|
|
103
|
+
GET /users/list?limit=10&offset=0&sortKey=name&sortDirection=asc
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Response:**
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"results": [
|
|
111
|
+
{ "_id": "user-123", "name": "Alice", "email": "alice@example.com" },
|
|
112
|
+
{ "_id": "user-456", "name": "Bob", "email": "bob@example.com" }
|
|
113
|
+
],
|
|
114
|
+
"total": 42,
|
|
115
|
+
"offset": 0,
|
|
116
|
+
"limit": 10
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### New
|
|
121
|
+
|
|
122
|
+
Creates a new record. The request body must be a JSON object with the fields to set.
|
|
123
|
+
|
|
124
|
+
**Endpoint:** `POST /resource/new`
|
|
125
|
+
|
|
126
|
+
**Body:** JSON object with field values.
|
|
127
|
+
|
|
128
|
+
**Example:**
|
|
129
|
+
|
|
130
|
+
```http
|
|
131
|
+
POST /users/new
|
|
132
|
+
Content-Type: application/json
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
"name": "Bob",
|
|
136
|
+
"email": "bob@example.com",
|
|
137
|
+
"password": "secure123"
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Response:**
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
["user-789"]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Before inserting, the `insert` event is triggered on any table modifiers attached to the database table.
|
|
148
|
+
|
|
149
|
+
### Edit
|
|
150
|
+
|
|
151
|
+
Updates an existing record by its identifier.
|
|
152
|
+
|
|
153
|
+
**Endpoint:** `PUT /resource/edit`
|
|
154
|
+
|
|
155
|
+
**Query Parameters:**
|
|
156
|
+
|
|
157
|
+
| Parameter | Required | Description |
|
|
158
|
+
| --------- | -------- | ----------- |
|
|
159
|
+
| `id` | Yes | Record identifier |
|
|
160
|
+
|
|
161
|
+
**Body:** JSON object with fields to update.
|
|
162
|
+
|
|
163
|
+
**Example:**
|
|
164
|
+
|
|
165
|
+
```http
|
|
166
|
+
PUT /users/edit?id=user-123
|
|
167
|
+
Content-Type: application/json
|
|
168
|
+
|
|
169
|
+
{
|
|
170
|
+
"name": "Robert",
|
|
171
|
+
"email": "robert@example.com"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Response:**
|
|
176
|
+
|
|
177
|
+
```http
|
|
178
|
+
200 OK
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Before updating, the `update` event is triggered on any table modifiers attached to the database table.
|
|
182
|
+
|
|
183
|
+
### Delete
|
|
184
|
+
|
|
185
|
+
Deletes one or more records by their identifiers.
|
|
186
|
+
|
|
187
|
+
**Endpoint:** `DELETE /resource/delete`
|
|
188
|
+
|
|
189
|
+
**Query Parameters:**
|
|
190
|
+
|
|
191
|
+
| Parameter | Required | Description |
|
|
192
|
+
| --------- | -------- | ----------- |
|
|
193
|
+
| `id` | Yes | Record identifier(s). Pass multiple `id` parameters to delete several records at once. |
|
|
194
|
+
|
|
195
|
+
**Example:**
|
|
196
|
+
|
|
197
|
+
```http
|
|
198
|
+
DELETE /users/delete?id=user-123
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Response:**
|
|
202
|
+
|
|
203
|
+
```http
|
|
204
|
+
200 OK
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Route Options with `WithOptions`
|
|
208
|
+
|
|
209
|
+
The `DefaultRoutes.WithOptions` function customizes route behavior by overriding query parameters or route settings.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
DefaultRoutes.WithOptions(callback, options?, endpoint?)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
| Parameter | Description |
|
|
216
|
+
| --------- | ----------- |
|
|
217
|
+
| `callback` | The route callback to customize (e.g., `DefaultRoutes.Edit`). |
|
|
218
|
+
| `options` | An object with option overrides. These values take precedence over query parameters. |
|
|
219
|
+
| `endpoint` | An optional custom endpoint path. Defaults to the route key name. |
|
|
220
|
+
|
|
221
|
+
### Example
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
const routes = {
|
|
225
|
+
edit: DefaultRoutes.Edit,
|
|
226
|
+
quickEdit: DefaultRoutes.WithOptions(DefaultRoutes.Edit, {
|
|
227
|
+
noMandatory: "true",
|
|
228
|
+
}),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
@RegisterDataController()
|
|
232
|
+
class UserAPI extends DataController(User, routes, Controller("/users")) {
|
|
233
|
+
// /users/edit enforces mandatory fields
|
|
234
|
+
// /users/quickEdit skips mandatory field validation
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Available Options per Route
|
|
239
|
+
|
|
240
|
+
**Get:**
|
|
241
|
+
|
|
242
|
+
| Option | Type | Description |
|
|
243
|
+
| ------ | ---- | ----------- |
|
|
244
|
+
| `index` | `string` | Use a secondary index instead of the primary key |
|
|
245
|
+
| `noForeign` | `string` | Skip foreign key resolution |
|
|
246
|
+
|
|
247
|
+
**List:**
|
|
248
|
+
|
|
249
|
+
| Option | Type | Description |
|
|
250
|
+
| ------ | ---- | ----------- |
|
|
251
|
+
| `maxPage` | `number` | Maximum allowed value for `limit` (caps the page size) |
|
|
252
|
+
| `noForeign` | `boolean` | Skip foreign key resolution |
|
|
253
|
+
| `noPluck` | `boolean` | Return all database fields instead of plucked fields |
|
|
254
|
+
| `pluckMode` | `string` | Select a named pluck mode (see `@Listable`) |
|
|
255
|
+
|
|
256
|
+
**New:**
|
|
257
|
+
|
|
258
|
+
| Option | Type | Description |
|
|
259
|
+
| ------ | ---- | ----------- |
|
|
260
|
+
| `noMandatory` | `string` | Skip mandatory field validation |
|
|
261
|
+
|
|
262
|
+
**Edit:**
|
|
263
|
+
|
|
264
|
+
| Option | Type | Description |
|
|
265
|
+
| ------ | ---- | ----------- |
|
|
266
|
+
| `index` | `string` | Use a secondary index instead of the primary key |
|
|
267
|
+
| `noMandatory` | `string` | Skip mandatory field validation |
|
|
268
|
+
|
|
269
|
+
## Custom Routes
|
|
270
|
+
|
|
271
|
+
For operations beyond CRUD, define a custom route callback with `func`, `args`, and `method` properties.
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { Context } from "@antelopejs/interface-api";
|
|
275
|
+
import {
|
|
276
|
+
DataController,
|
|
277
|
+
DefaultRoutes,
|
|
278
|
+
RegisterDataController,
|
|
279
|
+
} from "@antelopejs/interface-data-api";
|
|
280
|
+
import { Parameters } from "@antelopejs/interface-data-api/components";
|
|
281
|
+
|
|
282
|
+
const Search = {
|
|
283
|
+
func: async (ctx, params) => {
|
|
284
|
+
// Custom search logic
|
|
285
|
+
return { results: [] };
|
|
286
|
+
},
|
|
287
|
+
args: [Context(), Parameters.List()],
|
|
288
|
+
method: "get",
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
@RegisterDataController()
|
|
292
|
+
class UserAPI extends DataController(
|
|
293
|
+
User,
|
|
294
|
+
{
|
|
295
|
+
...DefaultRoutes.All,
|
|
296
|
+
search: Search,
|
|
297
|
+
},
|
|
298
|
+
Controller("/users"),
|
|
299
|
+
) {
|
|
300
|
+
// All default routes plus a custom /users/search endpoint
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The `args` array contains parameter decorators that extract values from the request context. Each decorator corresponds to a parameter in the `func` callback, in order.
|
|
305
|
+
|
|
306
|
+
## Error Handling
|
|
307
|
+
|
|
308
|
+
Default routes return standard HTTP error responses:
|
|
309
|
+
|
|
310
|
+
| Status | Condition |
|
|
311
|
+
| ------ | --------- |
|
|
312
|
+
| `400 Bad Request` | Validation fails, missing required fields, or invalid parameters |
|
|
313
|
+
| `404 Not Found` | Record does not exist |
|
|
314
|
+
| `500 Internal Server Error` | Database operation fails |
|
|
315
|
+
|
|
316
|
+
Error messages are returned as plain text in the response body.
|
|
317
|
+
|
|
318
|
+
## Next Steps
|
|
319
|
+
|
|
320
|
+
See the [access rights documentation](./4.access-rights.md) to learn how to control field read/write permissions.
|
|
@@ -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.
|