@antelopejs/interface-data-api 0.5.0 → 0.6.0
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/README.md +26 -22
- package/dist/antelope.test.js +1 -5
- package/dist/antelope.test.js.map +1 -1
- package/dist/components.d.ts +57 -37
- package/dist/components.js +36 -68
- package/dist/components.js.map +1 -1
- package/dist/index.d.ts +44 -60
- package/dist/index.js +22 -10
- package/dist/index.js.map +1 -1
- package/dist/internal/computed.js +12 -4
- package/dist/internal/computed.js.map +1 -1
- package/dist/internal/controller-types.d.ts +19 -4
- package/dist/internal/default-filter.js +11 -1
- package/dist/internal/default-filter.js.map +1 -1
- package/dist/internal/deferred-joined-stream.js +40 -27
- package/dist/internal/deferred-joined-stream.js.map +1 -1
- package/dist/internal/extractors.d.ts +25 -0
- package/dist/internal/extractors.js +31 -0
- package/dist/internal/extractors.js.map +1 -0
- package/dist/internal/field-types.d.ts +22 -6
- package/dist/internal/filters.d.ts +3 -1
- package/dist/internal/filters.js +9 -2
- package/dist/internal/filters.js.map +1 -1
- package/dist/internal/foreign.js +1 -1
- package/dist/internal/foreign.js.map +1 -1
- package/dist/internal/guards.d.ts +20 -1
- package/dist/internal/guards.js +42 -0
- package/dist/internal/guards.js.map +1 -1
- package/dist/internal/joined.js +1 -1
- package/dist/internal/joined.js.map +1 -1
- package/dist/internal/list.d.ts +2 -2
- package/dist/internal/list.js +10 -6
- package/dist/internal/list.js.map +1 -1
- package/dist/internal/meta-records.d.ts +2 -0
- package/dist/internal/meta-records.js +10 -5
- package/dist/internal/meta-records.js.map +1 -1
- package/dist/internal/properties.d.ts +2 -2
- package/dist/internal/properties.js +6 -4
- package/dist/internal/properties.js.map +1 -1
- package/dist/internal/prototypes.d.ts +14 -0
- package/dist/internal/prototypes.js +38 -0
- package/dist/internal/prototypes.js.map +1 -0
- package/dist/internal/read-properties.d.ts +3 -0
- package/dist/internal/read-properties.js +29 -0
- package/dist/internal/read-properties.js.map +1 -0
- package/dist/internal/tables.d.ts +12 -2
- package/dist/internal/tables.js +11 -0
- package/dist/internal/tables.js.map +1 -1
- package/dist/internal/validation.d.ts +4 -3
- package/dist/internal/validation.js +18 -6
- package/dist/internal/validation.js.map +1 -1
- package/dist/metadata.d.ts +13 -13
- package/dist/metadata.js +29 -31
- package/dist/metadata.js.map +1 -1
- package/dist/tests/components/access_control.test.js +11 -3
- package/dist/tests/components/access_control.test.js.map +1 -1
- package/dist/tests/components/computed.test.js +14 -3
- package/dist/tests/components/computed.test.js.map +1 -1
- package/dist/tests/components/cross_instance.test.js +2 -2
- package/dist/tests/components/cross_instance.test.js.map +1 -1
- package/dist/tests/components/deferred_joined.test.js +1 -1
- package/dist/tests/components/deferred_joined.test.js.map +1 -1
- package/dist/tests/components/extract_generic.test.d.ts +1 -0
- package/dist/tests/components/extract_generic.test.js +64 -0
- package/dist/tests/components/extract_generic.test.js.map +1 -0
- package/dist/tests/components/filter_comparisons.test.d.ts +1 -0
- package/dist/tests/components/filter_comparisons.test.js +34 -0
- package/dist/tests/components/filter_comparisons.test.js.map +1 -0
- package/dist/tests/components/filter_list_route.test.d.ts +1 -0
- package/dist/tests/components/filter_list_route.test.js +182 -0
- package/dist/tests/components/filter_list_route.test.js.map +1 -0
- package/dist/tests/components/filter_options.test.js +3 -3
- package/dist/tests/components/filter_options.test.js.map +1 -1
- package/dist/tests/components/foreign_computed.test.js +15 -4
- package/dist/tests/components/foreign_computed.test.js.map +1 -1
- package/dist/tests/components/foreign_joined.test.js +16 -4
- package/dist/tests/components/foreign_joined.test.js.map +1 -1
- package/dist/tests/components/joined.test.js +26 -8
- package/dist/tests/components/joined.test.js.map +1 -1
- package/dist/tests/components/listable.test.js +31 -12
- package/dist/tests/components/listable.test.js.map +1 -1
- package/dist/tests/components/localized_sort.test.js +5 -1
- package/dist/tests/components/localized_sort.test.js.map +1 -1
- package/dist/tests/components/mandatory.test.js +1 -1
- package/dist/tests/components/mandatory.test.js.map +1 -1
- package/dist/tests/components/modifier_key.test.js +12 -4
- package/dist/tests/components/modifier_key.test.js.map +1 -1
- package/dist/tests/components/parse_body.test.js +17 -16
- package/dist/tests/components/parse_body.test.js.map +1 -1
- package/dist/tests/components/parsed_value.test.js +2 -2
- package/dist/tests/components/parsed_value.test.js.map +1 -1
- package/dist/tests/components/partial_edit.test.js +8 -3
- package/dist/tests/components/partial_edit.test.js.map +1 -1
- package/dist/tests/components/per_action_access.test.js +11 -3
- package/dist/tests/components/per_action_access.test.js.map +1 -1
- package/dist/tests/components/prototype_named_filter.test.d.ts +1 -0
- package/dist/tests/components/prototype_named_filter.test.js +87 -0
- package/dist/tests/components/prototype_named_filter.test.js.map +1 -0
- package/dist/tests/components/validator.test.js +1 -1
- package/dist/tests/components/validator.test.js.map +1 -1
- package/dist/tests/components/written_row.test.d.ts +1 -0
- package/dist/tests/components/written_row.test.js +99 -0
- package/dist/tests/components/written_row.test.js.map +1 -0
- package/dist/tests/helpers/other-instance.js +8 -4
- package/dist/tests/helpers/other-instance.js.map +1 -1
- package/dist/tests/index/field_decorator_target.test.d.ts +1 -0
- package/dist/tests/index/field_decorator_target.test.js +20 -0
- package/dist/tests/index/field_decorator_target.test.js.map +1 -0
- package/dist/tests/index/inheritance.test.js +6 -2
- package/dist/tests/index/inheritance.test.js.map +1 -1
- package/dist/tests/index/metadata_names.test.d.ts +1 -0
- package/dist/tests/index/metadata_names.test.js +91 -0
- package/dist/tests/index/metadata_names.test.js.map +1 -0
- package/dist/tests/index/routes.test.js +14 -5
- package/dist/tests/index/routes.test.js.map +1 -1
- package/dist/tests/integration.test.js +24 -11
- package/dist/tests/integration.test.js.map +1 -1
- package/dist/tests/internal/controller-fields.test.d.ts +1 -0
- package/dist/tests/internal/controller-fields.test.js +25 -0
- package/dist/tests/internal/controller-fields.test.js.map +1 -0
- package/dist/tests/internal/http.d.ts +21 -9
- package/dist/tests/internal/http.js +24 -3
- package/dist/tests/internal/http.js.map +1 -1
- package/dist/tests/internal/shop-schema.d.ts +28 -20
- package/dist/tests/utils.d.ts +5 -7
- package/dist/tests/utils.js.map +1 -1
- package/docs/1.introduction.md +38 -35
- package/docs/10.joined.md +51 -40
- package/docs/11.computed.md +91 -47
- package/docs/2.data-controllers.md +50 -42
- package/docs/3.routes.md +150 -133
- package/docs/4.access-rights.md +48 -44
- package/docs/5.validators.md +54 -45
- package/docs/6.listable.md +53 -49
- package/docs/7.foreign-keys.md +55 -48
- package/docs/8.filters.md +64 -57
- package/docs/9.modifiers.md +29 -25
- package/package.json +24 -14
- package/skills/data-api-interface/SKILL.md +9 -3
package/docs/7.foreign-keys.md
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Foreign keys
|
|
3
|
+
description: Resolve a foreign key field into the referenced record with @Foreign, for single and array references, across schemas, and per route.
|
|
4
|
+
mode: reference
|
|
5
|
+
---
|
|
2
6
|
|
|
3
7
|
## Overview
|
|
4
8
|
|
|
5
|
-
The `@Foreign` decorator
|
|
9
|
+
The `@Foreign` decorator links a field of your data controller to another table. When a field holds a foreign key (an ID of a record in another table), the Data API looks up the referenced record and puts it in the response. The field can hold one key or an array of keys.
|
|
6
10
|
|
|
7
|
-
## Basic
|
|
11
|
+
## Basic usage
|
|
8
12
|
|
|
9
|
-
Apply `@Foreign` to a field that stores a foreign key value. The
|
|
13
|
+
Apply `@Foreign` to a field that stores a foreign key value. The response then holds the referenced record in place of the raw ID.
|
|
10
14
|
|
|
11
|
-
Choose the response fields
|
|
15
|
+
Choose the response fields with `pluck`. A foreign lookup reads the target table, not its API controller, so the target controller's `@Access` policy does not hide passwords or internal fields. The example below selects only the user's ID, name, and email.
|
|
12
16
|
|
|
13
|
-
The snippets use the `myapp` schema. Register it in the declaring module before serving requests, as in the [quick start](./1.introduction.md#quick-start).
|
|
17
|
+
The snippets use the `myapp` schema. Register it in the declaring module before serving requests, as in the [quick start](./1.introduction.md#quick-start). The controller snippets assume that the matching model and the shared imports exist.
|
|
14
18
|
|
|
15
|
-
```typescript
|
|
19
|
+
```typescript fragment
|
|
16
20
|
import { Foreign } from "@antelopejs/interface-data-api/metadata";
|
|
17
21
|
import {
|
|
18
22
|
Field,
|
|
@@ -71,7 +75,7 @@ class OrderAPI extends DataController(
|
|
|
71
75
|
}
|
|
72
76
|
```
|
|
73
77
|
|
|
74
|
-
A GET request to `/orders/get?id=order-123` returns the
|
|
78
|
+
A GET request to `/orders/get?id=order-123` returns the user object in place of the raw ID:
|
|
75
79
|
|
|
76
80
|
```json
|
|
77
81
|
{
|
|
@@ -85,47 +89,48 @@ A GET request to `/orders/get?id=order-123` returns the full user object instead
|
|
|
85
89
|
}
|
|
86
90
|
```
|
|
87
91
|
|
|
88
|
-
## Decorator
|
|
92
|
+
## Decorator parameters
|
|
89
93
|
|
|
90
|
-
```typescript
|
|
91
|
-
@Foreign(table, index?, multi?, pluck?, schema?)
|
|
94
|
+
```typescript fragment
|
|
95
|
+
@Foreign(table, index?, multi?, pluck?, schema?, targetMeta?)
|
|
92
96
|
```
|
|
93
97
|
|
|
94
|
-
| Parameter
|
|
95
|
-
|
|
|
96
|
-
| `table`
|
|
97
|
-
| `index`
|
|
98
|
-
| `multi`
|
|
99
|
-
| `pluck`
|
|
100
|
-
| `schema`
|
|
98
|
+
| Parameter | Type | Required | Default | Description |
|
|
99
|
+
| ------------ | -------------------------- | -------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
+
| `table` | `Class<Table>` or `string` | Yes | None | The target table class or table name string |
|
|
101
|
+
| `index` | `string` | No | `"_id"` | The index on the target table to match against |
|
|
102
|
+
| `multi` | `boolean` | No | `false` | Set to `true` when the field holds an array of foreign keys |
|
|
103
|
+
| `pluck` | `string[]` | No | All fields | List of fields to include from the referenced record |
|
|
104
|
+
| `schema` | `string` | No | Controller schema | Name of the schema the target table is registered in, when it differs from the controller's schema |
|
|
105
|
+
| `targetMeta` | `DataAPIMeta` | No | None | Metadata of the target's data controller. Its `@Joined` fields that `pluck` names, or all of them when `pluck` is unset, are resolved on a relation that is not `multi` |
|
|
101
106
|
|
|
102
|
-
## Table
|
|
107
|
+
## Table reference methods
|
|
103
108
|
|
|
104
|
-
### Class
|
|
109
|
+
### Class reference
|
|
105
110
|
|
|
106
|
-
Pass the table class
|
|
111
|
+
Pass the table class. Your editor can then complete and rename the reference, and the compiler checks it.
|
|
107
112
|
|
|
108
|
-
```typescript
|
|
113
|
+
```typescript fragment
|
|
109
114
|
@Foreign(User)
|
|
110
115
|
declare userId: User;
|
|
111
116
|
```
|
|
112
117
|
|
|
113
|
-
### String
|
|
118
|
+
### String reference
|
|
114
119
|
|
|
115
|
-
Pass the registered table name as a string. Use
|
|
120
|
+
Pass the registered table name as a string. Use a string when importing the class would create a circular import.
|
|
116
121
|
|
|
117
|
-
```typescript
|
|
122
|
+
```typescript fragment
|
|
118
123
|
@Foreign("users")
|
|
119
124
|
declare userId: User;
|
|
120
125
|
```
|
|
121
126
|
|
|
122
|
-
## Multi-
|
|
127
|
+
## Multi-record references
|
|
123
128
|
|
|
124
|
-
When a field stores an array of foreign key IDs, set the `multi` parameter to `true`. The API
|
|
129
|
+
When a field stores an array of foreign key IDs, set the `multi` parameter to `true`. The API looks up each ID and returns an array of records.
|
|
125
130
|
|
|
126
|
-
The table-level `@Relation({ many: true })` is
|
|
131
|
+
The table-level `@Relation({ many: true })` is metadata only. It records that the field holds several target keys. Only the `multi` parameter of `@Foreign` controls how the API resolves the field.
|
|
127
132
|
|
|
128
|
-
```typescript
|
|
133
|
+
```typescript fragment
|
|
129
134
|
@RegisterTable("carts", "myapp")
|
|
130
135
|
class Cart extends Table {
|
|
131
136
|
@Field("string")
|
|
@@ -181,11 +186,11 @@ Response:
|
|
|
181
186
|
}
|
|
182
187
|
```
|
|
183
188
|
|
|
184
|
-
## Selective
|
|
189
|
+
## Selective field retrieval with pluck
|
|
185
190
|
|
|
186
|
-
The `pluck` parameter limits
|
|
191
|
+
The `pluck` parameter limits the fields taken from the referenced record. Use it to keep the response small when you need only some fields.
|
|
187
192
|
|
|
188
|
-
```typescript
|
|
193
|
+
```typescript fragment
|
|
189
194
|
@RegisterDataController()
|
|
190
195
|
class CartAPI extends DataController(
|
|
191
196
|
Cart,
|
|
@@ -239,46 +244,48 @@ Response with `pluck: ["name", "email"]`:
|
|
|
239
244
|
}
|
|
240
245
|
```
|
|
241
246
|
|
|
242
|
-
## Custom
|
|
247
|
+
## Custom index
|
|
243
248
|
|
|
244
|
-
|
|
249
|
+
The `index` parameter matches the field against a secondary index of the target table, in place of the primary key.
|
|
245
250
|
|
|
246
|
-
```typescript
|
|
247
|
-
// Match
|
|
251
|
+
```typescript fragment
|
|
252
|
+
// Match userEmail against the "email" index on the User table
|
|
248
253
|
@Foreign(User, "email")
|
|
249
254
|
declare userEmail: User;
|
|
250
255
|
```
|
|
251
256
|
|
|
252
|
-
## Cross-
|
|
257
|
+
## Cross-schema references
|
|
253
258
|
|
|
254
|
-
By default, the target table
|
|
259
|
+
By default, the Data API looks for the target table in the schema of the controller's table. The `schema` parameter names a different schema. At runtime, the foreign lookup goes through the target's table class, against the default instance of the schema that `RegisterSchema` bound it to. The lookup therefore uses the database of the module that declared the table. Register the target's class with `@RegisterTable` before you declare the field. A string table name with no registered class in that schema throws at declaration.
|
|
255
260
|
|
|
256
|
-
```typescript
|
|
261
|
+
```typescript fragment
|
|
257
262
|
// Resolve userId against the User table registered in the "auth" schema
|
|
258
263
|
@Access(AccessMode.ReadOnly)
|
|
259
264
|
@Foreign(User, undefined, false, undefined, "auth")
|
|
260
265
|
declare userId: User;
|
|
261
266
|
```
|
|
262
267
|
|
|
263
|
-
When a table class
|
|
268
|
+
When you pass a table class, the Data API reads its schema from its `@RegisterTable` metadata. The `schema` parameter is then needed only with a string table name, or when the class metadata names no schema. If the class metadata and the `schema` parameter name different schemas, the declaration throws.
|
|
264
269
|
|
|
265
|
-
## Disable
|
|
270
|
+
## Disable foreign key resolution
|
|
266
271
|
|
|
267
|
-
|
|
272
|
+
The `noForeign` route option skips foreign key resolution on one route. The route then returns raw IDs in place of records. On a `get` route the option is a string, and on a `list` route it is a boolean.
|
|
268
273
|
|
|
269
274
|
```typescript
|
|
275
|
+
import { DefaultRoutes } from "@antelopejs/interface-data-api";
|
|
276
|
+
|
|
270
277
|
const routes = {
|
|
271
278
|
get: DefaultRoutes.Get,
|
|
272
279
|
getRaw: DefaultRoutes.WithOptions(DefaultRoutes.Get, { noForeign: "true" }),
|
|
273
280
|
};
|
|
274
281
|
```
|
|
275
282
|
|
|
276
|
-
## Error
|
|
283
|
+
## Error handling
|
|
277
284
|
|
|
278
|
-
-
|
|
279
|
-
-
|
|
280
|
-
-
|
|
285
|
+
- If a referenced record does not exist, the field value is `null`.
|
|
286
|
+
- An invalid foreign key value resolves to `null`.
|
|
287
|
+
- In a multi-record reference, each missing item is `null` in the array.
|
|
281
288
|
|
|
282
|
-
## Next
|
|
289
|
+
## Next steps
|
|
283
290
|
|
|
284
|
-
|
|
291
|
+
To filter list results, see [Filters](./8.filters.md).
|
package/docs/8.filters.md
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Filters
|
|
3
|
+
description: Let list requests filter rows with @Filter, through the default comparisons, custom filter functions, and mandatory route filters.
|
|
4
|
+
mode: reference
|
|
5
|
+
---
|
|
2
6
|
|
|
3
7
|
## Overview
|
|
4
8
|
|
|
5
|
-
The `@Filter` decorator
|
|
9
|
+
The `@Filter` decorator lets a list request narrow its results through query parameters. The default filter compares the field with six comparison operators. A custom filter function can hold any other logic.
|
|
6
10
|
|
|
7
|
-
## Basic
|
|
11
|
+
## Basic usage
|
|
8
12
|
|
|
9
|
-
### Default
|
|
13
|
+
### Default filter
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
`@Filter()` without arguments enables the default comparison filter on a string field. Filter values stay strings. TypeScript annotations, `@Field`, and write validators do not parse them into numbers or booleans.
|
|
12
16
|
|
|
13
|
-
```typescript
|
|
17
|
+
```typescript fragment
|
|
14
18
|
import { Filter } from "@antelopejs/interface-data-api/metadata";
|
|
15
19
|
|
|
16
20
|
@RegisterDataController()
|
|
@@ -39,18 +43,18 @@ class UserAPI extends DataController(
|
|
|
39
43
|
}
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
### Query
|
|
46
|
+
### Query parameter format
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
A filter is a query parameter in one of these forms:
|
|
45
49
|
|
|
46
50
|
```
|
|
47
51
|
filter_<fieldName>=<value>
|
|
48
52
|
filter_<fieldName>=<operator>:<value>
|
|
49
53
|
```
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
Without an operator, the filter uses `eq` (equal). The text before the first colon is the operator, so a value that contains a colon needs the `eq:` prefix. The default filter answers `400` to an operator that is not in the table below.
|
|
52
56
|
|
|
53
|
-
### Comparison
|
|
57
|
+
### Comparison operators
|
|
54
58
|
|
|
55
59
|
| Operator | Description |
|
|
56
60
|
| -------- | ------------------------ |
|
|
@@ -75,7 +79,7 @@ Retrieve users whose role is not admin:
|
|
|
75
79
|
GET /users/list?filter_role=ne:admin
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
Combine
|
|
82
|
+
Combine filters on several fields:
|
|
79
83
|
|
|
80
84
|
```http
|
|
81
85
|
GET /users/list?filter_status=active&filter_role=ne:admin
|
|
@@ -87,16 +91,24 @@ Filter one field several times to keep the rows that pass every filter (AND):
|
|
|
87
91
|
GET /users/list?filter_age=ge:18&filter_age=lt:65
|
|
88
92
|
```
|
|
89
93
|
|
|
90
|
-
The route receives that field
|
|
94
|
+
The route receives the filters of that field as a list of `FilterValue` tuples (`FieldFilter`). A field filtered once keeps a single tuple. `FilterValuesOf(filter)` from `@antelopejs/interface-data-api/metadata` reads either shape as a list. An indexed equality filter uses its index only when it is the only filter on the field.
|
|
91
95
|
|
|
92
|
-
### Numeric and
|
|
96
|
+
### Numeric and boolean filters
|
|
93
97
|
|
|
94
|
-
Parse and validate
|
|
98
|
+
Parse and validate numbers and booleans in a custom filter. The filters below support the six numeric comparisons and boolean `eq` and `ne`. Any other input fails with an HTTP 400 response.
|
|
95
99
|
|
|
96
100
|
```ts docs-check
|
|
97
101
|
import { assert } from "@antelopejs/interface-api-util";
|
|
98
102
|
import type { FilterFunction } from "@antelopejs/interface-data-api/metadata";
|
|
99
103
|
|
|
104
|
+
type Comparison = "eq" | "ne" | "gt" | "ge" | "lt" | "le";
|
|
105
|
+
|
|
106
|
+
const COMPARISONS: readonly Comparison[] = ["eq", "ne", "gt", "ge", "lt", "le"];
|
|
107
|
+
|
|
108
|
+
function isComparison(mode: string): mode is Comparison {
|
|
109
|
+
return COMPARISONS.some((comparison) => comparison === mode);
|
|
110
|
+
}
|
|
111
|
+
|
|
100
112
|
export const numberFilter: FilterFunction<Record<string, unknown>> = (
|
|
101
113
|
_context,
|
|
102
114
|
proxy,
|
|
@@ -106,11 +118,7 @@ export const numberFilter: FilterFunction<Record<string, unknown>> = (
|
|
|
106
118
|
) => {
|
|
107
119
|
const value = Number(raw);
|
|
108
120
|
assert(raw.trim() !== "" && Number.isFinite(value), 400, "Invalid number");
|
|
109
|
-
assert(
|
|
110
|
-
["eq", "ne", "gt", "ge", "lt", "le"].includes(mode),
|
|
111
|
-
400,
|
|
112
|
-
"Invalid comparison",
|
|
113
|
-
);
|
|
121
|
+
assert(isComparison(mode), 400, "Invalid comparison");
|
|
114
122
|
return proxy[mode](value);
|
|
115
123
|
};
|
|
116
124
|
|
|
@@ -127,9 +135,9 @@ export const booleanFilter: FilterFunction<Record<string, unknown>> = (
|
|
|
127
135
|
};
|
|
128
136
|
```
|
|
129
137
|
|
|
130
|
-
Apply the functions to the
|
|
138
|
+
Apply the functions to the matching fields of your controller:
|
|
131
139
|
|
|
132
|
-
```typescript
|
|
140
|
+
```typescript fragment
|
|
133
141
|
@Filter(numberFilter, false)
|
|
134
142
|
declare age: number;
|
|
135
143
|
|
|
@@ -137,41 +145,40 @@ declare age: number;
|
|
|
137
145
|
declare isActive: boolean;
|
|
138
146
|
```
|
|
139
147
|
|
|
140
|
-
With these custom filters, `?filter_age=gt:30&filter_isActive=true` compares against `30` and `true`, not `"30"` and `"true"`. Keep `useIndex` false for
|
|
148
|
+
With these custom filters, `?filter_age=gt:30&filter_isActive=true` compares against `30` and `true`, not `"30"` and `"true"`. Keep `useIndex` false for a filter that parses its value. An indexed equality lookup uses the raw string and skips the filter function.
|
|
141
149
|
|
|
142
|
-
## Custom
|
|
150
|
+
## Custom filters
|
|
143
151
|
|
|
144
|
-
For
|
|
152
|
+
For logic beyond the comparison operators, pass a custom filter function to `@Filter`.
|
|
145
153
|
|
|
146
|
-
### Filter
|
|
154
|
+
### Filter function signature
|
|
147
155
|
|
|
148
|
-
```typescript
|
|
149
|
-
type FilterFunction<
|
|
156
|
+
```typescript fragment
|
|
157
|
+
type FilterFunction<
|
|
158
|
+
T extends Record<string, any>,
|
|
159
|
+
U extends Record<string, any> = FieldValues,
|
|
160
|
+
> = (
|
|
150
161
|
context: RequestContext & { this: T },
|
|
151
162
|
proxy: ValueProxy<any>,
|
|
152
163
|
key: string,
|
|
153
|
-
...rest: [
|
|
154
|
-
value: string,
|
|
155
|
-
mode: "eq" | "ne" | "gt" | "ge" | "lt" | "le",
|
|
156
|
-
row: ValueProxy<U>,
|
|
157
|
-
]
|
|
164
|
+
...rest: [value: FilterValue[0], mode: FilterValue[1], row: ValueProxy<U>]
|
|
158
165
|
) => ValueProxyOrValue<boolean>;
|
|
159
166
|
```
|
|
160
167
|
|
|
161
|
-
| Parameter | Description
|
|
162
|
-
| --------- |
|
|
163
|
-
| `context` | The request context,
|
|
164
|
-
| `proxy` | A query expression
|
|
165
|
-
| `key` | The
|
|
166
|
-
| `value` | The filter value from the query parameter
|
|
167
|
-
| `mode` | The
|
|
168
|
-
| `row` | The full database row as a `ValueProxy`
|
|
168
|
+
| Parameter | Description |
|
|
169
|
+
| --------- | -------------------------------------------------------------------------------------------------------- |
|
|
170
|
+
| `context` | The request context, with `this` set to the controller instance |
|
|
171
|
+
| `proxy` | A query expression that the field modifier's `unlockrequest()` method builds. See the limitations below. |
|
|
172
|
+
| `key` | The name of the filtered field |
|
|
173
|
+
| `value` | The filter value from the query parameter, a string |
|
|
174
|
+
| `mode` | The text before the first colon of the query parameter, unchecked. The filter defines its own modes. |
|
|
175
|
+
| `row` | The full database row as a `ValueProxy` |
|
|
169
176
|
|
|
170
|
-
`value`, `mode
|
|
177
|
+
`value`, `mode`, and `row` form one rest tuple, so the function type stays within five parameters. A function that declares them as separate parameters is assignable to the type, and `Parameters<FilterFunction<T, U>>` lists all six.
|
|
171
178
|
|
|
172
|
-
### Example:
|
|
179
|
+
### Example: pattern matching filter
|
|
173
180
|
|
|
174
|
-
```typescript
|
|
181
|
+
```typescript fragment
|
|
175
182
|
function filterByEmailDomain(
|
|
176
183
|
context: RequestContext,
|
|
177
184
|
proxy: ValueProxy<any>,
|
|
@@ -213,13 +220,13 @@ Query for users with Gmail addresses:
|
|
|
213
220
|
GET /users/list?filter_email=gmail.com
|
|
214
221
|
```
|
|
215
222
|
|
|
216
|
-
Import `assert` from `@antelopejs/interface-api-util`.
|
|
223
|
+
Import `assert` from `@antelopejs/interface-api-util`. The function escapes the domain, so the dot matches a literal dot and not any character. The function rejects operators other than `eq`, so a request with another operator fails instead of running as an equality filter.
|
|
217
224
|
|
|
218
|
-
### The `useIndex`
|
|
225
|
+
### The `useIndex` parameter
|
|
219
226
|
|
|
220
|
-
The
|
|
227
|
+
The second parameter of `@Filter`, `useIndex`, allows the filter to use a database index.
|
|
221
228
|
|
|
222
|
-
```typescript
|
|
229
|
+
```typescript fragment
|
|
223
230
|
@Filter(undefined, true) // Default filter with index optimization
|
|
224
231
|
declare status: string;
|
|
225
232
|
|
|
@@ -227,16 +234,16 @@ declare status: string;
|
|
|
227
234
|
declare name: string;
|
|
228
235
|
```
|
|
229
236
|
|
|
230
|
-
When `useIndex` is not
|
|
237
|
+
When `useIndex` is not set:
|
|
231
238
|
|
|
232
|
-
-
|
|
233
|
-
-
|
|
239
|
+
- The default filter (no custom function) uses the index when the field has a single-field index.
|
|
240
|
+
- A custom filter uses no index.
|
|
234
241
|
|
|
235
|
-
|
|
242
|
+
An indexed filter with the `eq` operator reads the matching rows through the index instead of scanning the whole table.
|
|
236
243
|
|
|
237
|
-
## Mandatory
|
|
244
|
+
## Mandatory route filters
|
|
238
245
|
|
|
239
|
-
|
|
246
|
+
`DefaultRoutes.WithOptions` can constrain a list route. Its `filters` map holds `[stringValue, comparison]` tuples for fields declared with `@Filter`. The route keeps request filters on other fields. When the route and the request filter the same field, the route filter wins.
|
|
240
247
|
|
|
241
248
|
```ts docs-check
|
|
242
249
|
import { DefaultRoutes } from "@antelopejs/interface-data-api";
|
|
@@ -246,10 +253,10 @@ const activeUsers = DefaultRoutes.WithOptions(DefaultRoutes.List, {
|
|
|
246
253
|
});
|
|
247
254
|
```
|
|
248
255
|
|
|
249
|
-
On a controller with a `status` filter, `?filter_status=inactive&filter_role=admin` still restricts `status` to `"active"`
|
|
256
|
+
On a controller with a `status` filter, `?filter_status=inactive&filter_role=admin` still restricts `status` to `"active"` and keeps the requested `role`. Route filters constrain list queries only. They do not authorize or restrict the `get`, `edit`, or `delete` routes.
|
|
250
257
|
|
|
251
|
-
## Modifier
|
|
258
|
+
## Modifier integration
|
|
252
259
|
|
|
253
|
-
The `proxy` parameter is a database expression, not an unlocked model instance. The Data API calls the modifier's `unlockrequest()` method to build that expression. Localization can select a locale at query time
|
|
260
|
+
The `proxy` parameter is a database expression, not an unlocked model instance. The Data API calls the modifier's `unlockrequest()` method to build that expression. Localization can select a locale at query time. The built-in `EncryptionModifier.unlockrequest()` returns the stored expression unchanged, so it does not decrypt ciphertext in the database. A custom modifier must support query-time access before a filter can compare its plain value.
|
|
254
261
|
|
|
255
|
-
For raw database values,
|
|
262
|
+
For raw database values, read the field from the `row` parameter.
|
package/docs/9.modifiers.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Modifiers
|
|
3
|
+
description: Use database modifiers such as @Hashed, @Encrypted, and @Localized with a data controller, and pass a per-request modifier key with @ModifierKey.
|
|
4
|
+
mode: reference
|
|
5
|
+
---
|
|
2
6
|
|
|
3
7
|
## Overview
|
|
4
8
|
|
|
5
|
-
Modifiers from `@antelopejs/interface-database-decorators` work
|
|
9
|
+
Modifiers from `@antelopejs/interface-database-decorators` work with the Data API without extra configuration in the data controller. Modifiers such as `@Hashed` or `@Encrypted` transform data on every read and write.
|
|
6
10
|
|
|
7
|
-
Some modifiers
|
|
11
|
+
Some modifiers need a value at runtime, such as the locale key of the localization modifier. The `@ModifierKey` decorator maps a controller field to the runtime key of one modifier.
|
|
8
12
|
|
|
9
|
-
## The `@ModifierKey`
|
|
13
|
+
## The `@ModifierKey` decorator
|
|
10
14
|
|
|
11
|
-
Use `@ModifierKey` when a modifier needs a
|
|
15
|
+
Use `@ModifierKey` when a modifier needs a value that changes per request. The decorator links a controller field to a modifier class. At request time, the Data API passes the value of the field to the modifier when it locks and unlocks data.
|
|
12
16
|
|
|
13
17
|
```typescript
|
|
14
18
|
import { ModifierKey } from "@antelopejs/interface-data-api/metadata";
|
|
@@ -16,17 +20,17 @@ import { ModifierKey } from "@antelopejs/interface-data-api/metadata";
|
|
|
16
20
|
|
|
17
21
|
### Signature
|
|
18
22
|
|
|
19
|
-
```typescript
|
|
23
|
+
```typescript fragment
|
|
20
24
|
@ModifierKey(modifierClass: typeof ContainerModifier<any>)
|
|
21
25
|
```
|
|
22
26
|
|
|
23
|
-
| Parameter | Description
|
|
24
|
-
| --------------- |
|
|
25
|
-
| `modifierClass` | The modifier class that
|
|
27
|
+
| Parameter | Description |
|
|
28
|
+
| --------------- | ------------------------------------------------------------------------------- |
|
|
29
|
+
| `modifierClass` | The modifier class that needs a runtime key, for example `LocalizationModifier` |
|
|
26
30
|
|
|
27
|
-
### Example:
|
|
31
|
+
### Example: localized content
|
|
28
32
|
|
|
29
|
-
The `LocalizationModifier`
|
|
33
|
+
The `LocalizationModifier` needs a locale identifier to choose which translation to store or read. In this example, `@ModifierKey` takes that value from an HTTP header.
|
|
30
34
|
|
|
31
35
|
```typescript
|
|
32
36
|
import { Controller, Parameter } from "@antelopejs/interface-api";
|
|
@@ -92,14 +96,14 @@ class ContentAPI extends DataController(
|
|
|
92
96
|
}
|
|
93
97
|
```
|
|
94
98
|
|
|
95
|
-
### How
|
|
99
|
+
### How it works
|
|
96
100
|
|
|
97
101
|
1. The HTTP request includes the header `x-content-language: fr`.
|
|
98
102
|
2. The `language` property receives the value `"fr"`.
|
|
99
|
-
3.
|
|
100
|
-
4.
|
|
103
|
+
3. On a write, the modifier stores the localized value under the key `"fr"`.
|
|
104
|
+
4. On a read, the modifier reads the localized value stored under `"fr"`.
|
|
101
105
|
|
|
102
|
-
|
|
106
|
+
This request writes French values:
|
|
103
107
|
|
|
104
108
|
```http
|
|
105
109
|
POST /content/new
|
|
@@ -112,24 +116,24 @@ Content-Type: application/json
|
|
|
112
116
|
}
|
|
113
117
|
```
|
|
114
118
|
|
|
115
|
-
The
|
|
119
|
+
The record stores the data under the `"fr"` locale key. A later request with `x-content-language: en` reads and writes other localized values of the same record.
|
|
116
120
|
|
|
117
|
-
|
|
121
|
+
Most modifiers, such as `@Hashed` and `@Encrypted`, do not need `@ModifierKey`. Only a modifier that needs a runtime value per request needs this decorator.
|
|
118
122
|
|
|
119
|
-
## Integration with
|
|
123
|
+
## Integration with other features
|
|
120
124
|
|
|
121
|
-
### Foreign
|
|
125
|
+
### Foreign keys
|
|
122
126
|
|
|
123
|
-
Modifiers work
|
|
127
|
+
Modifiers work with foreign key relationships. When the Data API resolves foreign records, it unlocks the modifier fields of those records. See [Foreign keys](./7.foreign-keys.md) for details.
|
|
124
128
|
|
|
125
129
|
### Filters
|
|
126
130
|
|
|
127
|
-
|
|
131
|
+
A custom filter receives a query expression that the modifier's `unlockrequest()` method builds, not a decrypted JavaScript value. Localization supports access at query time. The built-in encryption modifier leaves its stored value unchanged in query expressions, so do not filter on the plain text of an encrypted field. See [Modifier integration](./8.filters.md#modifier-integration) in the filters page for details.
|
|
128
132
|
|
|
129
|
-
## Learn
|
|
133
|
+
## Learn more
|
|
130
134
|
|
|
131
|
-
For the full list of
|
|
135
|
+
For the full list of modifiers and how to write your own, see the [`@antelopejs/interface-database-decorators` repository](https://github.com/AntelopeJS/interface-database-decorators).
|
|
132
136
|
|
|
133
|
-
## Next
|
|
137
|
+
## Next steps
|
|
134
138
|
|
|
135
|
-
|
|
139
|
+
To flatten a field of another table onto each row for sorting and filtering in the database, see [Joined fields](./10.joined.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/interface-data-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "AntelopeJS interface that generates CRUD REST endpoints from database tables, with access rules, validation and pagination",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
@@ -64,22 +64,21 @@
|
|
|
64
64
|
"prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi",
|
|
65
65
|
"checks": "antelopejs-checks",
|
|
66
66
|
"size": "antelopejs-check-size",
|
|
67
|
-
"ratchet": "antelopejs-ratchet",
|
|
68
67
|
"sync": "antelopejs-sync",
|
|
69
|
-
"ratchet:update": "antelopejs-ratchet --update",
|
|
70
68
|
"sync:check": "antelopejs-sync --check",
|
|
71
69
|
"typecheck": "tsc --noEmit",
|
|
72
70
|
"suppressions": "antelopejs-suppressions",
|
|
73
71
|
"suppressions:update": "antelopejs-suppressions --update"
|
|
74
72
|
},
|
|
75
73
|
"devDependencies": {
|
|
76
|
-
"@antelopejs/
|
|
77
|
-
"@antelopejs/
|
|
78
|
-
"@antelopejs/interface-api
|
|
79
|
-
"@antelopejs/interface-
|
|
80
|
-
"@antelopejs/interface-
|
|
81
|
-
"@antelopejs/interface-database
|
|
82
|
-
"@antelopejs/
|
|
74
|
+
"@antelopejs/api": "file:.campaign/tgz/antelopejs-api-6afe3e2.tgz",
|
|
75
|
+
"@antelopejs/core": "~1.20.0",
|
|
76
|
+
"@antelopejs/interface-api": "^0.2.0",
|
|
77
|
+
"@antelopejs/interface-api-util": "^0.3.0",
|
|
78
|
+
"@antelopejs/interface-core": "^0.4.0",
|
|
79
|
+
"@antelopejs/interface-database": "^0.3.0",
|
|
80
|
+
"@antelopejs/interface-database-decorators": "^0.2.0",
|
|
81
|
+
"@antelopejs/tooling-configs": "^0.2.17",
|
|
83
82
|
"@types/chai": "^4.3.20",
|
|
84
83
|
"@types/mocha": "^10.0.10",
|
|
85
84
|
"@types/node": "^22.19.15",
|
|
@@ -95,21 +94,32 @@
|
|
|
95
94
|
"release-it": "^19.2.4",
|
|
96
95
|
"release-it-changelogen": "^0.1.0",
|
|
97
96
|
"rimraf": "^6.1.3",
|
|
98
|
-
"typescript": "^5.9.3"
|
|
97
|
+
"typescript": "^5.9.3",
|
|
98
|
+
"zod": "4.5.4"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
101
|
-
"@antelopejs/interface-api": ">=0.0
|
|
101
|
+
"@antelopejs/interface-api": ">=0.3.0 <1.0.0",
|
|
102
102
|
"@antelopejs/interface-api-util": ">=0.1.3 <1.0.0",
|
|
103
103
|
"@antelopejs/interface-core": ">=0.2.4 <1.0.0",
|
|
104
104
|
"@antelopejs/interface-database": ">=0.1.10 <1.0.0",
|
|
105
105
|
"@antelopejs/interface-database-decorators": ">=0.1.10 <1.0.0"
|
|
106
106
|
},
|
|
107
|
-
"packageManager": "pnpm@10.
|
|
107
|
+
"packageManager": "pnpm@10.33.0",
|
|
108
|
+
"pnpm": {
|
|
109
|
+
"overrides": {
|
|
110
|
+
"@antelopejs/interface-api": "0.2.0",
|
|
111
|
+
"@antelopejs/interface-api-util": "0.3.0",
|
|
112
|
+
"@antelopejs/api": "file:.campaign/tgz/antelopejs-api-6afe3e2.tgz"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
108
115
|
"antelopeJs": {
|
|
109
116
|
"standalone": true,
|
|
110
117
|
"test": "src/antelope.test.ts",
|
|
111
118
|
"skills": [
|
|
112
119
|
"./skills"
|
|
113
|
-
]
|
|
120
|
+
],
|
|
121
|
+
"docs": {
|
|
122
|
+
"standard": true
|
|
123
|
+
}
|
|
114
124
|
}
|
|
115
125
|
}
|