@antelopejs/interface-data-api 0.4.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 +67 -39
- package/dist/components.js +106 -685
- package/dist/components.js.map +1 -1
- package/dist/index.d.ts +57 -68
- package/dist/index.js +114 -100
- package/dist/index.js.map +1 -1
- package/dist/internal/computed.d.ts +6 -0
- package/dist/internal/computed.js +51 -0
- package/dist/internal/computed.js.map +1 -0
- package/dist/internal/controller-types.d.ts +34 -0
- package/dist/internal/controller-types.js +4 -0
- package/dist/internal/controller-types.js.map +1 -0
- package/dist/internal/default-filter.d.ts +4 -0
- package/dist/internal/default-filter.js +29 -0
- package/dist/internal/default-filter.js.map +1 -0
- package/dist/internal/deferred-joined-stream.d.ts +11 -0
- package/dist/internal/deferred-joined-stream.js +125 -0
- package/dist/internal/deferred-joined-stream.js.map +1 -0
- 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 +199 -0
- package/dist/internal/field-types.js +4 -0
- package/dist/internal/field-types.js.map +1 -0
- package/dist/internal/filters.d.ts +6 -0
- package/dist/internal/filters.js +22 -0
- package/dist/internal/filters.js.map +1 -0
- package/dist/internal/foreign.d.ts +6 -0
- package/dist/internal/foreign.js +116 -0
- package/dist/internal/foreign.js.map +1 -0
- package/dist/internal/guards.d.ts +39 -0
- package/dist/internal/guards.js +116 -0
- package/dist/internal/guards.js.map +1 -0
- package/dist/internal/joined.d.ts +28 -0
- package/dist/internal/joined.js +127 -0
- package/dist/internal/joined.js.map +1 -0
- package/dist/internal/list.d.ts +16 -0
- package/dist/internal/list.js +182 -0
- package/dist/internal/list.js.map +1 -0
- package/dist/internal/meta-records.d.ts +19 -0
- package/dist/internal/meta-records.js +78 -0
- package/dist/internal/meta-records.js.map +1 -0
- package/dist/internal/properties.d.ts +4 -0
- package/dist/internal/properties.js +45 -0
- package/dist/internal/properties.js.map +1 -0
- 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/table-reference.d.ts +11 -0
- package/dist/internal/table-reference.js +46 -0
- package/dist/internal/table-reference.js.map +1 -0
- package/dist/internal/tables.d.ts +28 -0
- package/dist/internal/tables.js +48 -0
- package/dist/internal/tables.js.map +1 -0
- package/dist/internal/validation.d.ts +12 -0
- package/dist/internal/validation.js +100 -0
- package/dist/internal/validation.js.map +1 -0
- package/dist/metadata.d.ts +27 -228
- package/dist/metadata.js +84 -213
- package/dist/metadata.js.map +1 -1
- package/dist/tests/components/access_control.test.js +28 -20
- package/dist/tests/components/access_control.test.js.map +1 -1
- package/dist/tests/components/computed.test.js +36 -27
- package/dist/tests/components/computed.test.js.map +1 -1
- package/dist/tests/components/cross_instance.test.js +8 -6
- package/dist/tests/components/cross_instance.test.js.map +1 -1
- package/dist/tests/components/deferred_joined.test.js +57 -54
- 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 +11 -10
- package/dist/tests/components/filter_options.test.js.map +1 -1
- package/dist/tests/components/foreign_computed.test.js +23 -14
- package/dist/tests/components/foreign_computed.test.js.map +1 -1
- package/dist/tests/components/foreign_joined.test.js +31 -27
- package/dist/tests/components/foreign_joined.test.js.map +1 -1
- package/dist/tests/components/joined.test.js +59 -45
- package/dist/tests/components/joined.test.js.map +1 -1
- package/dist/tests/components/listable.test.js +78 -58
- package/dist/tests/components/listable.test.js.map +1 -1
- package/dist/tests/components/localized_sort.test.js +14 -11
- package/dist/tests/components/localized_sort.test.js.map +1 -1
- package/dist/tests/components/mandatory.test.js +25 -25
- package/dist/tests/components/mandatory.test.js.map +1 -1
- package/dist/tests/components/modifier_key.test.js +27 -18
- package/dist/tests/components/modifier_key.test.js.map +1 -1
- package/dist/tests/components/parse_body.test.d.ts +1 -0
- package/dist/tests/components/parse_body.test.js +76 -0
- package/dist/tests/components/parse_body.test.js.map +1 -0
- package/dist/tests/components/parsed_value.test.js +23 -21
- package/dist/tests/components/parsed_value.test.js.map +1 -1
- package/dist/tests/components/partial_edit.test.js +38 -32
- package/dist/tests/components/partial_edit.test.js.map +1 -1
- package/dist/tests/components/per_action_access.test.js +20 -12
- 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 +45 -37
- 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.d.ts +11 -1
- package/dist/tests/helpers/other-instance.js +35 -9
- 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/get_guard.test.js +6 -6
- package/dist/tests/index/get_guard.test.js.map +1 -1
- package/dist/tests/index/inheritance.test.js +14 -9
- 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 +49 -44
- package/dist/tests/index/routes.test.js.map +1 -1
- package/dist/tests/integration.test.d.ts +1 -1
- package/dist/tests/integration.test.js +66 -729
- 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 +35 -0
- package/dist/tests/internal/http.js +56 -0
- package/dist/tests/internal/http.js.map +1 -0
- package/dist/tests/internal/shop-api.d.ts +1 -0
- package/dist/tests/internal/shop-api.js +331 -0
- package/dist/tests/internal/shop-api.js.map +1 -0
- package/dist/tests/internal/shop-schema.d.ts +162 -0
- package/dist/tests/internal/shop-schema.js +387 -0
- package/dist/tests/internal/shop-schema.js.map +1 -0
- package/dist/tests/utils.d.ts +8 -10
- package/dist/tests/utils.js +1 -3
- package/dist/tests/utils.js.map +1 -1
- package/dist/tests/utils.test.js +10 -11
- package/dist/tests/utils.test.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 +156 -134
- package/docs/4.access-rights.md +48 -44
- package/docs/5.validators.md +65 -47
- package/docs/6.listable.md +53 -49
- package/docs/7.foreign-keys.md +55 -48
- package/docs/8.filters.md +65 -54
- package/docs/9.modifiers.md +29 -25
- package/package.json +34 -13
- package/skills/data-api-interface/SKILL.md +13 -3
- package/skills/data-api-interface/cases.json +99 -0
package/dist/tests/utils.test.js
CHANGED
|
@@ -5,22 +5,21 @@ const chai_1 = require("chai");
|
|
|
5
5
|
const utils_1 = require("./utils");
|
|
6
6
|
const WRAPPER_DEPTHS = [1, 2, 5, 20];
|
|
7
7
|
describe("Test utils", () => {
|
|
8
|
-
it("returns the name of the calling function", () =>
|
|
9
|
-
it("skips frames added outside the tests folder", () =>
|
|
8
|
+
it("returns the name of the calling function", () => expectReturnsTheCallerName());
|
|
9
|
+
it("skips frames added outside the tests folder", () => expectSkipsFramesAddedOutsideTheTestsFolder());
|
|
10
10
|
});
|
|
11
|
-
function
|
|
12
|
-
(0, chai_1.expect)((0, utils_1.getFunctionName)()).to.equal("
|
|
11
|
+
function expectReturnsTheCallerName() {
|
|
12
|
+
(0, chai_1.expect)((0, utils_1.getFunctionName)()).to.equal("expectReturnsTheCallerName");
|
|
13
13
|
}
|
|
14
14
|
function nameSeenThroughAsyncContexts(depth) {
|
|
15
15
|
const storage = new node_async_hooks_1.AsyncLocalStorage();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
run,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return run(depth, ...nestedRuns, utils_1.getFunctionName);
|
|
16
|
+
let read = utils_1.getFunctionName;
|
|
17
|
+
for (let store = 0; store < depth; store++) {
|
|
18
|
+
read = storage.run.bind(storage, store, read);
|
|
19
|
+
}
|
|
20
|
+
return read();
|
|
22
21
|
}
|
|
23
|
-
function
|
|
22
|
+
function expectSkipsFramesAddedOutsideTheTestsFolder() {
|
|
24
23
|
for (const depth of WRAPPER_DEPTHS) {
|
|
25
24
|
(0, chai_1.expect)(nameSeenThroughAsyncContexts(depth)).to.equal("nameSeenThroughAsyncContexts");
|
|
26
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.test.js","sourceRoot":"","sources":["../../src/tests/utils.test.ts"],"names":[],"mappings":";;AAAA,uDAAqD;AACrD,+BAA8B;AAE9B,mCAA0C;AAI1C,MAAM,cAAc,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AAErC,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE,
|
|
1
|
+
{"version":3,"file":"utils.test.js","sourceRoot":"","sources":["../../src/tests/utils.test.ts"],"names":[],"mappings":";;AAAA,uDAAqD;AACrD,+BAA8B;AAE9B,mCAA0C;AAI1C,MAAM,cAAc,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AAErC,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE,CAClD,0BAA0B,EAAE,CAAC,CAAC;IAChC,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE,CACrD,2CAA2C,EAAE,CAAC,CAAC;AACnD,CAAC,CAAC,CAAC;AAEH,SAAS,0BAA0B;IACjC,IAAA,aAAM,EAAC,IAAA,uBAAe,GAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;AACnE,CAAC;AAED,SAAS,4BAA4B,CAAC,KAAa;IACjD,MAAM,OAAO,GAAG,IAAI,oCAAiB,EAAU,CAAC;IAChD,IAAI,IAAI,GAAe,uBAAe,CAAC;IACvC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;QAC3C,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAKrB,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,IAAI,EAAE,CAAC;AAChB,CAAC;AAED,SAAS,2CAA2C;IAClD,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;QACnC,IAAA,aAAM,EAAC,4BAA4B,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAClD,8BAA8B,CAC/B,CAAC;IACJ,CAAC;AACH,CAAC"}
|
package/docs/1.introduction.md
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Expose database records through an API
|
|
3
|
+
description: Build a first data controller that serves CRUD routes for a users table, then check which fields it reads and writes.
|
|
4
|
+
mode: tutorial
|
|
5
|
+
---
|
|
2
6
|
|
|
3
7
|
## Overview
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
In this tutorial, we expose a `users` table through five CRUD routes with `@antelopejs/interface-data-api`. A data controller connects the table model to HTTP handlers. Its metadata decides which fields clients can read, write, list and filter.
|
|
6
10
|
|
|
7
11
|
## Build one resource first
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
2. **Choose the routes.** Start with the [route set](./3.routes.md) your clients need, rather than exposing every write operation by default.
|
|
11
|
-
3. **Set the field policy.** Add [access rights](./4.access-rights.md), [validators](./5.validators.md), and [list fields](./6.listable.md). Field permissions are not user authentication or row-level authorization.
|
|
12
|
-
4. **Check one read and one rejected write.** Confirm the response excludes private fields and that invalid input cannot reach storage. Then add [relationships](./7.foreign-keys.md) and [filters](./8.filters.md).
|
|
13
|
+
We build one resource in four steps.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
1. **Prepare the database.** Define a table and register its schema with database-decorators. The AntelopeJS project needs a database module and an API module.
|
|
16
|
+
2. **Choose the routes.** Start with the [route set](./3.routes.md) your clients need, instead of exposing every write route by default.
|
|
17
|
+
3. **Set the field policy.** Add [access rights](./4.access-rights.md), [validators](./5.validators.md) and [list fields](./6.listable.md). Field permissions do not authenticate users and do not authorize access to rows.
|
|
18
|
+
4. **Check one read and one rejected write.** Confirm that the response leaves out private fields and that invalid input never reaches storage. Then add [relationships](./7.foreign-keys.md) and [filters](./8.filters.md).
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
## Quick start
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
The example declares all five CRUD routes for a `users` table. It assumes a configured database module and an existing `my-database` instance. Import the table declaration, then `await RegisterSchema("myapp")` when the declaring module initializes. Wait for that setup to finish before the module serves requests.
|
|
23
|
+
|
|
24
|
+
This example sets a field policy. It is not a production account system. `AccessMode.WriteOnly` keeps the password out of responses, but it does not hash the password or authenticate requests. Add a [database modifier](./9.modifiers.md) and your authentication policy before you accept real credentials.
|
|
19
25
|
|
|
20
26
|
```ts docs-check
|
|
21
27
|
import { Controller } from "@antelopejs/interface-api";
|
|
@@ -40,7 +46,6 @@ import {
|
|
|
40
46
|
Table,
|
|
41
47
|
} from "@antelopejs/interface-database-decorators";
|
|
42
48
|
|
|
43
|
-
// Define the database table
|
|
44
49
|
@RegisterTable("users", "myapp")
|
|
45
50
|
class User extends Table {
|
|
46
51
|
@Field("string")
|
|
@@ -57,10 +62,8 @@ class User extends Table {
|
|
|
57
62
|
declare password: string;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
// Create the database model
|
|
61
65
|
class UserModel extends BasicDataModel(User, "users") {}
|
|
62
66
|
|
|
63
|
-
// Create the data controller
|
|
64
67
|
@RegisterDataController()
|
|
65
68
|
class UserAPI extends DataController(
|
|
66
69
|
User,
|
|
@@ -91,38 +94,38 @@ class UserAPI extends DataController(
|
|
|
91
94
|
}
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
The controller serves these REST endpoints.
|
|
95
98
|
|
|
96
|
-
| Method | Endpoint | Description
|
|
97
|
-
| ------ | ----------------------- |
|
|
98
|
-
| GET | `/users/get?id=<id>` |
|
|
99
|
-
| GET | `/users/list?limit=10` | List users
|
|
100
|
-
| POST | `/users/new` | Create a
|
|
101
|
-
| PUT | `/users/edit?id=<id>` | Update
|
|
102
|
-
| DELETE | `/users/delete?id=<id>` | Delete a user
|
|
99
|
+
| Method | Endpoint | Description |
|
|
100
|
+
| ------ | ----------------------- | -------------------- |
|
|
101
|
+
| GET | `/users/get?id=<id>` | Read one user |
|
|
102
|
+
| GET | `/users/list?limit=10` | List users, one page |
|
|
103
|
+
| POST | `/users/new` | Create a user |
|
|
104
|
+
| PUT | `/users/edit?id=<id>` | Update a user |
|
|
105
|
+
| DELETE | `/users/delete?id=<id>` | Delete a user |
|
|
103
106
|
|
|
104
107
|
### Check the field policy
|
|
105
108
|
|
|
106
|
-
Use disposable data and your
|
|
109
|
+
Use disposable data and the base URL of your API module. First, create a user with `POST /users/new` and a body that holds `name`, `email` and a dummy `password`. The response is the created user, with its `_id` and without `password`. Use that `_id` in the next checks.
|
|
107
110
|
|
|
108
|
-
1. `GET /users/get?id=<id>` returns the readable fields
|
|
109
|
-
2. `GET /users/list?limit=10`
|
|
110
|
-
3. `PUT /users/edit?id=<id>` with `{ "name": "Updated" }`
|
|
111
|
+
1. `GET /users/get?id=<id>` returns the readable fields. You see `_id`, `email` and `name`, and never `password`.
|
|
112
|
+
2. `GET /users/list?limit=10` returns `_id`, `name` and `email` for each entry of `results`.
|
|
113
|
+
3. `PUT /users/edit?id=<id>` with `{ "name": "Updated" }` keeps the email you did not send. With `{ "email": "" }`, the route answers `400`, because `email` is mandatory on `edit`.
|
|
111
114
|
|
|
112
|
-
`@Mandatory` does not check email syntax or password strength. Add [validators](./5.validators.md) before
|
|
115
|
+
`@Mandatory` does not check the email syntax or the password strength. Add [validators](./5.validators.md) before you accept untrusted input.
|
|
113
116
|
|
|
114
117
|
## Core guides
|
|
115
118
|
|
|
116
|
-
- [Data
|
|
117
|
-
- [Routes](./3.routes.md)
|
|
118
|
-
- [Access
|
|
119
|
-
- [Validators](./5.validators.md)
|
|
120
|
-
- [Listable
|
|
119
|
+
- [Data controllers](./2.data-controllers.md). Create and configure data controllers.
|
|
120
|
+
- [Routes](./3.routes.md). Default routes, custom routes and route options.
|
|
121
|
+
- [Access rights](./4.access-rights.md). Which fields each action reads and writes.
|
|
122
|
+
- [Validators](./5.validators.md). Field validation and mandatory fields.
|
|
123
|
+
- [Listable fields](./6.listable.md). Pagination, pluck modes and sorting.
|
|
121
124
|
|
|
122
125
|
## Relationships and advanced queries
|
|
123
126
|
|
|
124
|
-
- [Foreign
|
|
125
|
-
- [Filters](./8.filters.md)
|
|
126
|
-
- [Modifiers](./9.modifiers.md)
|
|
127
|
-
- [Joined
|
|
128
|
-
- [Computed
|
|
127
|
+
- [Foreign keys](./7.foreign-keys.md). Relations between tables.
|
|
128
|
+
- [Filters](./8.filters.md). Filters on list endpoints.
|
|
129
|
+
- [Modifiers](./9.modifiers.md). Data transformation with database-decorators modifiers.
|
|
130
|
+
- [Joined fields](./10.joined.md). A remote field flattened onto each row, sortable and filterable in the database.
|
|
131
|
+
- [Computed fields](./11.computed.md). Values computed in the database, sortable and pageable there.
|
package/docs/10.joined.md
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Joined fields
|
|
3
|
+
description: Flatten one field of another table onto each row with @Joined, so list requests sort and filter on it in the database.
|
|
4
|
+
mode: reference
|
|
5
|
+
---
|
|
2
6
|
|
|
3
7
|
## Overview
|
|
4
8
|
|
|
5
|
-
The `@Joined` decorator
|
|
9
|
+
The `@Joined` decorator copies one scalar field from another table onto each row at query time. `@Foreign` nests the whole referenced record under the field. `@Joined` puts one remote field at the top level of the row, before the list query sorts and filters. The database can therefore sort, filter, and search on the joined field.
|
|
6
10
|
|
|
7
|
-
Joined fields are read-only
|
|
11
|
+
Joined fields are meant to be read-only. A lookup sets the value at query time, and the controller's own table never stores it. `@Joined` sets the field's access mode to read-only when it runs. An `@Access` written above it runs later and replaces that mode, so do not combine the two. `@Joined` also makes the field's sort non-indexed, whatever the decorator order.
|
|
8
12
|
|
|
9
|
-
## Basic
|
|
13
|
+
## Basic usage
|
|
10
14
|
|
|
11
|
-
Apply `@Joined` to a field that
|
|
15
|
+
Apply `@Joined` to a field that takes its value from a remote table. The local row must hold a key (`localKey`) that matches the remote table's `remoteIndex`.
|
|
12
16
|
|
|
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
|
|
17
|
+
Use [foreign keys](./7.foreign-keys.md) when the response needs a nested record instead. The snippets below assume the controller imports and the model from the [quick start](./1.introduction.md#quick-start), with `myapp` registered before the module serves requests.
|
|
14
18
|
|
|
15
|
-
```typescript
|
|
19
|
+
```typescript fragment
|
|
16
20
|
import { Joined } from "@antelopejs/interface-data-api/metadata";
|
|
17
21
|
import {
|
|
18
22
|
Field,
|
|
@@ -84,25 +88,27 @@ A GET request to `/orders/get?id=order-123` returns the joined value at the top
|
|
|
84
88
|
}
|
|
85
89
|
```
|
|
86
90
|
|
|
87
|
-
## Decorator
|
|
91
|
+
## Decorator parameters
|
|
88
92
|
|
|
89
|
-
```typescript
|
|
93
|
+
```typescript fragment
|
|
90
94
|
@Joined({ table, localKey, remoteField, remoteIndex?, schema? })
|
|
91
95
|
```
|
|
92
96
|
|
|
93
97
|
| Option | Type | Required | Default | Description |
|
|
94
98
|
| ------------- | -------------------------- | -------- | ----------------- | -------------------------------------------------------------------------------------------------- |
|
|
95
|
-
| `table` | `Class<Table>` or `string` | Yes |
|
|
96
|
-
| `localKey` | `string` | Yes |
|
|
97
|
-
| `remoteField` | `string` | Yes |
|
|
99
|
+
| `table` | `Class<Table>` or `string` | Yes | None | The remote table class or registered table name string |
|
|
100
|
+
| `localKey` | `string` | Yes | None | The field on the local row holding the value matched against the remote table |
|
|
101
|
+
| `remoteField` | `string` | Yes | None | The remote field whose value is flattened onto the row |
|
|
98
102
|
| `remoteIndex` | `string` | No | `"_id"` | The index on the remote table to match `localKey` against |
|
|
99
103
|
| `schema` | `string` | No | Controller schema | Name of the schema the remote table is registered in, when it differs from the controller's schema |
|
|
100
104
|
|
|
101
|
-
|
|
105
|
+
When a list mode includes a joined field, the mode also selects the field's `localKey`.
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
## Sorting and filtering
|
|
104
108
|
|
|
105
|
-
|
|
109
|
+
The list query applies the join before it sorts and filters, so a joined field behaves like a stored column. Combine `@Joined` with `@Sortable` or `@Filter` to sort or filter on the joined value.
|
|
110
|
+
|
|
111
|
+
```typescript fragment
|
|
106
112
|
@Sortable()
|
|
107
113
|
@Filter()
|
|
108
114
|
@Joined({
|
|
@@ -113,30 +119,35 @@ Because the join is applied to the underlying query before sorting and filtering
|
|
|
113
119
|
declare userName: string;
|
|
114
120
|
```
|
|
115
121
|
|
|
116
|
-
|
|
122
|
+
A joined field always sorts without a database index, because its value comes from a lookup at query time and not from a stored column. `@Sortable` on a joined field is always non-indexed.
|
|
117
123
|
|
|
118
|
-
## Deferred
|
|
124
|
+
## Deferred resolution on lists
|
|
119
125
|
|
|
120
|
-
The default list route
|
|
126
|
+
The default list route schedules the lookups itself. Skip to [cross-schema joins](#cross-schema-joins) unless you are building a custom route with `Query.List`.
|
|
121
127
|
|
|
122
|
-
{width="480"}
|
|
128
|
+
{width="480"}
|
|
123
129
|
|
|
124
130
|
### Advanced: composing `Query.List`
|
|
125
131
|
|
|
126
|
-
`Query.List`
|
|
132
|
+
`Query.List` resolves the joined fields that the request's filters read before it counts the rows. It resolves the joined fields that the sort reads after the count and before the sort. It defers the remaining display-only joined fields. The returned stream resolves them as soon as an operation can read rows, so destructuring `[query, total]` alone always gives rows with every `@Joined` field resolved.
|
|
127
133
|
|
|
128
|
-
- Paging the stream (`slice`, `nth`)
|
|
129
|
-
- A bare `count()`
|
|
130
|
-
- `changes()` attaches to the raw stream,
|
|
131
|
-
- Any other operation
|
|
134
|
+
- Paging the stream (`slice`, `nth`) adds the lookups after the page boundary, so they run only on the returned rows.
|
|
135
|
+
- A bare `count()` skips the lookups, and so does a field aggregate such as `sum`, `min`, or `distinct(field)` on a field that is not joined. A left-join lookup never changes the row count or the other fields.
|
|
136
|
+
- `changes()` attaches to the raw stream, because change feeds cannot carry the join lookups.
|
|
137
|
+
- Any other operation, such as `filter`, `orderBy`, or `map`, resolves the lookups first, because it may read the joined values.
|
|
132
138
|
|
|
133
|
-
The
|
|
139
|
+
The lookups therefore run on the page only when paging is the first operation the caller applies. An operation on the whole stream before paging resolves the joined fields over the whole matched set. Such operations include a filter, a sort, and the `lookup` that `Query.Foreign` adds for a `@Foreign` field. The lookups still stay out of the count. A caller that wants them on the page only must pass `{ exposeDeferredJoined: true }` to `Query.List` and resolve them after the slice. `Query.List` then returns the raw stream unchanged, and the third tuple element names the joined fields to resolve with `Query.Joined`. The default list route works this way, which also lets it limit the page lookups to the plucked fields.
|
|
134
140
|
|
|
135
141
|
### Reading a page: `Query.ListPage`
|
|
136
142
|
|
|
137
|
-
A custom list route over `Query.List` reads its page with `Query.ListPage`
|
|
143
|
+
A custom list route over `Query.List` reads its page with `Query.ListPage` instead of chaining the steps itself. `Query.ListPage` slices the stream, then resolves these on the page only, in this order:
|
|
144
|
+
|
|
145
|
+
1. The deferred joined fields.
|
|
146
|
+
2. The `@Computed` fields that the filters and the sort did not already merge.
|
|
147
|
+
3. The `@Foreign` relations. They come last because a computed expression may read a relation's key.
|
|
148
|
+
4. The pluck.
|
|
138
149
|
|
|
139
|
-
```typescript
|
|
150
|
+
```typescript fragment
|
|
140
151
|
const [query, total, deferredJoined] = Query.List(
|
|
141
152
|
this,
|
|
142
153
|
meta,
|
|
@@ -158,11 +169,11 @@ const rows = await Query.ListPage(model.database, meta, query, {
|
|
|
158
169
|
});
|
|
159
170
|
```
|
|
160
171
|
|
|
161
|
-
## Cross-
|
|
172
|
+
## Cross-schema joins
|
|
162
173
|
|
|
163
|
-
By default, the remote table
|
|
174
|
+
By default, the Data API looks for the remote table in the schema of the controller's table. The `schema` option joins a table registered in a different schema. The join goes through the remote table's class, against the default instance of the schema that `RegisterSchema` bound it to. Register the remote class with `@RegisterTable` before you declare the field. A string table name with no registered class in that schema throws at declaration.
|
|
164
175
|
|
|
165
|
-
```typescript
|
|
176
|
+
```typescript fragment
|
|
166
177
|
@Joined({
|
|
167
178
|
table: "users",
|
|
168
179
|
localKey: "userId",
|
|
@@ -172,17 +183,17 @@ By default, the remote table is resolved in the same schema as the controller's
|
|
|
172
183
|
declare userName: string;
|
|
173
184
|
```
|
|
174
185
|
|
|
175
|
-
When a table class
|
|
186
|
+
When you pass a table class, the Data API reads its schema from its `@RegisterTable` metadata. If the class metadata and the `schema` option name different schemas, the declaration throws.
|
|
176
187
|
|
|
177
|
-
## Joined
|
|
188
|
+
## `@Joined` compared with `@Foreign`
|
|
178
189
|
|
|
179
|
-
| Aspect | `@Joined`
|
|
180
|
-
| ------------------- |
|
|
181
|
-
| Result shape | A single scalar value flattened onto the row
|
|
182
|
-
| Access | Read-only
|
|
183
|
-
| Sort
|
|
184
|
-
| Multiple references | One remote field per decorator
|
|
190
|
+
| Aspect | `@Joined` | `@Foreign` |
|
|
191
|
+
| ------------------- | ----------------------------------------------- | ------------------------------------------------------------------------- |
|
|
192
|
+
| Result shape | A single scalar value flattened onto the row | The full referenced record, or its plucked fields, nested under the field |
|
|
193
|
+
| Access | Read-only, unless a later `@Access` replaces it | Set with `@Access` |
|
|
194
|
+
| Sort and filter | In the database, join applied before both | No sort or filter on remote fields |
|
|
195
|
+
| Multiple references | One remote field per decorator | Single or array (`multi`) references |
|
|
185
196
|
|
|
186
|
-
## Next
|
|
197
|
+
## Next steps
|
|
187
198
|
|
|
188
|
-
|
|
199
|
+
To merge any in-database expression onto rows, see [Computed fields](./11.computed.md). To see how modifiers transform data in the Data API, see [Modifiers](./9.modifiers.md).
|
package/docs/11.computed.md
CHANGED
|
@@ -1,19 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Computed fields
|
|
3
|
+
description: Compute a field in the database with @Computed, from the row or from a per-row subquery, and sort, filter, and page on it.
|
|
4
|
+
mode: reference
|
|
5
|
+
---
|
|
2
6
|
|
|
3
7
|
## Overview
|
|
4
8
|
|
|
5
|
-
The `@Computed` decorator declares a field whose value
|
|
9
|
+
The `@Computed` decorator declares a field whose value the database computes from an expression and merges onto each row at query time. The expression can compute from other fields of the same row, or run a per-row subquery such as an aggregate over another table. The merge happens before the list query sorts and filters, so the database sorts, filters, and pages computed fields. The API never fetches every row or sorts in memory.
|
|
6
10
|
|
|
7
|
-
Computed fields are read-only
|
|
11
|
+
Computed fields are meant to be read-only. The query produces the value, and the controller's table never stores it. `@Computed` sets the field's access mode to read-only when it runs. An `@Access` written above it runs later and replaces that mode, so do not combine the two. `@Computed` also makes the field's sort non-indexed, whatever the decorator order.
|
|
8
12
|
|
|
9
|
-
## Basic
|
|
13
|
+
## Basic usage
|
|
10
14
|
|
|
11
|
-
The expression receives the row proxy and the schema instance of the controller's table. It must return a value built
|
|
15
|
+
The expression receives the row proxy and the schema instance of the controller's table. It must return a value built with the query builder (`ValueProxy` operations or a subquery), not plain JavaScript computed at request time.
|
|
12
16
|
|
|
13
|
-
The
|
|
17
|
+
The example follows the [quick-start controller pattern](./1.introduction.md#quick-start). Register the `myapp` schema before the module serves requests. Check [driver support](#driver-support) before you use aggregate subqueries.
|
|
14
18
|
|
|
15
19
|
```typescript
|
|
16
|
-
import {
|
|
20
|
+
import { Controller } from "@antelopejs/interface-api";
|
|
21
|
+
import {
|
|
22
|
+
DataController,
|
|
23
|
+
DefaultRoutes,
|
|
24
|
+
RegisterDataController,
|
|
25
|
+
} from "@antelopejs/interface-data-api";
|
|
26
|
+
import {
|
|
27
|
+
Access,
|
|
28
|
+
AccessMode,
|
|
29
|
+
Computed,
|
|
30
|
+
Listable,
|
|
31
|
+
ModelReference,
|
|
32
|
+
Sortable,
|
|
33
|
+
} from "@antelopejs/interface-data-api/metadata";
|
|
34
|
+
import {
|
|
35
|
+
BasicDataModel,
|
|
36
|
+
Field,
|
|
37
|
+
Index,
|
|
38
|
+
Model,
|
|
39
|
+
RegisterTable,
|
|
40
|
+
Relation,
|
|
41
|
+
Table,
|
|
42
|
+
} from "@antelopejs/interface-database-decorators";
|
|
17
43
|
|
|
18
44
|
@RegisterTable("groups", "myapp")
|
|
19
45
|
class Group extends Table {
|
|
@@ -35,6 +61,8 @@ class Device extends Table {
|
|
|
35
61
|
declare group_id: string;
|
|
36
62
|
}
|
|
37
63
|
|
|
64
|
+
class GroupModel extends BasicDataModel(Group, "groups") {}
|
|
65
|
+
|
|
38
66
|
@RegisterDataController()
|
|
39
67
|
class GroupAPI extends DataController(
|
|
40
68
|
Group,
|
|
@@ -56,7 +84,11 @@ class GroupAPI extends DataController(
|
|
|
56
84
|
// Aggregate: number of devices referencing this group, computed in DB
|
|
57
85
|
@Listable()
|
|
58
86
|
@Computed(
|
|
59
|
-
(row, db) =>
|
|
87
|
+
(row, db) =>
|
|
88
|
+
db
|
|
89
|
+
.table("devices")
|
|
90
|
+
.filter((device) => device.key("group_id").eq(row.key("_id")))
|
|
91
|
+
.count(),
|
|
60
92
|
{ default: 0 },
|
|
61
93
|
)
|
|
62
94
|
@Sortable()
|
|
@@ -64,11 +96,13 @@ class GroupAPI extends DataController(
|
|
|
64
96
|
|
|
65
97
|
// Row-local expression
|
|
66
98
|
@Listable()
|
|
67
|
-
@Computed((row) =>
|
|
99
|
+
@Computed((row) => row.key("name").concat("!"))
|
|
68
100
|
declare display_name: string;
|
|
69
101
|
}
|
|
70
102
|
```
|
|
71
103
|
|
|
104
|
+
The row proxy types every field as a `FieldValue`. The subquery compares each device's `group_id` with the row's `_id` through `eq`, which accepts that type. This filter checks every device for each row and does not use the `group_id` index. `getAll(row.key("_id"), "group_id")` would use the index, but `getAll` does not accept a row key typed as a `FieldValue`, so it compiles only with a cast.
|
|
105
|
+
|
|
72
106
|
A GET request to `/groups/get?id=group-123` returns the computed values at the top level:
|
|
73
107
|
|
|
74
108
|
```json
|
|
@@ -80,69 +114,79 @@ A GET request to `/groups/get?id=group-123` returns the computed values at the t
|
|
|
80
114
|
}
|
|
81
115
|
```
|
|
82
116
|
|
|
83
|
-
## Decorator
|
|
117
|
+
## Decorator parameters
|
|
84
118
|
|
|
85
|
-
```typescript
|
|
119
|
+
```typescript fragment
|
|
86
120
|
@Computed(expr, options?)
|
|
87
121
|
```
|
|
88
122
|
|
|
89
|
-
| Parameter | Type | Required | Description
|
|
90
|
-
| ----------------- | ------------------------- | -------- |
|
|
91
|
-
| `expr` | `(row, db) => expression` | Yes | Expression receiving the row proxy and the schema instance
|
|
92
|
-
| `options.default` | `
|
|
123
|
+
| Parameter | Type | Required | Description |
|
|
124
|
+
| ----------------- | ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
125
|
+
| `expr` | `(row, db) => expression` | Yes | Expression receiving the row proxy and the schema instance. Returns a `ValueProxy` expression, a subquery (`Query`), or a constant |
|
|
126
|
+
| `options.default` | `FieldValue` | No | Fallback merged over the computed value when the expression yields `null` |
|
|
93
127
|
|
|
94
128
|
### The `default` option
|
|
95
129
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
@Computed(
|
|
100
|
-
|
|
101
|
-
|
|
130
|
+
An aggregate subquery over an empty set yields `null` instead of its zero value. A `count()` over a group with no devices returns `null`, not `0`. Calling `.default(0)` inside the subquery expression does not help, because the aggregation produces no document to apply the default to. The `options.default` value works because the Data API applies it to the row, after the merge:
|
|
131
|
+
|
|
132
|
+
```typescript fragment
|
|
133
|
+
@Computed(
|
|
134
|
+
(row, db) =>
|
|
135
|
+
db
|
|
136
|
+
.table("devices")
|
|
137
|
+
.filter((device) => device.key("group_id").eq(row.key("_id")))
|
|
138
|
+
.count(),
|
|
139
|
+
{ default: 0 },
|
|
140
|
+
)
|
|
102
141
|
declare devices_count: number;
|
|
103
142
|
```
|
|
104
143
|
|
|
105
|
-
Without it,
|
|
144
|
+
Without it, a row with an empty aggregate holds `null`. An equality filter such as `filter_devices_count=eq:0` then misses the row, and the sort order depends on BSON type ordering.
|
|
106
145
|
|
|
107
|
-
## Sorting,
|
|
146
|
+
## Sorting, filtering, and pagination
|
|
108
147
|
|
|
109
|
-
Computed fields combine with `@Sortable` and `@Filter` like
|
|
148
|
+
Computed fields combine with `@Sortable` and `@Filter` like stored columns. The list query merges each computed field only where a step needs its value:
|
|
110
149
|
|
|
111
|
-
- A computed field
|
|
112
|
-
- A computed field used as the sort key is merged after the total count
|
|
113
|
-
- A computed field requested only for display
|
|
150
|
+
- A computed field that an active filter reads is merged before the filters run.
|
|
151
|
+
- A computed field used as the sort key is merged after the total count and before the non-indexed `orderBy`, so the count does not compute the aggregate.
|
|
152
|
+
- A computed field requested only for display, in the pluck and neither sorted nor filtered, is merged after pagination (`slice`). The database computes it for the returned page only.
|
|
114
153
|
|
|
115
|
-
Sorting and pagination both stay in the database
|
|
154
|
+
Sorting and pagination both stay in the database. Sorting by a computed aggregate with `limit` and `offset` gives correct pages and the same `total` on every page.
|
|
116
155
|
|
|
117
|
-
|
|
156
|
+
A computed field always sorts without a database index, because its value is not a stored column. `@Sortable` on a computed field is always non-indexed.
|
|
118
157
|
|
|
119
158
|
### Filtering on numeric computed fields
|
|
120
159
|
|
|
121
|
-
Filter values arrive as strings from the query string.
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
@Computed(
|
|
125
|
-
|
|
126
|
-
|
|
160
|
+
Filter values arrive as strings from the query string. Import the validated `numberFilter` from [Numeric and boolean filters](./8.filters.md#numeric-and-boolean-filters) into the controller file. It rejects invalid numbers and applies the requested comparison operator. `parseFloat` alone accepts partial input such as `"3devices"`.
|
|
161
|
+
|
|
162
|
+
```typescript fragment
|
|
163
|
+
@Computed(
|
|
164
|
+
(row, db) =>
|
|
165
|
+
db
|
|
166
|
+
.table("devices")
|
|
167
|
+
.filter((device) => device.key("group_id").eq(row.key("_id")))
|
|
168
|
+
.count(),
|
|
169
|
+
{ default: 0 },
|
|
170
|
+
)
|
|
127
171
|
@Filter(numberFilter, false)
|
|
128
172
|
declare devices_count: number;
|
|
129
173
|
```
|
|
130
174
|
|
|
131
|
-
## Computed
|
|
175
|
+
## `@Computed` compared with `@Joined`
|
|
132
176
|
|
|
133
|
-
| Aspect
|
|
134
|
-
|
|
|
135
|
-
| Source
|
|
136
|
-
| Typical use
|
|
137
|
-
| Access
|
|
138
|
-
| Sort
|
|
177
|
+
| Aspect | `@Computed` | `@Joined` |
|
|
178
|
+
| --------------- | ---------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
179
|
+
| Source | Any expression: a computation over the row or a per-row subquery | One scalar field from another table matched by a foreign key |
|
|
180
|
+
| Typical use | Aggregates (`count`, `sum`), derived values | Flattening a field of a referenced record |
|
|
181
|
+
| Access | Read-only, unless a later `@Access` replaces it | Read-only, unless a later `@Access` replaces it |
|
|
182
|
+
| Sort and filter | In the database, merged only where a step needs the value | In the database, join applied before sort and filter |
|
|
139
183
|
|
|
140
|
-
|
|
184
|
+
For one field copied from one referenced record, `@Joined` is simpler. `@Computed` applies the same merge to any expression.
|
|
141
185
|
|
|
142
|
-
## Driver
|
|
186
|
+
## Driver support
|
|
143
187
|
|
|
144
|
-
The aggregate form
|
|
188
|
+
The aggregate form needs a database driver that turns a per-row subquery inside a merge into a correlated lookup. On MongoDB, the lookup is a `$lookup` with `let` and `pipeline`. The `@antelopejs/mongodb` changelog names the release that added this translation. Row-local expressions use standard expression compilation and work on any driver.
|
|
145
189
|
|
|
146
|
-
## Next
|
|
190
|
+
## Next steps
|
|
147
191
|
|
|
148
|
-
|
|
192
|
+
For the decorator that copies one field from a referenced record, see [Joined fields](./10.joined.md).
|