@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.
@@ -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.