@antelopejs/interface-data-api 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,128 @@
1
+ # Data API Documentation
2
+
3
+ ## Overview
4
+
5
+ The Data API interface provides a declarative approach to building RESTful APIs with automatic CRUD operations. It connects your database models directly to API endpoints, handling validation, access control, foreign key resolution, and pagination with minimal boilerplate.
6
+
7
+ ## Key Features
8
+
9
+ - **Database-to-API Mapping** - Expose database tables as REST endpoints through data controllers.
10
+ - **Default CRUD Routes** - Built-in `get`, `list`, `new`, `edit`, and `delete` operations that work out of the box.
11
+ - **Access Control** - Define read-only, write-only, or read-write permissions per field, with per-action overrides.
12
+ - **Field Validation** - Attach custom validation functions to individual fields and enforce mandatory fields per operation.
13
+ - **Listable Fields and Pagination** - Control which fields appear in list responses, with support for multiple pluck modes and paginated results.
14
+ - **Sorting** - Mark fields as sortable to enable ordering in list queries.
15
+ - **Foreign Key Resolution** - Automatically resolve relationships between tables with optional field plucking.
16
+ - **Joined Fields** - Flatten a scalar field from another table onto each row so it can be sorted, filtered, and searched natively in the database.
17
+ - **Computed Fields** - Merge in-database computed expressions (row-local values or aggregates over other tables) onto each row, natively sortable, filterable, and pageable.
18
+ - **Filters** - Add default or custom filter logic to list endpoints using query parameters.
19
+ - **Modifier Integration** - Automatic data transformation (encryption, hashing, localization) through database-decorators.
20
+
21
+ ## Dependencies
22
+
23
+ This interface relies on the following AntelopeJS packages:
24
+
25
+ - [@antelopejs/interface-api](https://github.com/AntelopeJS/interface-api) - API controllers and route registration.
26
+ - [@antelopejs/interface-database-decorators](https://github.com/AntelopeJS/interface-database-decorators) - Database table definitions and models.
27
+
28
+ ## Quick Start
29
+
30
+ The following example creates a full CRUD API for a `users` table.
31
+
32
+ ```typescript
33
+ import { Controller } from "@antelopejs/interface-api";
34
+ import {
35
+ DataController,
36
+ DefaultRoutes,
37
+ RegisterDataController,
38
+ } from "@antelopejs/interface-data-api";
39
+ import {
40
+ Access,
41
+ AccessMode,
42
+ Listable,
43
+ Mandatory,
44
+ ModelReference,
45
+ } from "@antelopejs/interface-data-api/metadata";
46
+ import {
47
+ BasicDataModel,
48
+ Field,
49
+ Index,
50
+ Model,
51
+ RegisterTable,
52
+ Table,
53
+ } from "@antelopejs/interface-database-decorators";
54
+
55
+ // Define the database table
56
+ @RegisterTable("users")
57
+ class User extends Table {
58
+ @Index({ primary: true })
59
+ @Field("string")
60
+ declare _id: string;
61
+
62
+ @Index()
63
+ @Field("string")
64
+ declare email: string;
65
+
66
+ @Field("string")
67
+ declare name: string;
68
+
69
+ @Field("string")
70
+ declare password: string;
71
+ }
72
+
73
+ // Create the database model
74
+ class UserModel extends BasicDataModel(User, "users") {}
75
+
76
+ // Create the data controller
77
+ @RegisterDataController()
78
+ class UserAPI extends DataController(
79
+ User,
80
+ DefaultRoutes.All,
81
+ Controller("/users"),
82
+ ) {
83
+ @ModelReference()
84
+ @Model(UserModel, "my-database")
85
+ declare userModel: UserModel;
86
+
87
+ @Listable()
88
+ @Access(AccessMode.ReadOnly)
89
+ declare _id: string;
90
+
91
+ @Listable()
92
+ @Access(AccessMode.ReadWrite)
93
+ @Mandatory("new", "edit")
94
+ declare email: string;
95
+
96
+ @Listable()
97
+ @Access(AccessMode.ReadWrite)
98
+ @Mandatory("new")
99
+ declare name: string;
100
+
101
+ @Access(AccessMode.WriteOnly)
102
+ @Mandatory("new")
103
+ declare password: string;
104
+ }
105
+ ```
106
+
107
+ This generates the following REST endpoints:
108
+
109
+ | Method | Endpoint | Description |
110
+ | ------ | -------- | ----------- |
111
+ | GET | `/users/get?id=<id>` | Retrieve a single user |
112
+ | GET | `/users/list?limit=10` | List users with pagination |
113
+ | POST | `/users/new` | Create a new user |
114
+ | PUT | `/users/edit?id=<id>` | Update an existing user |
115
+ | DELETE | `/users/delete?id=<id>` | Delete a user |
116
+
117
+ ## Documentation Sections
118
+
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) - Control field read/write permissions
122
+ - [Validators](./5.validators.md) - Field-level validation and mandatory fields
123
+ - [Listable Fields](./6.listable.md) - Pagination, pluck modes, and sorting
124
+ - [Foreign Keys](./7.foreign-keys.md) - Establish relationships between tables
125
+ - [Filters](./8.filters.md) - Add filtering capabilities to list endpoints
126
+ - [Modifiers](./9.modifiers.md) - Automatic data transformation with database-decorators
127
+ - [Joined Fields](./10.joined.md) - Flatten a remote field onto rows for native sort/filter
128
+ - [Computed Fields](./11.computed.md) - In-database computed values, sortable and pageable natively
@@ -0,0 +1,146 @@
1
+ # Joined Fields
2
+
3
+ ## Overview
4
+
5
+ The `@Joined` decorator imports a single scalar field from another table and flattens it onto each row at query time. Unlike `@Foreign`, which embeds the full referenced record as a nested object in the response, `@Joined` copies one remote field to the top level of the row before sorting and filtering are applied. This makes the joined field natively sortable, filterable, and searchable in the database.
6
+
7
+ Joined fields are read-only: the value is set by a lookup at query time and is never persisted on the controller's own table. Declaring a field as joined automatically sets its access mode to read-only and forces non-indexed sorting.
8
+
9
+ ## Basic Usage
10
+
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
+
13
+ ```typescript
14
+ import { Joined } from "@antelopejs/interface-data-api/metadata";
15
+ import {
16
+ Field,
17
+ Index,
18
+ RegisterTable,
19
+ Relation,
20
+ Table,
21
+ } from "@antelopejs/interface-database-decorators";
22
+
23
+ @RegisterTable("users")
24
+ class User extends Table {
25
+ @Index({ primary: true })
26
+ @Field("string")
27
+ declare _id: string;
28
+
29
+ @Field("string")
30
+ declare name: string;
31
+ }
32
+
33
+ @RegisterTable("orders")
34
+ class Order extends Table {
35
+ @Index({ primary: true })
36
+ @Field("string")
37
+ declare _id: string;
38
+
39
+ @Field("string")
40
+ @Relation({ to: () => User })
41
+ declare userId: string;
42
+
43
+ @Field("number")
44
+ declare total: number;
45
+ }
46
+
47
+ @RegisterDataController()
48
+ class OrderAPI extends DataController(
49
+ Order,
50
+ DefaultRoutes.All,
51
+ Controller("/orders"),
52
+ ) {
53
+ @ModelReference()
54
+ @Model(OrderModel, "my-database")
55
+ declare orderModel: OrderModel;
56
+
57
+ @Listable()
58
+ @Access(AccessMode.ReadOnly)
59
+ declare _id: string;
60
+
61
+ declare userId: string;
62
+
63
+ // Pull the remote "name" field from the User referenced by userId
64
+ @Listable()
65
+ @Joined({
66
+ table: User,
67
+ localKey: "userId",
68
+ remoteField: "name",
69
+ })
70
+ declare userName: string;
71
+
72
+ @Listable()
73
+ @Access(AccessMode.ReadOnly)
74
+ declare total: number;
75
+ }
76
+ ```
77
+
78
+ A GET request to `/orders/get?id=order-123` returns the joined value at the top level:
79
+
80
+ ```json
81
+ {
82
+ "_id": "order-123",
83
+ "userName": "Bob",
84
+ "total": 99.99
85
+ }
86
+ ```
87
+
88
+ ## Decorator Parameters
89
+
90
+ ```typescript
91
+ @Joined({ table, localKey, remoteField, remoteIndex?, schema? })
92
+ ```
93
+
94
+ | Option | Type | Required | Default | Description |
95
+ | ------ | ---- | -------- | ------- | ----------- |
96
+ | `table` | `Class<Table>` or `string` | Yes | - | The remote table class or registered table name string |
97
+ | `localKey` | `string` | Yes | - | The field on the local row holding the value matched against the remote table |
98
+ | `remoteField` | `string` | Yes | - | The remote field whose value is flattened onto the row |
99
+ | `remoteIndex` | `string` | No | `"_id"` | The index on the remote table to match `localKey` against |
100
+ | `schema` | `string` | No | Controller schema | Name of the schema the remote table is registered in, when it differs from the controller's schema |
101
+
102
+ ## Sorting and Filtering
103
+
104
+ Because the join is applied to the underlying query before sorting and filtering, joined fields behave like native columns. Combine `@Joined` with `@Sortable` or `@Filter` to order or filter by the imported value.
105
+
106
+ ```typescript
107
+ @Sortable()
108
+ @Filter()
109
+ @Joined({
110
+ table: User,
111
+ localKey: "userId",
112
+ remoteField: "name",
113
+ })
114
+ declare userName: string;
115
+ ```
116
+
117
+ Joined fields are always sorted without a database index (`@Sortable` is forced to non-indexed for them), since the value comes from a runtime lookup rather than a stored column.
118
+
119
+ ## Cross-Schema Joins
120
+
121
+ 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 against that schema's default instance.
122
+
123
+ ```typescript
124
+ @Joined({
125
+ table: "users",
126
+ localKey: "userId",
127
+ remoteField: "name",
128
+ schema: "auth",
129
+ })
130
+ declare userName: string;
131
+ ```
132
+
133
+ When a table class is passed, its schema is inferred from its `@RegisterTable` metadata. If both the class metadata and the explicit `schema` option specify a schema and they disagree, an error is thrown.
134
+
135
+ ## Joined vs. Foreign
136
+
137
+ | Aspect | `@Joined` | `@Foreign` |
138
+ | ------ | --------- | ---------- |
139
+ | Result shape | A single scalar value flattened onto the row | The full referenced record (or plucked subset) nested under the field |
140
+ | Access | Read-only (enforced) | Configurable via `@Access` |
141
+ | Sort / filter | Native (join applied before sort/filter) | Not natively sortable/filterable on remote fields |
142
+ | Multiple references | One remote field per decorator | Single or array (`multi`) references |
143
+
144
+ ## Next Steps
145
+
146
+ See the [computed fields documentation](./11.computed.md) to generalize this merge mechanism to arbitrary in-database expressions, or the [modifiers documentation](./9.modifiers.md) to learn how automatic data transformation integrates with the Data API.
@@ -0,0 +1,152 @@
1
+ # Computed Fields
2
+
3
+ ## Overview
4
+
5
+ The `@Computed` decorator declares a field whose value is computed in the database from an expression, then merged onto each row at query time. The expression can be a row-local computation (derived from other fields of the same row) or a per-row subquery, such as an aggregate over another table. Because the merge happens before sorting and filtering, computed fields are natively sortable, filterable, and pageable in DB — no fetch-all or in-memory sorting required.
6
+
7
+ Computed fields are read-only: the value is produced by the query and is never persisted on the controller's table. Declaring a field as computed automatically sets its access mode to read-only and forces non-indexed sorting, regardless of decorator order.
8
+
9
+ ## Basic Usage
10
+
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
+
13
+ ```typescript
14
+ import { Computed } from "@antelopejs/interface-data-api/metadata";
15
+
16
+ @RegisterTable("groups")
17
+ class Group extends Table {
18
+ @Field("string")
19
+ declare _id: string;
20
+
21
+ @Field("string")
22
+ declare name: string;
23
+ }
24
+
25
+ @RegisterTable("devices")
26
+ class Device extends Table {
27
+ @Field("string")
28
+ declare _id: string;
29
+
30
+ @Index()
31
+ @Field("string")
32
+ @Relation({ to: () => Group })
33
+ declare group_id: string;
34
+ }
35
+
36
+ @RegisterDataController()
37
+ class GroupAPI extends DataController(
38
+ Group,
39
+ DefaultRoutes.All,
40
+ Controller("/groups"),
41
+ ) {
42
+ @ModelReference()
43
+ @Model(GroupModel)
44
+ declare groupModel: GroupModel;
45
+
46
+ @Listable()
47
+ @Access(AccessMode.ReadOnly)
48
+ declare _id: string;
49
+
50
+ @Listable()
51
+ @Access(AccessMode.ReadWrite)
52
+ declare name: string;
53
+
54
+ // Aggregate: number of devices referencing this group, computed in DB
55
+ @Listable()
56
+ @Computed(
57
+ (row, db) =>
58
+ db
59
+ .table("devices")
60
+ .getAll(row.key("_id"), "group_id")
61
+ .count(),
62
+ { default: 0 },
63
+ )
64
+ @Sortable()
65
+ declare devices_count: number;
66
+
67
+ // Row-local expression
68
+ @Listable()
69
+ @Computed((row) => (row.key("name") as ValueProxy<string>).concat("!"))
70
+ declare display_name: string;
71
+ }
72
+ ```
73
+
74
+ A GET request to `/groups/get?id=group-123` returns the computed values at the top level:
75
+
76
+ ```json
77
+ {
78
+ "_id": "group-123",
79
+ "name": "Alpha",
80
+ "devices_count": 3,
81
+ "display_name": "Alpha!"
82
+ }
83
+ ```
84
+
85
+ ## Decorator Parameters
86
+
87
+ ```typescript
88
+ @Computed(expr, options?)
89
+ ```
90
+
91
+ | Parameter | Type | Required | Description |
92
+ | --------- | ---- | -------- | ----------- |
93
+ | `expr` | `(row, db) => expression` | Yes | Expression receiving the row proxy and the schema instance; returns a `ValueProxy` expression or a subquery (`Datum`) |
94
+ | `options.default` | `unknown` | No | Fallback merged over the computed value when the expression yields `null` |
95
+
96
+ ### The `default` option
97
+
98
+ Aggregate subqueries yield `null` instead of their natural zero value when the underlying set is empty (a `count()` over a group with no devices returns `null`, not `0`). Calling `.default(0)` inside the subquery expression does not help: the aggregation produces no document at all, so there is nothing to apply the default to. The `options.default` value solves this by being applied at the row level, after the merge:
99
+
100
+ ```typescript
101
+ @Computed((row, db) => db.table("devices").getAll(row.key("_id"), "group_id").count(), {
102
+ default: 0,
103
+ })
104
+ declare devices_count: number;
105
+ ```
106
+
107
+ Without it, rows with an empty aggregate carry `null`, which breaks equality filters (`filter_devices_count=eq:0`) and makes sort ordering depend on BSON type ordering.
108
+
109
+ ## Sorting, Filtering, and Pagination
110
+
111
+ Computed fields combine with `@Sortable` and `@Filter` like native columns. The framework injects the computation lazily, only when and where the value is needed:
112
+
113
+ - A computed field referenced by an active filter is merged before filters run.
114
+ - A computed field used as the sort key is merged after the total count is captured and before the non-indexed `orderBy`, so the aggregate is not paid twice.
115
+ - A computed field requested only for display (in the pluck, neither sorted nor filtered) is merged after pagination (`slice`), so it is computed only for the returned page, not the whole collection.
116
+
117
+ Sorting and pagination both stay in the database: sorting by a computed aggregate with `limit`/`offset` produces correct pages and a constant `total`.
118
+
119
+ Computed fields are always sorted without a database index (`@Sortable` is forced to non-indexed), since the value does not exist as a stored column.
120
+
121
+ ### Filtering on numeric computed fields
122
+
123
+ Filter values arrive as strings from the query string. The default filter compares them as-is, which never matches a numeric computed value (`"0"` is not `0` in BSON). Provide a custom filter function that coerces the value:
124
+
125
+ ```typescript
126
+ @Computed((row, db) => db.table("devices").getAll(row.key("_id"), "group_id").count(), {
127
+ default: 0,
128
+ })
129
+ @Filter((_context, proxy, _key, value) =>
130
+ (proxy as ValueProxy<number>).eq(parseFloat(value)),
131
+ )
132
+ declare devices_count: number;
133
+ ```
134
+
135
+ ## Computed vs. Joined
136
+
137
+ | Aspect | `@Computed` | `@Joined` |
138
+ | ------ | ----------- | --------- |
139
+ | Source | Arbitrary expression: row-local computation or per-row subquery | One scalar field from another table matched by a foreign key |
140
+ | Typical use | Aggregates (`count`, `sum`), derived values | Flattening a referenced record's field |
141
+ | Access | Read-only (enforced) | Read-only (enforced) |
142
+ | Sort / filter | Native, lazily injected only where needed | Native, always applied before sort/filter |
143
+
144
+ `@Joined` remains the simpler choice for the 1:1 flattening case; `@Computed` generalizes the same merge mechanism to arbitrary expressions.
145
+
146
+ ## Driver Support
147
+
148
+ The aggregate form relies on the database driver translating a per-row subquery inside a merge into a correlated lookup (`$lookup` with `let`/`pipeline` on MongoDB). This is supported by `@antelopejs/mongodb` >= 1.2.1. Row-local expressions only use standard expression compilation and work on any driver.
149
+
150
+ ## Next Steps
151
+
152
+ See the [joined fields documentation](./10.joined.md) for the dedicated 1:1 flattening decorator.
@@ -0,0 +1,120 @@
1
+ # Data Controllers
2
+
3
+ ## Overview
4
+
5
+ A data controller is the central building block of the Data API. It connects a database table to a set of API endpoints, exposing the table fields you declare on the class. The `DataController` function creates a base class that combines your table definition, route definitions, and controller path into a single cohesive unit.
6
+
7
+ ## Create a Data Controller
8
+
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
+
11
+ ```typescript
12
+ import { Controller } from "@antelopejs/interface-api";
13
+ import {
14
+ DataController,
15
+ DefaultRoutes,
16
+ RegisterDataController,
17
+ } from "@antelopejs/interface-data-api";
18
+ import {
19
+ Access,
20
+ AccessMode,
21
+ Listable,
22
+ ModelReference,
23
+ } from "@antelopejs/interface-data-api/metadata";
24
+ import {
25
+ BasicDataModel,
26
+ Field,
27
+ Index,
28
+ Model,
29
+ RegisterTable,
30
+ Table,
31
+ } from "@antelopejs/interface-database-decorators";
32
+
33
+ // Define the database table
34
+ @RegisterTable("users")
35
+ class User extends Table {
36
+ @Index({ primary: true })
37
+ @Field("string")
38
+ declare _id: string;
39
+
40
+ @Field("string")
41
+ declare email: string;
42
+
43
+ @Field("number")
44
+ declare age: number;
45
+ }
46
+
47
+ // Create the database model
48
+ class UserModel extends BasicDataModel(User, "users") {}
49
+
50
+ // Create the data controller
51
+ @RegisterDataController()
52
+ class UserAPI extends DataController(
53
+ User,
54
+ DefaultRoutes.All,
55
+ Controller("/users"),
56
+ ) {
57
+ @ModelReference()
58
+ @Model(UserModel, "my-database")
59
+ declare userModel: UserModel;
60
+
61
+ @Listable()
62
+ @Access(AccessMode.ReadOnly)
63
+ declare _id: string;
64
+
65
+ @Listable()
66
+ @Access(AccessMode.ReadWrite)
67
+ declare email: string;
68
+
69
+ @Access(AccessMode.ReadWrite)
70
+ declare age: number;
71
+ }
72
+ ```
73
+
74
+ ## `DataController` Signature
75
+
76
+ ```typescript
77
+ DataController(
78
+ tableClass: typeof Table, // Database table class
79
+ routes: DataControllerDef, // Route definitions (e.g., DefaultRoutes.All)
80
+ base: typeof Controller, // Controller base with path
81
+ )
82
+ ```
83
+
84
+ | Parameter | Description |
85
+ | --------- | ----------- |
86
+ | `tableClass` | The database table class decorated with `@RegisterTable`. The controller validates that this table exists in the database schema. |
87
+ | `routes` | An object mapping endpoint names to route callbacks. Use `DefaultRoutes.All` for standard CRUD or pass a custom subset. |
88
+ | `base` | The controller base, typically created with `Controller("/path")`, which sets the URL prefix for all endpoints. |
89
+
90
+ ## Required Components
91
+
92
+ Every data controller requires the following elements:
93
+
94
+ 1. **Table Class** - A database table decorated with `@RegisterTable`, defining the schema.
95
+ 2. **Route Definitions** - Which operations to expose. Use `DefaultRoutes.All` or a custom selection.
96
+ 3. **Controller Base** - The URL path prefix, created with `Controller("/path")`.
97
+ 4. **Model Reference** - A property decorated with `@ModelReference()` that holds a database model instance. This model provides the actual database operations (insert, get, update, delete).
98
+
99
+ ## The `@RegisterDataController` Decorator
100
+
101
+ Apply `@RegisterDataController()` to the class to finalize the controller setup. This decorator processes all field decorators, registers the defined routes, and connects parameter providers to each endpoint.
102
+
103
+ ## Field Declarations
104
+
105
+ Fields declared on the data controller class determine which database columns are exposed through the API. Each field can be annotated with decorators to control its behavior:
106
+
107
+ - `@Access` - Set read/write permissions.
108
+ - `@Listable` - Include the field in list responses.
109
+ - `@Mandatory` - Require the field on specific operations.
110
+ - `@Validator` - Attach a validation function.
111
+ - `@Foreign` - Establish a foreign key relationship.
112
+ - `@Joined` - Flatten a scalar field from another table onto the row.
113
+ - `@Filter` - Enable filtering on the field.
114
+ - `@Sortable` - Allow sorting by the field.
115
+
116
+ Fields without any decorator are ignored by the Data API and do not appear in API responses or accept input.
117
+
118
+ ## Next Steps
119
+
120
+ See the [routes documentation](./3.routes.md) to learn about available routes and how to customize them.