@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,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.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Modifiers
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Modifiers from `@antelopejs/interface-database-decorators` work automatically with the Data API. Modifiers such as `@Hashed` or `@Encrypted` transform data transparently during read and write operations without any additional configuration in the data controller.
|
|
6
|
+
|
|
7
|
+
Some modifiers require a runtime parameter (for example, a locale key for localization). The `@ModifierKey` decorator bridges this gap by mapping a controller field to a modifier's runtime key.
|
|
8
|
+
|
|
9
|
+
## The `@ModifierKey` Decorator
|
|
10
|
+
|
|
11
|
+
Use `@ModifierKey` when a modifier needs a runtime value that varies per request. The decorator associates a controller field with a specific modifier class. The field's value at request time is passed to the modifier during lock and unlock operations.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { ModifierKey } from "@antelopejs/interface-data-api/metadata";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Signature
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
@ModifierKey(modifierClass: typeof ContainerModifier<any>)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Parameter | Description |
|
|
24
|
+
| --------- | ----------- |
|
|
25
|
+
| `modifierClass` | The modifier class that requires a runtime key (e.g., `LocalizationModifier`) |
|
|
26
|
+
|
|
27
|
+
### Example: Localized Content
|
|
28
|
+
|
|
29
|
+
The `LocalizationModifier` requires a locale identifier to determine which translation to store or retrieve. Use `@ModifierKey` to supply this value from an HTTP header.
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { Controller, Parameter } from "@antelopejs/interface-api";
|
|
33
|
+
import {
|
|
34
|
+
DataController,
|
|
35
|
+
DefaultRoutes,
|
|
36
|
+
RegisterDataController,
|
|
37
|
+
} from "@antelopejs/interface-data-api";
|
|
38
|
+
import {
|
|
39
|
+
Access,
|
|
40
|
+
AccessMode,
|
|
41
|
+
Listable,
|
|
42
|
+
ModelReference,
|
|
43
|
+
ModifierKey,
|
|
44
|
+
} from "@antelopejs/interface-data-api/metadata";
|
|
45
|
+
import {
|
|
46
|
+
BasicDataModel,
|
|
47
|
+
Field,
|
|
48
|
+
Index,
|
|
49
|
+
Localized,
|
|
50
|
+
LocalizationModifier,
|
|
51
|
+
Model,
|
|
52
|
+
RegisterTable,
|
|
53
|
+
Table,
|
|
54
|
+
} from "@antelopejs/interface-database-decorators";
|
|
55
|
+
|
|
56
|
+
@RegisterTable("content")
|
|
57
|
+
class Content extends Table.with(LocalizationModifier) {
|
|
58
|
+
@Index({ primary: true })
|
|
59
|
+
@Field("string")
|
|
60
|
+
declare _id: string;
|
|
61
|
+
|
|
62
|
+
@Localized()
|
|
63
|
+
@Field("string")
|
|
64
|
+
declare title: string;
|
|
65
|
+
|
|
66
|
+
@Localized()
|
|
67
|
+
@Field("string")
|
|
68
|
+
declare description: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class ContentModel extends BasicDataModel(Content, "content") {}
|
|
72
|
+
|
|
73
|
+
@RegisterDataController()
|
|
74
|
+
class ContentAPI extends DataController(
|
|
75
|
+
Content,
|
|
76
|
+
DefaultRoutes.All,
|
|
77
|
+
Controller("/content"),
|
|
78
|
+
) {
|
|
79
|
+
@ModelReference()
|
|
80
|
+
@Model(ContentModel, "my-database")
|
|
81
|
+
declare contentModel: ContentModel;
|
|
82
|
+
|
|
83
|
+
@Parameter("x-content-language", "header")
|
|
84
|
+
@ModifierKey(LocalizationModifier)
|
|
85
|
+
declare language: string;
|
|
86
|
+
|
|
87
|
+
@Listable()
|
|
88
|
+
@Access(AccessMode.ReadWrite)
|
|
89
|
+
declare title: string;
|
|
90
|
+
|
|
91
|
+
@Listable()
|
|
92
|
+
@Access(AccessMode.ReadWrite)
|
|
93
|
+
declare description: string;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### How It Works
|
|
98
|
+
|
|
99
|
+
1. The HTTP request includes the header `x-content-language: fr`.
|
|
100
|
+
2. The `language` property receives the value `"fr"`.
|
|
101
|
+
3. During write operations, the modifier uses `"fr"` as the key to store the localized value.
|
|
102
|
+
4. During read operations, the modifier uses `"fr"` to retrieve the correct localized value.
|
|
103
|
+
|
|
104
|
+
**Example request:**
|
|
105
|
+
|
|
106
|
+
```http
|
|
107
|
+
POST /content/new
|
|
108
|
+
x-content-language: fr
|
|
109
|
+
Content-Type: application/json
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
"title": "Titre en francais",
|
|
113
|
+
"description": "Description en francais"
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The data is stored under the `"fr"` locale key. A subsequent request with `x-content-language: en` accesses different localized values for the same record.
|
|
118
|
+
|
|
119
|
+
> **Note:** Most modifiers (such as `@Hashed` and `@Encrypted`) do not require `@ModifierKey`. Only modifiers that need a runtime parameter per request require this decorator.
|
|
120
|
+
|
|
121
|
+
## Integration with Other Features
|
|
122
|
+
|
|
123
|
+
### Foreign Keys
|
|
124
|
+
|
|
125
|
+
Modifiers work automatically with foreign key relationships. When the Data API resolves foreign records, modifier-protected fields on those records are properly unlocked during retrieval. See the [foreign keys documentation](./7.foreign-keys.md) for details.
|
|
126
|
+
|
|
127
|
+
### Filters
|
|
128
|
+
|
|
129
|
+
When filtering modifier-protected fields, custom filter functions receive the unlocked (transformed) value through the `proxy` parameter. This ensures filters operate on the correct data. See the [filters documentation](./8.filters.md) for details.
|
|
130
|
+
|
|
131
|
+
## Learn More
|
|
132
|
+
|
|
133
|
+
For the full list of available modifiers and instructions on creating custom ones, see the [@antelopejs/interface-database-decorators documentation](https://github.com/AntelopeJS/interface-database-decorators).
|
|
134
|
+
|
|
135
|
+
## Next Steps
|
|
136
|
+
|
|
137
|
+
See the [joined fields documentation](./10.joined.md) to learn how to flatten a remote field onto rows for native sorting and filtering.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/interface-data-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "AntelopeJS interface for data API with CRUD controllers and route generation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"main": "dist/index.js",
|
|
18
18
|
"types": "dist/index.d.ts",
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"docs",
|
|
22
|
+
"skills"
|
|
21
23
|
],
|
|
22
24
|
"exports": {
|
|
23
25
|
".": {
|
|
@@ -79,6 +81,9 @@
|
|
|
79
81
|
},
|
|
80
82
|
"antelopeJs": {
|
|
81
83
|
"standalone": true,
|
|
82
|
-
"test": "src/antelope.test.ts"
|
|
84
|
+
"test": "src/antelope.test.ts",
|
|
85
|
+
"skills": [
|
|
86
|
+
"./skills"
|
|
87
|
+
]
|
|
83
88
|
}
|
|
84
89
|
}
|