@antelopejs/interface-data-api 0.2.1 → 0.3.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 +131 -21
- package/dist/antelope.test.js +1 -1
- package/dist/components.d.ts +64 -15
- package/dist/components.js +91 -23
- package/dist/components.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.js +42 -56
- package/dist/index.js.map +1 -1
- package/dist/metadata.d.ts +23 -3
- package/dist/metadata.js +32 -7
- package/dist/metadata.js.map +1 -1
- package/dist/tests/components/foreign_computed.test.d.ts +1 -0
- package/dist/tests/components/foreign_computed.test.js +172 -0
- package/dist/tests/components/foreign_computed.test.js.map +1 -0
- package/dist/tests/components/mandatory.test.js +3 -5
- package/dist/tests/components/mandatory.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 +2 -2
- package/dist/tests/components/partial_edit.test.js.map +1 -1
- package/dist/tests/components/per_action_access.test.js +12 -2
- package/dist/tests/components/per_action_access.test.js.map +1 -1
- package/dist/tests/components/validator.test.js +3 -5
- package/dist/tests/components/validator.test.js.map +1 -1
- package/dist/tests/index/inheritance.test.js +1 -1
- package/dist/tests/index/inheritance.test.js.map +1 -1
- package/dist/tests/index/routes.test.js +45 -3
- package/dist/tests/index/routes.test.js.map +1 -1
- package/dist/tests/integration.test.js +5 -7
- package/dist/tests/integration.test.js.map +1 -1
- package/dist/tests/utils.d.ts +6 -1
- package/dist/tests/utils.js +3 -1
- package/dist/tests/utils.js.map +1 -1
- package/docs/1.introduction.md +26 -26
- package/docs/10.joined.md +36 -5
- package/docs/11.computed.md +6 -6
- package/docs/2.data-controllers.md +4 -4
- package/docs/3.routes.md +85 -11
- package/docs/4.access-rights.md +21 -6
- package/docs/5.validators.md +2 -0
- package/docs/6.listable.md +5 -2
- package/docs/7.foreign-keys.md +9 -10
- package/docs/8.filters.md +96 -20
- package/docs/9.modifiers.md +2 -4
- package/docs/assets/joined-list.svg +23 -0
- package/docs/tsconfig.json +12 -0
- package/package.json +21 -15
- package/skills/data-api-interface/SKILL.md +9 -0
package/docs/8.filters.md
CHANGED
|
@@ -8,7 +8,7 @@ The `@Filter` decorator adds filtering capabilities to list endpoints. Filters a
|
|
|
8
8
|
|
|
9
9
|
### Default Filter
|
|
10
10
|
|
|
11
|
-
Apply `@Filter()` without arguments to enable standard comparison filtering on a field.
|
|
11
|
+
Apply `@Filter()` without arguments to enable standard comparison filtering on a string field. Query filter values remain strings: TypeScript annotations, `@Field`, and write validators do not parse them into numbers or booleans.
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { Filter } from "@antelopejs/interface-data-api/metadata";
|
|
@@ -30,12 +30,12 @@ class UserAPI extends DataController(
|
|
|
30
30
|
@Listable()
|
|
31
31
|
@Access(AccessMode.ReadWrite)
|
|
32
32
|
@Filter()
|
|
33
|
-
declare
|
|
33
|
+
declare status: string;
|
|
34
34
|
|
|
35
35
|
@Listable()
|
|
36
36
|
@Access(AccessMode.ReadWrite)
|
|
37
37
|
@Filter()
|
|
38
|
-
declare
|
|
38
|
+
declare role: string;
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
@@ -63,24 +63,82 @@ When no operator is specified, `eq` (equal) is used by default.
|
|
|
63
63
|
|
|
64
64
|
### Examples
|
|
65
65
|
|
|
66
|
-
Retrieve users
|
|
66
|
+
Retrieve active users:
|
|
67
67
|
|
|
68
68
|
```http
|
|
69
|
-
GET /users/list?
|
|
69
|
+
GET /users/list?filter_status=active
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
Retrieve
|
|
72
|
+
Retrieve users whose role is not admin:
|
|
73
|
+
|
|
74
|
+
```http
|
|
75
|
+
GET /users/list?filter_role=ne:admin
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Combine multiple filters:
|
|
73
79
|
|
|
74
80
|
```http
|
|
75
|
-
GET /users/list?
|
|
81
|
+
GET /users/list?filter_status=active&filter_role=ne:admin
|
|
76
82
|
```
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
Filter one field several times to keep the rows that pass every filter (AND):
|
|
79
85
|
|
|
80
86
|
```http
|
|
81
|
-
GET /users/list?filter_age=
|
|
87
|
+
GET /users/list?filter_age=ge:18&filter_age=lt:65
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The route receives that field's filters as a list of `FilterValue` tuples (`FieldFilter`); a field filtered once keeps a single tuple. Read either shape with `FilterValuesOf(filter)` from `@antelopejs/interface-data-api/metadata`. An indexed equality filter only uses its index when it is the field's only filter.
|
|
91
|
+
|
|
92
|
+
### Numeric and Boolean Filters
|
|
93
|
+
|
|
94
|
+
Parse and validate values explicitly in a custom filter. These filters support the six numeric comparisons and boolean equality/inequality; unsupported input throws an HTTP 400 response.
|
|
95
|
+
|
|
96
|
+
```ts docs-check
|
|
97
|
+
import { assert } from "@antelopejs/interface-api-util";
|
|
98
|
+
import type { FilterFunction } from "@antelopejs/interface-data-api/metadata";
|
|
99
|
+
|
|
100
|
+
export const numberFilter: FilterFunction<Record<string, unknown>> = (
|
|
101
|
+
_context,
|
|
102
|
+
proxy,
|
|
103
|
+
_key,
|
|
104
|
+
raw,
|
|
105
|
+
mode,
|
|
106
|
+
) => {
|
|
107
|
+
const value = Number(raw);
|
|
108
|
+
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
|
+
);
|
|
114
|
+
return proxy[mode](value);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const booleanFilter: FilterFunction<Record<string, unknown>> = (
|
|
118
|
+
_context,
|
|
119
|
+
proxy,
|
|
120
|
+
_key,
|
|
121
|
+
raw,
|
|
122
|
+
mode,
|
|
123
|
+
) => {
|
|
124
|
+
assert(raw === "true" || raw === "false", 400, "Invalid boolean");
|
|
125
|
+
assert(mode === "eq" || mode === "ne", 400, "Invalid comparison");
|
|
126
|
+
return proxy[mode](raw === "true");
|
|
127
|
+
};
|
|
82
128
|
```
|
|
83
129
|
|
|
130
|
+
Apply the functions to the corresponding fields in your controller:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
@Filter(numberFilter, false)
|
|
134
|
+
declare age: number;
|
|
135
|
+
|
|
136
|
+
@Filter(booleanFilter, false)
|
|
137
|
+
declare isActive: boolean;
|
|
138
|
+
```
|
|
139
|
+
|
|
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 callbacks that parse values: indexed equality uses the raw string directly and bypasses the filter callback.
|
|
141
|
+
|
|
84
142
|
## Custom Filters
|
|
85
143
|
|
|
86
144
|
For filtering logic beyond simple comparisons, pass a custom filter function to `@Filter`.
|
|
@@ -98,14 +156,14 @@ type FilterFunction<T, U> = (
|
|
|
98
156
|
) => ValueProxyOrValue<boolean>;
|
|
99
157
|
```
|
|
100
158
|
|
|
101
|
-
| Parameter | Description
|
|
102
|
-
| --------- |
|
|
103
|
-
| `context` | The request context, extended with `this` referencing the controller instance
|
|
104
|
-
| `proxy` | A
|
|
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`
|
|
159
|
+
| Parameter | Description |
|
|
160
|
+
| --------- | ------------------------------------------------------------------------------------------------------- |
|
|
161
|
+
| `context` | The request context, extended with `this` referencing the controller instance |
|
|
162
|
+
| `proxy` | A query expression produced by the field modifier's `unlockrequest()` method; see the limitations below |
|
|
163
|
+
| `key` | The field name being filtered |
|
|
164
|
+
| `value` | The filter value from the query parameter |
|
|
165
|
+
| `mode` | The comparison operator from the query parameter |
|
|
166
|
+
| `row` | The full database row as a `ValueProxy` |
|
|
109
167
|
|
|
110
168
|
### Example: Pattern Matching Filter
|
|
111
169
|
|
|
@@ -118,7 +176,9 @@ function filterByEmailDomain(
|
|
|
118
176
|
mode: string,
|
|
119
177
|
row: ValueProxy<Record<string, any>>,
|
|
120
178
|
) {
|
|
121
|
-
|
|
179
|
+
assert(mode === "eq", 400, "Only equality is supported for email domains");
|
|
180
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
181
|
+
return proxy.match(`@${escaped}$`);
|
|
122
182
|
}
|
|
123
183
|
|
|
124
184
|
@RegisterDataController()
|
|
@@ -138,7 +198,7 @@ class UserAPI extends DataController(
|
|
|
138
198
|
|
|
139
199
|
@Listable()
|
|
140
200
|
@Access(AccessMode.ReadWrite)
|
|
141
|
-
@Filter()
|
|
201
|
+
@Filter(numberFilter, false)
|
|
142
202
|
declare age: number;
|
|
143
203
|
}
|
|
144
204
|
```
|
|
@@ -149,6 +209,8 @@ Query for users with Gmail addresses:
|
|
|
149
209
|
GET /users/list?filter_email=gmail.com
|
|
150
210
|
```
|
|
151
211
|
|
|
212
|
+
Import `assert` from `@antelopejs/interface-api-util`. Escaping the domain makes the dot literal rather than a regular-expression wildcard. This example deliberately rejects operators other than `eq` instead of silently treating every request as equality.
|
|
213
|
+
|
|
152
214
|
### The `useIndex` Parameter
|
|
153
215
|
|
|
154
216
|
The `@Filter` decorator accepts an optional second parameter, `useIndex`, that controls whether the filter can use a database index for optimized lookups.
|
|
@@ -168,8 +230,22 @@ When `useIndex` is not specified:
|
|
|
168
230
|
|
|
169
231
|
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
232
|
|
|
233
|
+
## Mandatory Route Filters
|
|
234
|
+
|
|
235
|
+
Use `DefaultRoutes.WithOptions` to constrain a list route. The `filters` map contains `[stringValue, comparison]` tuples for fields declared with `@Filter`. Request filters for other fields are retained, but a route filter wins when both specify the same field.
|
|
236
|
+
|
|
237
|
+
```ts docs-check
|
|
238
|
+
import { DefaultRoutes } from "@antelopejs/interface-data-api";
|
|
239
|
+
|
|
240
|
+
const activeUsers = DefaultRoutes.WithOptions(DefaultRoutes.List, {
|
|
241
|
+
filters: { status: ["active", "eq"] },
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
On a controller with a `status` filter, `?filter_status=inactive&filter_role=admin` still restricts `status` to `"active"` while retaining the requested `role`. Route filters are list-query constraints, not authorization for separate get/edit/delete routes.
|
|
246
|
+
|
|
171
247
|
## Modifier Integration
|
|
172
248
|
|
|
173
|
-
The `proxy` parameter
|
|
249
|
+
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, but the built-in `EncryptionModifier.unlockrequest()` returns the stored expression unchanged: it does not decrypt ciphertext in the database. A custom modifier must explicitly support query-time access before a filter can operate on its logical value.
|
|
174
250
|
|
|
175
251
|
For raw database values, use the `row` parameter and access the field directly.
|
package/docs/9.modifiers.md
CHANGED
|
@@ -45,7 +45,6 @@ import {
|
|
|
45
45
|
import {
|
|
46
46
|
BasicDataModel,
|
|
47
47
|
Field,
|
|
48
|
-
Index,
|
|
49
48
|
Localized,
|
|
50
49
|
LocalizationModifier,
|
|
51
50
|
Model,
|
|
@@ -53,9 +52,8 @@ import {
|
|
|
53
52
|
Table,
|
|
54
53
|
} from "@antelopejs/interface-database-decorators";
|
|
55
54
|
|
|
56
|
-
@RegisterTable("content")
|
|
55
|
+
@RegisterTable("content", "myapp")
|
|
57
56
|
class Content extends Table.with(LocalizationModifier) {
|
|
58
|
-
@Index({ primary: true })
|
|
59
57
|
@Field("string")
|
|
60
58
|
declare _id: string;
|
|
61
59
|
|
|
@@ -126,7 +124,7 @@ Modifiers work automatically with foreign key relationships. When the Data API r
|
|
|
126
124
|
|
|
127
125
|
### Filters
|
|
128
126
|
|
|
129
|
-
|
|
127
|
+
Custom filters receive a query expression produced by the modifier's `unlockrequest()` method, not a decrypted JavaScript value. Localization supports query-time access; the built-in encryption modifier leaves its stored representation unchanged in query expressions. Do not use it to filter by plaintext. See the [filters documentation](./8.filters.md#modifier-integration) for details.
|
|
130
128
|
|
|
131
129
|
## Learn More
|
|
132
130
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 510" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">When the default list route resolves joined fields</title>
|
|
3
|
+
<desc id="desc">Resolve fields needed by filters before filtering and counting. Resolve sort fields before ordering and paging. Resolve remaining selected display fields only for the page. This describes the default list route, not arbitrary Query.List composition.</desc>
|
|
4
|
+
<rect width="480" height="510" rx="16" fill="#f5f7fb"/>
|
|
5
|
+
<g font-family="system-ui, sans-serif" fill="#172b45">
|
|
6
|
+
<text x="24" y="36" font-size="20" font-weight="700">Joined fields in a default list request</text>
|
|
7
|
+
<rect x="24" y="58" width="432" height="100" rx="10" fill="#e1ecfa"/>
|
|
8
|
+
<text x="42" y="87" font-size="18" font-weight="700">1. Needed by a filter</text>
|
|
9
|
+
<text x="42" y="116" font-size="16">Resolve → filter matching rows</text>
|
|
10
|
+
<text x="42" y="143" font-size="16">Count these rows for total.</text>
|
|
11
|
+
<path d="M240 168v24m-6-6 6 6 6-6" fill="none" stroke="#526780" stroke-width="2"/>
|
|
12
|
+
<rect x="24" y="204" width="432" height="100" rx="10" fill="#e1ecfa"/>
|
|
13
|
+
<text x="42" y="233" font-size="18" font-weight="700">2. Needed only for sorting</text>
|
|
14
|
+
<text x="42" y="262" font-size="16">Resolve → sort matching rows</text>
|
|
15
|
+
<text x="42" y="289" font-size="16">Then select the requested page.</text>
|
|
16
|
+
<path d="M240 314v24m-6-6 6 6 6-6" fill="none" stroke="#526780" stroke-width="2"/>
|
|
17
|
+
<rect x="24" y="350" width="432" height="100" rx="10" fill="#d9eee8"/>
|
|
18
|
+
<text x="42" y="379" font-size="18" font-weight="700">3. Needed only for display</text>
|
|
19
|
+
<text x="42" y="408" font-size="16">Resolve selected fields on the page</text>
|
|
20
|
+
<text x="42" y="435" font-size="16">Return results with pagination metadata.</text>
|
|
21
|
+
<text x="24" y="484" font-size="15">A field resolved earlier is not deferred again.</text>
|
|
22
|
+
</g>
|
|
23
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/interface-data-api",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AntelopeJS interface
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "AntelopeJS interface that generates CRUD REST endpoints from database tables, with access rules, validation and pagination",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
7
|
+
"api",
|
|
7
8
|
"crud",
|
|
8
9
|
"data-api",
|
|
9
|
-
"interface"
|
|
10
|
+
"interface",
|
|
11
|
+
"rest"
|
|
10
12
|
],
|
|
13
|
+
"homepage": "https://interfaces.antelopejs.com/docs/data-api",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/AntelopeJS/interface-data-api/issues"
|
|
16
|
+
},
|
|
11
17
|
"license": "Apache-2.0",
|
|
12
18
|
"repository": {
|
|
13
19
|
"type": "git",
|
|
@@ -57,13 +63,13 @@
|
|
|
57
63
|
"knip": "knip"
|
|
58
64
|
},
|
|
59
65
|
"devDependencies": {
|
|
60
|
-
"@antelopejs/core": "^1.
|
|
61
|
-
"@antelopejs/interface-api": "^0.0.
|
|
62
|
-
"@antelopejs/interface-api-util": "^0.1.
|
|
63
|
-
"@antelopejs/interface-core": "^0.
|
|
64
|
-
"@antelopejs/interface-database": "^0.1.
|
|
65
|
-
"@antelopejs/interface-database-decorators": "^0.1.
|
|
66
|
-
"@antelopejs/tooling-configs": "^0.0.
|
|
66
|
+
"@antelopejs/core": "^1.16.0",
|
|
67
|
+
"@antelopejs/interface-api": "^0.0.15",
|
|
68
|
+
"@antelopejs/interface-api-util": "^0.1.3",
|
|
69
|
+
"@antelopejs/interface-core": "^0.2.4",
|
|
70
|
+
"@antelopejs/interface-database": "^0.1.10",
|
|
71
|
+
"@antelopejs/interface-database-decorators": "^0.1.9",
|
|
72
|
+
"@antelopejs/tooling-configs": "^0.0.9",
|
|
67
73
|
"@types/chai": "^4.3.20",
|
|
68
74
|
"@types/mocha": "^10.0.10",
|
|
69
75
|
"@types/node": "^22.19.15",
|
|
@@ -81,11 +87,11 @@
|
|
|
81
87
|
"typescript": "^5.9.3"
|
|
82
88
|
},
|
|
83
89
|
"peerDependencies": {
|
|
84
|
-
"@antelopejs/interface-api": ">=0.0.
|
|
85
|
-
"@antelopejs/interface-api-util": ">=0.1.
|
|
86
|
-
"@antelopejs/interface-core": ">=0.
|
|
87
|
-
"@antelopejs/interface-database": ">=0.1.
|
|
88
|
-
"@antelopejs/interface-database-decorators": ">=0.1.
|
|
90
|
+
"@antelopejs/interface-api": ">=0.0.15 <1.0.0",
|
|
91
|
+
"@antelopejs/interface-api-util": ">=0.1.3 <1.0.0",
|
|
92
|
+
"@antelopejs/interface-core": ">=0.2.4 <1.0.0",
|
|
93
|
+
"@antelopejs/interface-database": ">=0.1.10 <1.0.0",
|
|
94
|
+
"@antelopejs/interface-database-decorators": ">=0.1.9 <1.0.0"
|
|
89
95
|
},
|
|
90
96
|
"packageManager": "pnpm@10.6.5",
|
|
91
97
|
"antelopeJs": {
|
|
@@ -121,6 +121,8 @@ This exposes `GET /tasks/get?id=`, `GET /tasks/list`, `POST /tasks/new`, `PUT /t
|
|
|
121
121
|
- Filters come from `filter_<name>=<mode>:<value>` query params (plain value means `eq`); modes are
|
|
122
122
|
`eq|ne|gt|ge|lt|le` (the default filter compares the raw query-string value — pass a custom filter
|
|
123
123
|
function to cast for numeric ranges). Only fields declared with `@Filter()` are filterable.
|
|
124
|
+
Repeating a field's filter (`filter_age=ge:18&filter_age=lt:65`) keeps the rows passing them all;
|
|
125
|
+
the route then receives a list of tuples for it (`FieldFilter`, read with `FilterValuesOf`).
|
|
124
126
|
- `@Joined` and `@Computed` fields are materialized in-database, so `@Sortable` and `@Filter()` work
|
|
125
127
|
efficiently on them — but those decorators must still be applied explicitly. Such fields are
|
|
126
128
|
forced read-only and to non-indexed sorting.
|
|
@@ -128,6 +130,13 @@ This exposes `GET /tasks/get?id=`, `GET /tasks/list`, `POST /tasks/new`, `PUT /t
|
|
|
128
130
|
of `DefaultRoutes.All`; use `DefaultRoutes.WithOptions(route, options, endpoint)` to rename an
|
|
129
131
|
endpoint or preset parameter options.
|
|
130
132
|
- `@Access` accepts per-action overrides, e.g. `@Access(AccessMode.ReadOnly, { edit: AccessMode.ReadWrite })`.
|
|
133
|
+
`GetEffectiveMode(field, action)` reads the mode a field has in one action.
|
|
134
|
+
- A custom list route reads its page with `Query.ListPage(db, meta, query, options)` (`offset`,
|
|
135
|
+
`limit`, `filters`, `sortKey`, `pluck`, `noForeign`, `deferredJoined`): joined, then computed, then
|
|
136
|
+
foreign, then pluck.
|
|
137
|
+
- `new` answers the created row as `get` reads it, with its `_id`; `edit` answers the stored row after
|
|
138
|
+
the update. Both accept a body already parsed (`Validation.WrittenBody`), so a wrapping route can
|
|
139
|
+
amend it before the write.
|
|
131
140
|
- `edit` is a partial update: a field absent from the body is left unchanged, `null` clears it, and
|
|
132
141
|
setters only run for the fields present. `@Mandatory("edit")` rejects a field cleared by the body
|
|
133
142
|
(`null`, `""`, `[]`), not an absent one. `new` still writes every writable field and runs every setter.
|