@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.
@@ -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.6",
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
  }