@antelopejs/interface-data-api 0.2.2 → 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/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 +14 -0
- package/dist/metadata.js +23 -4
- 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 +56 -8
- package/docs/4.access-rights.md +21 -6
- package/docs/5.validators.md +2 -0
- package/docs/6.listable.md +2 -2
- package/docs/7.foreign-keys.md +9 -10
- package/docs/8.filters.md +22 -10
- package/docs/9.modifiers.md +2 -4
- package/docs/assets/joined-list.svg +23 -0
- package/package.json +1 -1
- package/skills/data-api-interface/SKILL.md +9 -0
package/docs/10.joined.md
CHANGED
|
@@ -10,19 +10,19 @@ Joined fields are read-only: the value is set by a lookup at query time and is n
|
|
|
10
10
|
|
|
11
11
|
Apply `@Joined` to a field that should be populated from a remote table. The local row must carry a foreign key (`localKey`) that matches the remote table's `remoteIndex`.
|
|
12
12
|
|
|
13
|
+
Use [foreign keys](./7.foreign-keys.md) when the response needs a nested record instead. The snippets below assume the controller imports and model from the [quick start](./1.introduction.md#quick-start), with `myapp` registered before requests are served.
|
|
14
|
+
|
|
13
15
|
```typescript
|
|
14
16
|
import { Joined } from "@antelopejs/interface-data-api/metadata";
|
|
15
17
|
import {
|
|
16
18
|
Field,
|
|
17
|
-
Index,
|
|
18
19
|
RegisterTable,
|
|
19
20
|
Relation,
|
|
20
21
|
Table,
|
|
21
22
|
} from "@antelopejs/interface-database-decorators";
|
|
22
23
|
|
|
23
|
-
@RegisterTable("users")
|
|
24
|
+
@RegisterTable("users", "myapp")
|
|
24
25
|
class User extends Table {
|
|
25
|
-
@Index({ primary: true })
|
|
26
26
|
@Field("string")
|
|
27
27
|
declare _id: string;
|
|
28
28
|
|
|
@@ -30,9 +30,8 @@ class User extends Table {
|
|
|
30
30
|
declare name: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
@RegisterTable("orders")
|
|
33
|
+
@RegisterTable("orders", "myapp")
|
|
34
34
|
class Order extends Table {
|
|
35
|
-
@Index({ primary: true })
|
|
36
35
|
@Field("string")
|
|
37
36
|
declare _id: string;
|
|
38
37
|
|
|
@@ -118,6 +117,12 @@ Joined fields are always sorted without a database index (`@Sortable` is forced
|
|
|
118
117
|
|
|
119
118
|
## Deferred Resolution on Lists
|
|
120
119
|
|
|
120
|
+
The default list route handles this scheduling for you. Continue with [cross-schema joins](#cross-schema-joins) unless you are building a custom route with `Query.List`.
|
|
121
|
+
|
|
122
|
+
{width="480"}
|
|
123
|
+
|
|
124
|
+
### Advanced: composing `Query.List`
|
|
125
|
+
|
|
121
126
|
`Query.List` only materializes the joined groups that the request's sort or filters reference before the count. The remaining display-only groups are deferred: the returned stream resolves them itself as soon as an operation can observe them, so destructuring `[query, total]` alone always yields rows with every `@Joined` field resolved.
|
|
122
127
|
|
|
123
128
|
- Paging the stream (`slice`, `nth`) appends the lookups after the page boundary, so they only run on the returned rows.
|
|
@@ -127,6 +132,32 @@ Joined fields are always sorted without a database index (`@Sortable` is forced
|
|
|
127
132
|
|
|
128
133
|
The page-only benefit therefore holds as long as paging is the first thing the caller does. An operation applied to the whole stream before paging — a filter, a sort, or the `lookup` that `Query.Foreign` stages for a `@Foreign` field — materializes the joined groups over the whole matched set instead. The lookups still stay out of the count pipeline, but a caller that wants them strictly on the page must pass `{ exposeDeferredJoined: true }` to `Query.List` and resolve them itself after the slice: the raw stream is then returned unchanged and the third tuple element names the joined fields left to resolve with `Query.Joined`. This is what the default list route does, which also lets it limit the page lookups to the plucked fields.
|
|
129
134
|
|
|
135
|
+
### Reading a page: `Query.ListPage`
|
|
136
|
+
|
|
137
|
+
A custom list route over `Query.List` reads its page with `Query.ListPage` rather than chaining the steps itself. It slices the stream, then resolves on the page only, in this order: the deferred joined groups, the `@Computed` fields the filters and the sort did not already merge, the `@Foreign` relations (last, since a computed expression may read a relation's key), and the pluck.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const [query, total, deferredJoined] = Query.List(
|
|
141
|
+
this,
|
|
142
|
+
meta,
|
|
143
|
+
model.table,
|
|
144
|
+
ctx,
|
|
145
|
+
[params.sortKey, params.sortDirection],
|
|
146
|
+
params.filters,
|
|
147
|
+
model.database,
|
|
148
|
+
{ exposeDeferredJoined: true },
|
|
149
|
+
);
|
|
150
|
+
const rows = await Query.ListPage(model.database, meta, query, {
|
|
151
|
+
offset: params.offset ?? 0,
|
|
152
|
+
limit: params.limit ?? 10,
|
|
153
|
+
filters: params.filters,
|
|
154
|
+
sortKey: params.sortKey,
|
|
155
|
+
pluck, // Set of field names; every field when unset
|
|
156
|
+
noForeign: params.noForeign,
|
|
157
|
+
deferredJoined,
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
130
161
|
## Cross-Schema Joins
|
|
131
162
|
|
|
132
163
|
By default, the remote table is resolved in the same schema as the controller's table. Use the `schema` option to join against a table registered in a different schema; the join is resolved through the remote table's class, against the default instance of the schema `RegisterSchema` bound it to. The remote class must be registered with `@RegisterTable` before the field is declared; a string table name with no registered class in that schema throws at declaration.
|
package/docs/11.computed.md
CHANGED
|
@@ -10,10 +10,12 @@ Computed fields are read-only: the value is produced by the query and is never p
|
|
|
10
10
|
|
|
11
11
|
The expression receives the row proxy and the schema instance of the controller's table. It must return a value built from the query builder (`ValueProxy` operations or a subquery), not plain JavaScript computed at request time.
|
|
12
12
|
|
|
13
|
+
The snippets extend the [quick-start controller pattern](./1.introduction.md#quick-start); define `GroupModel` with `BasicDataModel(Group)` and register the `myapp` schema before serving requests. Check [driver support](#driver-support) before using aggregate subqueries.
|
|
14
|
+
|
|
13
15
|
```typescript
|
|
14
16
|
import { Computed } from "@antelopejs/interface-data-api/metadata";
|
|
15
17
|
|
|
16
|
-
@RegisterTable("groups")
|
|
18
|
+
@RegisterTable("groups", "myapp")
|
|
17
19
|
class Group extends Table {
|
|
18
20
|
@Field("string")
|
|
19
21
|
declare _id: string;
|
|
@@ -22,7 +24,7 @@ class Group extends Table {
|
|
|
22
24
|
declare name: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
@RegisterTable("devices")
|
|
27
|
+
@RegisterTable("devices", "myapp")
|
|
26
28
|
class Device extends Table {
|
|
27
29
|
@Field("string")
|
|
28
30
|
declare _id: string;
|
|
@@ -116,15 +118,13 @@ Computed fields are always sorted without a database index (`@Sortable` is force
|
|
|
116
118
|
|
|
117
119
|
### Filtering on numeric computed fields
|
|
118
120
|
|
|
119
|
-
Filter values arrive as strings from the query string.
|
|
121
|
+
Filter values arrive as strings from the query string. Use the validated `numberFilter` from [Numeric and Boolean Filters](./8.filters.md#numeric-and-boolean-filters), imported into the controller file. It rejects invalid numbers and honors the requested comparison operator; `parseFloat` alone accepts partial input such as `"3devices"`.
|
|
120
122
|
|
|
121
123
|
```typescript
|
|
122
124
|
@Computed((row, db) => db.table("devices").getAll(row.key("_id"), "group_id").count(), {
|
|
123
125
|
default: 0,
|
|
124
126
|
})
|
|
125
|
-
@Filter(
|
|
126
|
-
(proxy as ValueProxy<number>).eq(parseFloat(value)),
|
|
127
|
-
)
|
|
127
|
+
@Filter(numberFilter, false)
|
|
128
128
|
declare devices_count: number;
|
|
129
129
|
```
|
|
130
130
|
|
|
@@ -8,7 +8,9 @@ A data controller is the central building block of the Data API. It connects a d
|
|
|
8
8
|
|
|
9
9
|
Use the `DataController` function to define a new controller. It accepts three arguments: the table class, a route definition object, and a controller base with the endpoint path.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
This example uses the `myapp` schema and the `my-database` instance. Import the table declaration and await `RegisterSchema("myapp")` in the declaring module before serving requests; provision the named instance through your database setup.
|
|
12
|
+
|
|
13
|
+
```ts docs-check
|
|
12
14
|
import { Controller } from "@antelopejs/interface-api";
|
|
13
15
|
import {
|
|
14
16
|
DataController,
|
|
@@ -24,16 +26,14 @@ import {
|
|
|
24
26
|
import {
|
|
25
27
|
BasicDataModel,
|
|
26
28
|
Field,
|
|
27
|
-
Index,
|
|
28
29
|
Model,
|
|
29
30
|
RegisterTable,
|
|
30
31
|
Table,
|
|
31
32
|
} from "@antelopejs/interface-database-decorators";
|
|
32
33
|
|
|
33
34
|
// Define the database table
|
|
34
|
-
@RegisterTable("users")
|
|
35
|
+
@RegisterTable("users", "myapp")
|
|
35
36
|
class User extends Table {
|
|
36
|
-
@Index({ primary: true })
|
|
37
37
|
@Field("string")
|
|
38
38
|
declare _id: string;
|
|
39
39
|
|
package/docs/3.routes.md
CHANGED
|
@@ -138,10 +138,14 @@ Content-Type: application/json
|
|
|
138
138
|
}
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
-
**Response:**
|
|
141
|
+
**Response:** the created record, as [Get](#get) answers it, with its `_id` even when `get` does not read it.
|
|
142
142
|
|
|
143
143
|
```json
|
|
144
|
-
|
|
144
|
+
{
|
|
145
|
+
"_id": "user-789",
|
|
146
|
+
"name": "Bob",
|
|
147
|
+
"email": "bob@example.com"
|
|
148
|
+
}
|
|
145
149
|
```
|
|
146
150
|
|
|
147
151
|
Every writable field is written: a field absent from the body is written as `undefined`, and every writable setter of the controller runs (with `undefined` for absent fields). A body that is not valid JSON or not a JSON object is rejected with `400 Bad Request`.
|
|
@@ -184,10 +188,14 @@ Content-Type: application/json
|
|
|
184
188
|
}
|
|
185
189
|
```
|
|
186
190
|
|
|
187
|
-
**Response:**
|
|
191
|
+
**Response:** the stored record after the update, as [Get](#get) answers it (joined, computed and foreign fields included), so the caller needs no second read.
|
|
188
192
|
|
|
189
|
-
```
|
|
190
|
-
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"_id": "user-123",
|
|
196
|
+
"name": "Robert",
|
|
197
|
+
"email": "robert@example.com"
|
|
198
|
+
}
|
|
191
199
|
```
|
|
192
200
|
|
|
193
201
|
Before updating, the `update` event is triggered on any table modifiers attached to the database table.
|
|
@@ -264,7 +272,7 @@ class UserAPI extends DataController(User, routes, Controller("/users")) {
|
|
|
264
272
|
| `noForeign` | `boolean` | Skip foreign key resolution |
|
|
265
273
|
| `noPluck` | `boolean` | Return all database fields instead of plucked fields |
|
|
266
274
|
| `pluckMode` | `string` | Select a named pluck mode (see `@Listable`) |
|
|
267
|
-
| `filters` | `Record<string,
|
|
275
|
+
| `filters` | `Record<string, FieldFilter>` | Mandatory filters merged with request filters; route field keys win (see [filters](./8.filters.md#mandatory-route-filters)). |
|
|
268
276
|
|
|
269
277
|
**New:**
|
|
270
278
|
|
|
@@ -279,6 +287,33 @@ class UserAPI extends DataController(User, routes, Controller("/users")) {
|
|
|
279
287
|
| `index` | `string` | Use a secondary index instead of the primary key |
|
|
280
288
|
| `noMandatory` | `string` | Skip mandatory field validation |
|
|
281
289
|
|
|
290
|
+
## Amending the Body of New and Edit
|
|
291
|
+
|
|
292
|
+
The `new` and `edit` routes take the raw request body (`Buffer`) or a body already parsed (`Validation.WrittenBody`). A route wrapping them can parse the body, amend it (stamp a field, drop one) and hand the object on: the route validates and writes it as handed, without a second serialization.
|
|
293
|
+
|
|
294
|
+
```ts docs-check
|
|
295
|
+
import { DefaultRoutes } from "@antelopejs/interface-data-api";
|
|
296
|
+
import {
|
|
297
|
+
type Parameters,
|
|
298
|
+
Validation,
|
|
299
|
+
} from "@antelopejs/interface-data-api/components";
|
|
300
|
+
import type { RequestContext } from "@antelopejs/interface-api";
|
|
301
|
+
|
|
302
|
+
const stampedNew = {
|
|
303
|
+
...DefaultRoutes.New,
|
|
304
|
+
func: function (
|
|
305
|
+
this: unknown,
|
|
306
|
+
ctx: RequestContext,
|
|
307
|
+
params: Parameters.NewParameters,
|
|
308
|
+
body: Buffer,
|
|
309
|
+
) {
|
|
310
|
+
const parsed = Validation.ParseBody(body);
|
|
311
|
+
parsed.source = "import";
|
|
312
|
+
return DefaultRoutes.New.func.call(this, ctx, params, parsed);
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
```
|
|
316
|
+
|
|
282
317
|
## Loaded-Model Guards with `WithGetGuard`
|
|
283
318
|
|
|
284
319
|
`DefaultRoutes.WithGetGuard(guard)` returns a Get route that awaits a guard after loading and unlocking the model, but before selecting readable fields and applying response getters. Use it to check a stored discriminator or ownership field that must not appear in the response.
|
|
@@ -300,9 +335,22 @@ const routes = {
|
|
|
300
335
|
};
|
|
301
336
|
```
|
|
302
337
|
|
|
303
|
-
The `GetGuard` callback receives `(ctx, current, params)
|
|
338
|
+
The `GetGuard` callback receives `(ctx, current, params)`. Declare it as a regular function to access the controller through `this`.
|
|
339
|
+
|
|
340
|
+
| Argument | Contents | Use it for |
|
|
341
|
+
| --------- | --------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
342
|
+
| `ctx` | Request context | Request-specific policy. |
|
|
343
|
+
| `current` | Loaded model, including non-readable fields and normal Get joined/computed/foreign data | Inspecting stored ownership or discriminators. |
|
|
344
|
+
| `params` | `Parameters.GetParameters` | The parsed Get parameters. |
|
|
345
|
+
|
|
346
|
+
Treat `current` as read-only: it is not the response DTO and the API does not freeze it.
|
|
347
|
+
|
|
348
|
+
Keep these boundaries when writing a guard:
|
|
304
349
|
|
|
305
|
-
Throw an `HTTPResult`
|
|
350
|
+
- **Deny by throwing.** Throw an `HTTPResult` or use `assert`. A normal return permits the read, including a return value of `false`.
|
|
351
|
+
- **Missing records bypass the guard.** The route returns `404` without invoking it.
|
|
352
|
+
- **Use the already-loaded model.** The guard does not issue a second read or change field-access rules.
|
|
353
|
+
- **Protect each route deliberately.** The guard applies only to the route using it, not list, edit, or delete routes.
|
|
306
354
|
|
|
307
355
|
## Custom Routes
|
|
308
356
|
|
package/docs/4.access-rights.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
The `@Access` decorator controls how fields are read and written through the API. Each field can be set to read-only, write-only, or read-write, providing fine-grained control over data exposure. Access modes can also be overridden on a per-action basis, allowing different permissions for `get`, `list`, `new`, and `edit` operations.
|
|
6
6
|
|
|
7
|
+
## Separate field policy from caller permissions
|
|
8
|
+
|
|
9
|
+
`@Access` answers “may this action read or write this field?”, not “may this user access this record?”. Authenticate the caller and apply record-level policy separately. A write-only password is hidden from responses, but this decorator does not hash it.
|
|
10
|
+
|
|
11
|
+
Build the field policy in this order:
|
|
12
|
+
|
|
13
|
+
1. Set `@Access` for each public field. Undecorated fields remain private.
|
|
14
|
+
2. Add action overrides for differences between creation and editing.
|
|
15
|
+
3. Add `@Listable` for the fields selected by list responses; readable alone does not define a list projection.
|
|
16
|
+
4. Add validators and mandatory-field rules for writable inputs.
|
|
17
|
+
|
|
18
|
+
For example, a display name can be readable and writable, an ID readable only, and a password writable only. Test each direction: attempting to write a read-only field is ignored, while omitting a mandatory writable field is a validation concern.
|
|
19
|
+
|
|
7
20
|
## Access Modes
|
|
8
21
|
|
|
9
22
|
The `AccessMode` enum defines three permission levels:
|
|
@@ -12,11 +25,11 @@ The `AccessMode` enum defines three permission levels:
|
|
|
12
25
|
import { Access, AccessMode } from "@antelopejs/interface-data-api/metadata";
|
|
13
26
|
```
|
|
14
27
|
|
|
15
|
-
| Mode | Value | Description
|
|
16
|
-
| ---------------------- | ----- |
|
|
17
|
-
| `AccessMode.ReadOnly` | `1` | Field
|
|
18
|
-
| `AccessMode.WriteOnly` | `2` | Field is accepted in POST and PUT bodies but never appears in responses
|
|
19
|
-
| `AccessMode.ReadWrite` | `3` | Field is both readable and writable
|
|
28
|
+
| Mode | Value | Description |
|
|
29
|
+
| ---------------------- | ----- | --------------------------------------------------------------------------------------------------- |
|
|
30
|
+
| `AccessMode.ReadOnly` | `1` | Field is readable; list responses also require selection by the pluck mode. POST and PUT ignore it. |
|
|
31
|
+
| `AccessMode.WriteOnly` | `2` | Field is accepted in POST and PUT bodies but never appears in responses |
|
|
32
|
+
| `AccessMode.ReadWrite` | `3` | Field is both readable and writable |
|
|
20
33
|
|
|
21
34
|
## Basic Usage
|
|
22
35
|
|
|
@@ -105,6 +118,8 @@ The `@Access` decorator accepts an optional second argument to override the acce
|
|
|
105
118
|
|
|
106
119
|
The `overrides` object maps action names (`"get"`, `"list"`, `"new"`, `"edit"`) to an `AccessMode` value. Actions not listed in the overrides use the default mode.
|
|
107
120
|
|
|
121
|
+
To read the access a field has in one action, outside the routes (to render a form read-only on edit, for example), call `GetEffectiveMode(field, action)` from `@antelopejs/interface-data-api/metadata` with the field's metadata (`meta.fields[name]`). It is the mode the routes apply.
|
|
122
|
+
|
|
108
123
|
### Example: Writable on Create, Read-Only on Edit
|
|
109
124
|
|
|
110
125
|
```typescript
|
|
@@ -141,7 +156,7 @@ In this configuration:
|
|
|
141
156
|
|
|
142
157
|
## Fields Without `@Access`
|
|
143
158
|
|
|
144
|
-
|
|
159
|
+
Ordinary fields need `@Access` to appear in responses or accept input; `@Listable` alone does not grant access. The derived-field decorators [`@Joined`](./10.joined.md) and [`@Computed`](./11.computed.md) are exceptions: they set read-only access themselves.
|
|
145
160
|
|
|
146
161
|
## Next Steps
|
|
147
162
|
|
package/docs/5.validators.md
CHANGED
|
@@ -118,6 +118,8 @@ declare appointmentDate: string;
|
|
|
118
118
|
|
|
119
119
|
### Async Validation
|
|
120
120
|
|
|
121
|
+
An asynchronous lookup can give early feedback, but a check followed by an insert is not a uniqueness guarantee: concurrent requests can both pass. Enforce uniqueness in the database where supported, and handle write conflicts. `lookupByEmail` below is an application-supplied function.
|
|
122
|
+
|
|
121
123
|
```typescript
|
|
122
124
|
// Check uniqueness against the database
|
|
123
125
|
@Validator(async (value) => {
|
package/docs/6.listable.md
CHANGED
|
@@ -224,13 +224,13 @@ class UserAPI extends DataController(
|
|
|
224
224
|
|
|
225
225
|
### The `noIndex` Option
|
|
226
226
|
|
|
227
|
-
By default, `@Sortable()`
|
|
227
|
+
By default, `@Sortable()` marks the field for indexed ordering. Set `noIndex: true` when it is not indexed. Both paths build an AQL `orderBy` operation; this option changes where sorting is staged, not whether rows are sorted in application memory. The provider determines how the database executes that ordering.
|
|
228
228
|
|
|
229
229
|
```typescript
|
|
230
230
|
@Sortable() // Uses database index for sorting
|
|
231
231
|
declare _id: string;
|
|
232
232
|
|
|
233
|
-
@Sortable({ noIndex: true }) //
|
|
233
|
+
@Sortable({ noIndex: true }) // Non-indexed query ordering
|
|
234
234
|
declare name: string;
|
|
235
235
|
```
|
|
236
236
|
|
package/docs/7.foreign-keys.md
CHANGED
|
@@ -8,19 +8,21 @@ The `@Foreign` decorator establishes relationships between database tables in yo
|
|
|
8
8
|
|
|
9
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
10
|
|
|
11
|
+
Choose the response fields explicitly with `pluck`. A foreign lookup reads the target table, not its API controller: do not rely on the target controller's `@Access` policy to hide passwords or internal fields. The example below selects only the user's ID, name, and email.
|
|
12
|
+
|
|
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). Controller snippets assume the corresponding model and shared imports are already declared.
|
|
14
|
+
|
|
11
15
|
```typescript
|
|
12
16
|
import { Foreign } from "@antelopejs/interface-data-api/metadata";
|
|
13
17
|
import {
|
|
14
18
|
Field,
|
|
15
|
-
Index,
|
|
16
19
|
RegisterTable,
|
|
17
20
|
Relation,
|
|
18
21
|
Table,
|
|
19
22
|
} from "@antelopejs/interface-database-decorators";
|
|
20
23
|
|
|
21
|
-
@RegisterTable("users")
|
|
24
|
+
@RegisterTable("users", "myapp")
|
|
22
25
|
class User extends Table {
|
|
23
|
-
@Index({ primary: true })
|
|
24
26
|
@Field("string")
|
|
25
27
|
declare _id: string;
|
|
26
28
|
|
|
@@ -31,9 +33,8 @@ class User extends Table {
|
|
|
31
33
|
declare email: string;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
@RegisterTable("orders")
|
|
36
|
+
@RegisterTable("orders", "myapp")
|
|
35
37
|
class Order extends Table {
|
|
36
|
-
@Index({ primary: true })
|
|
37
38
|
@Field("string")
|
|
38
39
|
declare _id: string;
|
|
39
40
|
|
|
@@ -61,7 +62,7 @@ class OrderAPI extends DataController(
|
|
|
61
62
|
|
|
62
63
|
@Listable()
|
|
63
64
|
@Access(AccessMode.ReadOnly)
|
|
64
|
-
@Foreign(User)
|
|
65
|
+
@Foreign(User, undefined, false, ["_id", "name", "email"])
|
|
65
66
|
declare userId: User;
|
|
66
67
|
|
|
67
68
|
@Listable()
|
|
@@ -125,9 +126,8 @@ When a field stores an array of foreign key IDs, set the `multi` parameter to `t
|
|
|
125
126
|
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
|
|
|
127
128
|
```typescript
|
|
128
|
-
@RegisterTable("carts")
|
|
129
|
+
@RegisterTable("carts", "myapp")
|
|
129
130
|
class Cart extends Table {
|
|
130
|
-
@Index({ primary: true })
|
|
131
131
|
@Field("string")
|
|
132
132
|
declare _id: string;
|
|
133
133
|
|
|
@@ -136,9 +136,8 @@ class Cart extends Table {
|
|
|
136
136
|
declare items: string[]; // Array of CartItem IDs
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
@RegisterTable("cart_items")
|
|
139
|
+
@RegisterTable("cart_items", "myapp")
|
|
140
140
|
class CartItem extends Table {
|
|
141
|
-
@Index({ primary: true })
|
|
142
141
|
@Field("string")
|
|
143
142
|
declare _id: string;
|
|
144
143
|
|
package/docs/8.filters.md
CHANGED
|
@@ -81,6 +81,14 @@ Combine multiple filters:
|
|
|
81
81
|
GET /users/list?filter_status=active&filter_role=ne:admin
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
Filter one field several times to keep the rows that pass every filter (AND):
|
|
85
|
+
|
|
86
|
+
```http
|
|
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
|
+
|
|
84
92
|
### Numeric and Boolean Filters
|
|
85
93
|
|
|
86
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.
|
|
@@ -148,14 +156,14 @@ type FilterFunction<T, U> = (
|
|
|
148
156
|
) => ValueProxyOrValue<boolean>;
|
|
149
157
|
```
|
|
150
158
|
|
|
151
|
-
| Parameter | Description
|
|
152
|
-
| --------- |
|
|
153
|
-
| `context` | The request context, extended with `this` referencing the controller instance
|
|
154
|
-
| `proxy` | A
|
|
155
|
-
| `key` | The field name being filtered
|
|
156
|
-
| `value` | The filter value from the query parameter
|
|
157
|
-
| `mode` | The comparison operator from the query parameter
|
|
158
|
-
| `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` |
|
|
159
167
|
|
|
160
168
|
### Example: Pattern Matching Filter
|
|
161
169
|
|
|
@@ -168,7 +176,9 @@ function filterByEmailDomain(
|
|
|
168
176
|
mode: string,
|
|
169
177
|
row: ValueProxy<Record<string, any>>,
|
|
170
178
|
) {
|
|
171
|
-
|
|
179
|
+
assert(mode === "eq", 400, "Only equality is supported for email domains");
|
|
180
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
181
|
+
return proxy.match(`@${escaped}$`);
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
@RegisterDataController()
|
|
@@ -199,6 +209,8 @@ Query for users with Gmail addresses:
|
|
|
199
209
|
GET /users/list?filter_email=gmail.com
|
|
200
210
|
```
|
|
201
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
|
+
|
|
202
214
|
### The `useIndex` Parameter
|
|
203
215
|
|
|
204
216
|
The `@Filter` decorator accepts an optional second parameter, `useIndex`, that controls whether the filter can use a database index for optimized lookups.
|
|
@@ -234,6 +246,6 @@ On a controller with a `status` filter, `?filter_status=inactive&filter_role=adm
|
|
|
234
246
|
|
|
235
247
|
## Modifier Integration
|
|
236
248
|
|
|
237
|
-
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.
|
|
238
250
|
|
|
239
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
|
@@ -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.
|