@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
|
@@ -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.
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Foreign Keys
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `@Foreign` decorator establishes relationships between database tables in your data controller. When a field holds a foreign key (an ID referencing another table), the Data API automatically resolves the reference and includes the related record in API responses. This works for both single references and arrays of references.
|
|
6
|
+
|
|
7
|
+
## Basic Usage
|
|
8
|
+
|
|
9
|
+
Apply `@Foreign` to a field that stores a foreign key value. The decorator replaces the raw ID in the response with the full referenced record.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { Foreign } from "@antelopejs/interface-data-api/metadata";
|
|
13
|
+
import {
|
|
14
|
+
Field,
|
|
15
|
+
Index,
|
|
16
|
+
RegisterTable,
|
|
17
|
+
Relation,
|
|
18
|
+
Table,
|
|
19
|
+
} from "@antelopejs/interface-database-decorators";
|
|
20
|
+
|
|
21
|
+
@RegisterTable("users")
|
|
22
|
+
class User extends Table {
|
|
23
|
+
@Index({ primary: true })
|
|
24
|
+
@Field("string")
|
|
25
|
+
declare _id: string;
|
|
26
|
+
|
|
27
|
+
@Field("string")
|
|
28
|
+
declare name: string;
|
|
29
|
+
|
|
30
|
+
@Field("string")
|
|
31
|
+
declare email: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@RegisterTable("orders")
|
|
35
|
+
class Order extends Table {
|
|
36
|
+
@Index({ primary: true })
|
|
37
|
+
@Field("string")
|
|
38
|
+
declare _id: string;
|
|
39
|
+
|
|
40
|
+
@Field("string")
|
|
41
|
+
@Relation({ to: () => User })
|
|
42
|
+
declare userId: string;
|
|
43
|
+
|
|
44
|
+
@Field("number")
|
|
45
|
+
declare total: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@RegisterDataController()
|
|
49
|
+
class OrderAPI extends DataController(
|
|
50
|
+
Order,
|
|
51
|
+
DefaultRoutes.All,
|
|
52
|
+
Controller("/orders"),
|
|
53
|
+
) {
|
|
54
|
+
@ModelReference()
|
|
55
|
+
@Model(OrderModel, "my-database")
|
|
56
|
+
declare orderModel: OrderModel;
|
|
57
|
+
|
|
58
|
+
@Listable()
|
|
59
|
+
@Access(AccessMode.ReadOnly)
|
|
60
|
+
declare _id: string;
|
|
61
|
+
|
|
62
|
+
@Listable()
|
|
63
|
+
@Access(AccessMode.ReadOnly)
|
|
64
|
+
@Foreign(User)
|
|
65
|
+
declare userId: User;
|
|
66
|
+
|
|
67
|
+
@Listable()
|
|
68
|
+
@Access(AccessMode.ReadOnly)
|
|
69
|
+
declare total: number;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A GET request to `/orders/get?id=order-123` returns the full user object instead of a raw ID:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"_id": "order-123",
|
|
78
|
+
"userId": {
|
|
79
|
+
"_id": "user-456",
|
|
80
|
+
"name": "Bob",
|
|
81
|
+
"email": "bob@example.com"
|
|
82
|
+
},
|
|
83
|
+
"total": 99.99
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Decorator Parameters
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
@Foreign(table, index?, multi?, pluck?, schema?)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| Parameter | Type | Required | Default | Description |
|
|
94
|
+
| --------- | ---- | -------- | ------- | ----------- |
|
|
95
|
+
| `table` | `Class<Table>` or `string` | Yes | - | The target table class or table name string |
|
|
96
|
+
| `index` | `string` | No | `"_id"` | The index on the target table to match against |
|
|
97
|
+
| `multi` | `boolean` | No | `false` | Set to `true` when the field holds an array of foreign keys |
|
|
98
|
+
| `pluck` | `string[]` | No | All fields | List of fields to include from the referenced record |
|
|
99
|
+
| `schema` | `string` | No | Controller schema | Name of the schema the target table is registered in, when it differs from the controller's schema |
|
|
100
|
+
|
|
101
|
+
## Table Reference Methods
|
|
102
|
+
|
|
103
|
+
### Class Reference
|
|
104
|
+
|
|
105
|
+
Pass the table class directly. This provides IDE autocompletion, refactoring support, and compile-time validation.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
@Foreign(User)
|
|
109
|
+
declare userId: User;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### String Reference
|
|
113
|
+
|
|
114
|
+
Pass the registered table name as a string. Use this when importing the class would create circular dependencies.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
@Foreign("users")
|
|
118
|
+
declare userId: User;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Multi-Record References
|
|
122
|
+
|
|
123
|
+
When a field stores an array of foreign key IDs, set the `multi` parameter to `true`. The API resolves each ID and returns an array of full records.
|
|
124
|
+
|
|
125
|
+
The table-level `@Relation({ many: true })` is declarative metadata recording that the field holds multiple target keys; API-side resolution is controlled solely by the `multi` parameter of `@Foreign`.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
@RegisterTable("carts")
|
|
129
|
+
class Cart extends Table {
|
|
130
|
+
@Index({ primary: true })
|
|
131
|
+
@Field("string")
|
|
132
|
+
declare _id: string;
|
|
133
|
+
|
|
134
|
+
@Field(["string"])
|
|
135
|
+
@Relation({ to: () => CartItem, many: true })
|
|
136
|
+
declare items: string[]; // Array of CartItem IDs
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@RegisterTable("cart_items")
|
|
140
|
+
class CartItem extends Table {
|
|
141
|
+
@Index({ primary: true })
|
|
142
|
+
@Field("string")
|
|
143
|
+
declare _id: string;
|
|
144
|
+
|
|
145
|
+
@Field("string")
|
|
146
|
+
declare name: string;
|
|
147
|
+
|
|
148
|
+
@Field("number")
|
|
149
|
+
declare price: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@RegisterDataController()
|
|
153
|
+
class CartAPI extends DataController(
|
|
154
|
+
Cart,
|
|
155
|
+
DefaultRoutes.All,
|
|
156
|
+
Controller("/carts"),
|
|
157
|
+
) {
|
|
158
|
+
@ModelReference()
|
|
159
|
+
@Model(CartModel, "my-database")
|
|
160
|
+
declare cartModel: CartModel;
|
|
161
|
+
|
|
162
|
+
@Listable()
|
|
163
|
+
@Access(AccessMode.ReadOnly)
|
|
164
|
+
declare _id: string;
|
|
165
|
+
|
|
166
|
+
@Listable()
|
|
167
|
+
@Access(AccessMode.ReadOnly)
|
|
168
|
+
@Foreign(CartItem, undefined, true)
|
|
169
|
+
declare items: CartItem[];
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Response:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"_id": "cart-123",
|
|
178
|
+
"items": [
|
|
179
|
+
{ "_id": "item-1", "name": "Widget", "price": 9.99 },
|
|
180
|
+
{ "_id": "item-2", "name": "Gadget", "price": 24.99 }
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Selective Field Retrieval with Pluck
|
|
186
|
+
|
|
187
|
+
The `pluck` parameter limits which fields are included from the referenced record. This reduces response payload size when you only need specific fields.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
@RegisterDataController()
|
|
191
|
+
class CartAPI extends DataController(
|
|
192
|
+
Cart,
|
|
193
|
+
DefaultRoutes.All,
|
|
194
|
+
Controller("/carts"),
|
|
195
|
+
) {
|
|
196
|
+
@ModelReference()
|
|
197
|
+
@Model(CartModel, "my-database")
|
|
198
|
+
declare cartModel: CartModel;
|
|
199
|
+
|
|
200
|
+
@Listable()
|
|
201
|
+
@Access(AccessMode.ReadOnly)
|
|
202
|
+
declare _id: string;
|
|
203
|
+
|
|
204
|
+
// Only include name and email from User
|
|
205
|
+
@Access(AccessMode.ReadOnly)
|
|
206
|
+
@Foreign(User, undefined, false, ["name", "email"])
|
|
207
|
+
declare userId: User;
|
|
208
|
+
|
|
209
|
+
// Only include name and price from CartItem
|
|
210
|
+
@Access(AccessMode.ReadOnly)
|
|
211
|
+
@Foreign(CartItem, undefined, true, ["name", "price"])
|
|
212
|
+
declare items: CartItem[];
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Response without pluck:
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"_id": "cart-123",
|
|
221
|
+
"userId": {
|
|
222
|
+
"_id": "user-456",
|
|
223
|
+
"name": "Bob",
|
|
224
|
+
"email": "bob@example.com",
|
|
225
|
+
"password": "hashed_value",
|
|
226
|
+
"internalNotes": "..."
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Response with `pluck: ["name", "email"]`:
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"_id": "cart-123",
|
|
236
|
+
"userId": {
|
|
237
|
+
"name": "Bob",
|
|
238
|
+
"email": "bob@example.com"
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Custom Index
|
|
244
|
+
|
|
245
|
+
Use the `index` parameter to match against a secondary index instead of the primary key.
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// Match userId against the "email" index on the User table
|
|
249
|
+
@Foreign(User, "email")
|
|
250
|
+
declare userEmail: User;
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Cross-Schema References
|
|
254
|
+
|
|
255
|
+
By default, the target table is resolved in the same schema as the controller's table. Use the `schema` parameter to reference a table registered in a different schema. At runtime the foreign lookup is resolved against that schema's default instance.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// Resolve userId against the User table registered in the "auth" schema
|
|
259
|
+
@Access(AccessMode.ReadOnly)
|
|
260
|
+
@Foreign(User, undefined, false, undefined, "auth")
|
|
261
|
+
declare userId: User;
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
When a table class is passed (instead of a string), its schema is inferred from its `@RegisterTable` metadata, so the `schema` parameter is only needed to override that or when passing a string table name. If both the class metadata and the explicit `schema` option specify a schema and they disagree, an error is thrown.
|
|
265
|
+
|
|
266
|
+
## Disable Foreign Key Resolution
|
|
267
|
+
|
|
268
|
+
Use the `noForeign` route option to skip foreign key resolution for specific endpoints. This returns raw IDs instead of resolved objects.
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
const routes = {
|
|
272
|
+
get: DefaultRoutes.Get,
|
|
273
|
+
getRaw: DefaultRoutes.WithOptions(DefaultRoutes.Get, { noForeign: "true" }),
|
|
274
|
+
};
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Error Handling
|
|
278
|
+
|
|
279
|
+
- **Missing References** - If a referenced record does not exist, the field value is `null`.
|
|
280
|
+
- **Invalid IDs** - Invalid foreign key values result in `null` for the resolved field.
|
|
281
|
+
- **Arrays with Missing Items** - In multi-record references, missing items appear as `null` in the array.
|
|
282
|
+
|
|
283
|
+
## Next Steps
|
|
284
|
+
|
|
285
|
+
See the [filters documentation](./8.filters.md) to learn how to add filtering capabilities to list endpoints.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Filters
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `@Filter` decorator adds filtering capabilities to list endpoints. Filters allow clients to narrow down results using query parameters. The module provides a default filter with standard comparison operators and supports custom filter functions for advanced logic.
|
|
6
|
+
|
|
7
|
+
## Basic Usage
|
|
8
|
+
|
|
9
|
+
### Default Filter
|
|
10
|
+
|
|
11
|
+
Apply `@Filter()` without arguments to enable standard comparison filtering on a field.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Filter } 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
|
+
@Listable()
|
|
27
|
+
@Access(AccessMode.ReadOnly)
|
|
28
|
+
declare _id: string;
|
|
29
|
+
|
|
30
|
+
@Listable()
|
|
31
|
+
@Access(AccessMode.ReadWrite)
|
|
32
|
+
@Filter()
|
|
33
|
+
declare age: number;
|
|
34
|
+
|
|
35
|
+
@Listable()
|
|
36
|
+
@Access(AccessMode.ReadWrite)
|
|
37
|
+
@Filter()
|
|
38
|
+
declare isActive: boolean;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Query Parameter Format
|
|
43
|
+
|
|
44
|
+
Filters are applied through query parameters using the following format:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
filter_<fieldName>=<value>
|
|
48
|
+
filter_<fieldName>=<operator>:<value>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
When no operator is specified, `eq` (equal) is used by default.
|
|
52
|
+
|
|
53
|
+
### Comparison Operators
|
|
54
|
+
|
|
55
|
+
| Operator | Description |
|
|
56
|
+
| -------- | ----------- |
|
|
57
|
+
| `eq` | Equal to (default) |
|
|
58
|
+
| `ne` | Not equal to |
|
|
59
|
+
| `gt` | Greater than |
|
|
60
|
+
| `ge` | Greater than or equal to |
|
|
61
|
+
| `lt` | Less than |
|
|
62
|
+
| `le` | Less than or equal to |
|
|
63
|
+
|
|
64
|
+
### Examples
|
|
65
|
+
|
|
66
|
+
Retrieve users older than 30:
|
|
67
|
+
|
|
68
|
+
```http
|
|
69
|
+
GET /users/list?filter_age=gt:30
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Retrieve active users:
|
|
73
|
+
|
|
74
|
+
```http
|
|
75
|
+
GET /users/list?filter_isActive=true
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Combine multiple filters (users under 25 who are active):
|
|
79
|
+
|
|
80
|
+
```http
|
|
81
|
+
GET /users/list?filter_age=lt:25&filter_isActive=true
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Custom Filters
|
|
85
|
+
|
|
86
|
+
For filtering logic beyond simple comparisons, pass a custom filter function to `@Filter`.
|
|
87
|
+
|
|
88
|
+
### Filter Function Signature
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
type FilterFunction<T, U> = (
|
|
92
|
+
context: RequestContext & { this: T },
|
|
93
|
+
proxy: ValueProxy<any>,
|
|
94
|
+
key: string,
|
|
95
|
+
value: string,
|
|
96
|
+
mode: "eq" | "ne" | "gt" | "ge" | "lt" | "le",
|
|
97
|
+
row: ValueProxy<U>,
|
|
98
|
+
) => ValueProxyOrValue<boolean>;
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Parameter | Description |
|
|
102
|
+
| --------- | ----------- |
|
|
103
|
+
| `context` | The request context, extended with `this` referencing the controller instance |
|
|
104
|
+
| `proxy` | A `ValueProxy` representing the field value with modifier transformations applied (e.g., decrypted, localized) |
|
|
105
|
+
| `key` | The field name being filtered |
|
|
106
|
+
| `value` | The filter value from the query parameter |
|
|
107
|
+
| `mode` | The comparison operator from the query parameter |
|
|
108
|
+
| `row` | The full database row as a `ValueProxy` |
|
|
109
|
+
|
|
110
|
+
### Example: Pattern Matching Filter
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
function filterByEmailDomain(
|
|
114
|
+
context: RequestContext,
|
|
115
|
+
proxy: ValueProxy<any>,
|
|
116
|
+
key: string,
|
|
117
|
+
value: string,
|
|
118
|
+
mode: string,
|
|
119
|
+
row: ValueProxy<Record<string, any>>,
|
|
120
|
+
) {
|
|
121
|
+
return proxy.match(`@${value}$`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@RegisterDataController()
|
|
125
|
+
class UserAPI extends DataController(
|
|
126
|
+
User,
|
|
127
|
+
DefaultRoutes.All,
|
|
128
|
+
Controller("/users"),
|
|
129
|
+
) {
|
|
130
|
+
@ModelReference()
|
|
131
|
+
@Model(UserModel, "my-database")
|
|
132
|
+
declare userModel: UserModel;
|
|
133
|
+
|
|
134
|
+
@Listable()
|
|
135
|
+
@Access(AccessMode.ReadWrite)
|
|
136
|
+
@Filter(filterByEmailDomain)
|
|
137
|
+
declare email: string;
|
|
138
|
+
|
|
139
|
+
@Listable()
|
|
140
|
+
@Access(AccessMode.ReadWrite)
|
|
141
|
+
@Filter()
|
|
142
|
+
declare age: number;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Query for users with Gmail addresses:
|
|
147
|
+
|
|
148
|
+
```http
|
|
149
|
+
GET /users/list?filter_email=gmail.com
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### The `useIndex` Parameter
|
|
153
|
+
|
|
154
|
+
The `@Filter` decorator accepts an optional second parameter, `useIndex`, that controls whether the filter can use a database index for optimized lookups.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
@Filter(undefined, true) // Default filter with index optimization
|
|
158
|
+
declare status: string;
|
|
159
|
+
|
|
160
|
+
@Filter(customFilter, false) // Custom filter without index optimization
|
|
161
|
+
declare name: string;
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
When `useIndex` is not specified:
|
|
165
|
+
|
|
166
|
+
- Default filters (no custom function) automatically use the index if the field has one.
|
|
167
|
+
- Custom filters default to no index usage.
|
|
168
|
+
|
|
169
|
+
Index-optimized filters with an `eq` operator perform an indexed lookup instead of a full table scan, which significantly improves performance on large datasets.
|
|
170
|
+
|
|
171
|
+
## Modifier Integration
|
|
172
|
+
|
|
173
|
+
The `proxy` parameter in custom filter functions provides the field value after modifier transformations have been applied. For example, if a field uses an encryption modifier, `proxy` contains the decrypted value. This ensures filters work correctly with modifier-protected data.
|
|
174
|
+
|
|
175
|
+
For raw database values, use the `row` parameter and access the field directly.
|