@antelopejs/interface-data-api 0.1.5 → 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.
- package/dist/components.d.ts +3 -3
- package/dist/components.js +55 -5
- package/dist/components.js.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/tests/components/access_control.test.js +13 -0
- package/dist/tests/components/access_control.test.js.map +1 -1
- package/dist/tests/components/computed.test.js +10 -0
- package/dist/tests/components/computed.test.js.map +1 -1
- package/dist/tests/components/foreign_joined.test.js +18 -0
- package/dist/tests/components/foreign_joined.test.js.map +1 -1
- package/dist/tests/components/joined.test.js +105 -0
- package/dist/tests/components/joined.test.js.map +1 -1
- package/dist/tests/components/listable.test.js +25 -0
- package/dist/tests/components/listable.test.js.map +1 -1
- package/dist/tests/components/mandatory.test.js +21 -0
- package/dist/tests/components/mandatory.test.js.map +1 -1
- package/dist/tests/components/modifier_key.test.js +13 -0
- package/dist/tests/components/modifier_key.test.js.map +1 -1
- package/dist/tests/components/per_action_access.test.js +13 -0
- package/dist/tests/components/per_action_access.test.js.map +1 -1
- package/dist/tests/components/validator.test.js +25 -0
- package/dist/tests/components/validator.test.js.map +1 -1
- package/dist/tests/index/routes.test.js +13 -0
- package/dist/tests/index/routes.test.js.map +1 -1
- package/dist/tests/integration.test.js +169 -0
- package/dist/tests/integration.test.js.map +1 -1
- package/docs/1.introduction.md +128 -0
- package/docs/10.joined.md +146 -0
- package/docs/11.computed.md +152 -0
- package/docs/2.data-controllers.md +120 -0
- package/docs/3.routes.md +320 -0
- package/docs/4.access-rights.md +148 -0
- package/docs/5.validators.md +204 -0
- package/docs/6.listable.md +257 -0
- package/docs/7.foreign-keys.md +285 -0
- package/docs/8.filters.md +175 -0
- package/docs/9.modifiers.md +137 -0
- package/package.json +9 -4
- package/skills/data-api-interface/SKILL.md +91 -0
|
@@ -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.
|
package/docs/3.routes.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# Routes
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Routes define which operations are available on a data controller. The module provides predefined route configurations that automatically generate endpoints with the correct HTTP methods, parameter extraction, and response handling.
|
|
6
|
+
|
|
7
|
+
## Default Routes
|
|
8
|
+
|
|
9
|
+
The `DefaultRoutes` namespace provides five built-in routes covering standard CRUD operations.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { DefaultRoutes } from "@antelopejs/interface-data-api";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Route | Object | HTTP Method | Endpoint Pattern |
|
|
16
|
+
| ----- | ------ | ----------- | ---------------- |
|
|
17
|
+
| Get | `DefaultRoutes.Get` | GET | `/resource/get?id=<id>` |
|
|
18
|
+
| List | `DefaultRoutes.List` | GET | `/resource/list` |
|
|
19
|
+
| New | `DefaultRoutes.New` | POST | `/resource/new` |
|
|
20
|
+
| Edit | `DefaultRoutes.Edit` | PUT | `/resource/edit?id=<id>` |
|
|
21
|
+
| Delete | `DefaultRoutes.Delete` | DELETE | `/resource/delete?id=<id>` |
|
|
22
|
+
|
|
23
|
+
Use `DefaultRoutes.All` to include all five routes at once.
|
|
24
|
+
|
|
25
|
+
## Route Configuration
|
|
26
|
+
|
|
27
|
+
Pass a route definition object to `DataController` to control which endpoints are created.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// All CRUD routes
|
|
31
|
+
@RegisterDataController()
|
|
32
|
+
class UserAPI extends DataController(
|
|
33
|
+
User,
|
|
34
|
+
DefaultRoutes.All,
|
|
35
|
+
Controller("/users"),
|
|
36
|
+
) {
|
|
37
|
+
// ...
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Only specific routes
|
|
41
|
+
@RegisterDataController()
|
|
42
|
+
class UserAPI extends DataController(
|
|
43
|
+
User,
|
|
44
|
+
{
|
|
45
|
+
get: DefaultRoutes.Get,
|
|
46
|
+
list: DefaultRoutes.List,
|
|
47
|
+
new: DefaultRoutes.New,
|
|
48
|
+
},
|
|
49
|
+
Controller("/users"),
|
|
50
|
+
) {
|
|
51
|
+
// edit and delete endpoints are not created
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Route Details
|
|
56
|
+
|
|
57
|
+
### Get
|
|
58
|
+
|
|
59
|
+
Retrieves a single record by its identifier.
|
|
60
|
+
|
|
61
|
+
**Endpoint:** `GET /resource/get`
|
|
62
|
+
|
|
63
|
+
**Query Parameters:**
|
|
64
|
+
|
|
65
|
+
| Parameter | Required | Description |
|
|
66
|
+
| --------- | -------- | ----------- |
|
|
67
|
+
| `id` | Yes | Record identifier |
|
|
68
|
+
|
|
69
|
+
**Example:**
|
|
70
|
+
|
|
71
|
+
```http
|
|
72
|
+
GET /users/get?id=user-123
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Response:**
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"_id": "user-123",
|
|
80
|
+
"name": "Bob",
|
|
81
|
+
"email": "bob@example.com"
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### List
|
|
86
|
+
|
|
87
|
+
Retrieves multiple records with pagination, sorting, and filtering support.
|
|
88
|
+
|
|
89
|
+
**Endpoint:** `GET /resource/list`
|
|
90
|
+
|
|
91
|
+
**Query Parameters:**
|
|
92
|
+
|
|
93
|
+
| Parameter | Required | Default | Description |
|
|
94
|
+
| --------- | -------- | ------- | ----------- |
|
|
95
|
+
| `limit` | No | `10` | Maximum number of records to return |
|
|
96
|
+
| `offset` | No | `0` | Number of records to skip |
|
|
97
|
+
| `sortKey` | No | - | Field to sort by (must be marked `@Sortable`) |
|
|
98
|
+
| `sortDirection` | No | `asc` | Sort direction: `asc` or `desc` |
|
|
99
|
+
|
|
100
|
+
**Example:**
|
|
101
|
+
|
|
102
|
+
```http
|
|
103
|
+
GET /users/list?limit=10&offset=0&sortKey=name&sortDirection=asc
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Response:**
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"results": [
|
|
111
|
+
{ "_id": "user-123", "name": "Alice", "email": "alice@example.com" },
|
|
112
|
+
{ "_id": "user-456", "name": "Bob", "email": "bob@example.com" }
|
|
113
|
+
],
|
|
114
|
+
"total": 42,
|
|
115
|
+
"offset": 0,
|
|
116
|
+
"limit": 10
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### New
|
|
121
|
+
|
|
122
|
+
Creates a new record. The request body must be a JSON object with the fields to set.
|
|
123
|
+
|
|
124
|
+
**Endpoint:** `POST /resource/new`
|
|
125
|
+
|
|
126
|
+
**Body:** JSON object with field values.
|
|
127
|
+
|
|
128
|
+
**Example:**
|
|
129
|
+
|
|
130
|
+
```http
|
|
131
|
+
POST /users/new
|
|
132
|
+
Content-Type: application/json
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
"name": "Bob",
|
|
136
|
+
"email": "bob@example.com",
|
|
137
|
+
"password": "secure123"
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Response:**
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
["user-789"]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Before inserting, the `insert` event is triggered on any table modifiers attached to the database table.
|
|
148
|
+
|
|
149
|
+
### Edit
|
|
150
|
+
|
|
151
|
+
Updates an existing record by its identifier.
|
|
152
|
+
|
|
153
|
+
**Endpoint:** `PUT /resource/edit`
|
|
154
|
+
|
|
155
|
+
**Query Parameters:**
|
|
156
|
+
|
|
157
|
+
| Parameter | Required | Description |
|
|
158
|
+
| --------- | -------- | ----------- |
|
|
159
|
+
| `id` | Yes | Record identifier |
|
|
160
|
+
|
|
161
|
+
**Body:** JSON object with fields to update.
|
|
162
|
+
|
|
163
|
+
**Example:**
|
|
164
|
+
|
|
165
|
+
```http
|
|
166
|
+
PUT /users/edit?id=user-123
|
|
167
|
+
Content-Type: application/json
|
|
168
|
+
|
|
169
|
+
{
|
|
170
|
+
"name": "Robert",
|
|
171
|
+
"email": "robert@example.com"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Response:**
|
|
176
|
+
|
|
177
|
+
```http
|
|
178
|
+
200 OK
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Before updating, the `update` event is triggered on any table modifiers attached to the database table.
|
|
182
|
+
|
|
183
|
+
### Delete
|
|
184
|
+
|
|
185
|
+
Deletes one or more records by their identifiers.
|
|
186
|
+
|
|
187
|
+
**Endpoint:** `DELETE /resource/delete`
|
|
188
|
+
|
|
189
|
+
**Query Parameters:**
|
|
190
|
+
|
|
191
|
+
| Parameter | Required | Description |
|
|
192
|
+
| --------- | -------- | ----------- |
|
|
193
|
+
| `id` | Yes | Record identifier(s). Pass multiple `id` parameters to delete several records at once. |
|
|
194
|
+
|
|
195
|
+
**Example:**
|
|
196
|
+
|
|
197
|
+
```http
|
|
198
|
+
DELETE /users/delete?id=user-123
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Response:**
|
|
202
|
+
|
|
203
|
+
```http
|
|
204
|
+
200 OK
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Route Options with `WithOptions`
|
|
208
|
+
|
|
209
|
+
The `DefaultRoutes.WithOptions` function customizes route behavior by overriding query parameters or route settings.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
DefaultRoutes.WithOptions(callback, options?, endpoint?)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
| Parameter | Description |
|
|
216
|
+
| --------- | ----------- |
|
|
217
|
+
| `callback` | The route callback to customize (e.g., `DefaultRoutes.Edit`). |
|
|
218
|
+
| `options` | An object with option overrides. These values take precedence over query parameters. |
|
|
219
|
+
| `endpoint` | An optional custom endpoint path. Defaults to the route key name. |
|
|
220
|
+
|
|
221
|
+
### Example
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
const routes = {
|
|
225
|
+
edit: DefaultRoutes.Edit,
|
|
226
|
+
quickEdit: DefaultRoutes.WithOptions(DefaultRoutes.Edit, {
|
|
227
|
+
noMandatory: "true",
|
|
228
|
+
}),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
@RegisterDataController()
|
|
232
|
+
class UserAPI extends DataController(User, routes, Controller("/users")) {
|
|
233
|
+
// /users/edit enforces mandatory fields
|
|
234
|
+
// /users/quickEdit skips mandatory field validation
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Available Options per Route
|
|
239
|
+
|
|
240
|
+
**Get:**
|
|
241
|
+
|
|
242
|
+
| Option | Type | Description |
|
|
243
|
+
| ------ | ---- | ----------- |
|
|
244
|
+
| `index` | `string` | Use a secondary index instead of the primary key |
|
|
245
|
+
| `noForeign` | `string` | Skip foreign key resolution |
|
|
246
|
+
|
|
247
|
+
**List:**
|
|
248
|
+
|
|
249
|
+
| Option | Type | Description |
|
|
250
|
+
| ------ | ---- | ----------- |
|
|
251
|
+
| `maxPage` | `number` | Maximum allowed value for `limit` (caps the page size) |
|
|
252
|
+
| `noForeign` | `boolean` | Skip foreign key resolution |
|
|
253
|
+
| `noPluck` | `boolean` | Return all database fields instead of plucked fields |
|
|
254
|
+
| `pluckMode` | `string` | Select a named pluck mode (see `@Listable`) |
|
|
255
|
+
|
|
256
|
+
**New:**
|
|
257
|
+
|
|
258
|
+
| Option | Type | Description |
|
|
259
|
+
| ------ | ---- | ----------- |
|
|
260
|
+
| `noMandatory` | `string` | Skip mandatory field validation |
|
|
261
|
+
|
|
262
|
+
**Edit:**
|
|
263
|
+
|
|
264
|
+
| Option | Type | Description |
|
|
265
|
+
| ------ | ---- | ----------- |
|
|
266
|
+
| `index` | `string` | Use a secondary index instead of the primary key |
|
|
267
|
+
| `noMandatory` | `string` | Skip mandatory field validation |
|
|
268
|
+
|
|
269
|
+
## Custom Routes
|
|
270
|
+
|
|
271
|
+
For operations beyond CRUD, define a custom route callback with `func`, `args`, and `method` properties.
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { Context } from "@antelopejs/interface-api";
|
|
275
|
+
import {
|
|
276
|
+
DataController,
|
|
277
|
+
DefaultRoutes,
|
|
278
|
+
RegisterDataController,
|
|
279
|
+
} from "@antelopejs/interface-data-api";
|
|
280
|
+
import { Parameters } from "@antelopejs/interface-data-api/components";
|
|
281
|
+
|
|
282
|
+
const Search = {
|
|
283
|
+
func: async (ctx, params) => {
|
|
284
|
+
// Custom search logic
|
|
285
|
+
return { results: [] };
|
|
286
|
+
},
|
|
287
|
+
args: [Context(), Parameters.List()],
|
|
288
|
+
method: "get",
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
@RegisterDataController()
|
|
292
|
+
class UserAPI extends DataController(
|
|
293
|
+
User,
|
|
294
|
+
{
|
|
295
|
+
...DefaultRoutes.All,
|
|
296
|
+
search: Search,
|
|
297
|
+
},
|
|
298
|
+
Controller("/users"),
|
|
299
|
+
) {
|
|
300
|
+
// All default routes plus a custom /users/search endpoint
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The `args` array contains parameter decorators that extract values from the request context. Each decorator corresponds to a parameter in the `func` callback, in order.
|
|
305
|
+
|
|
306
|
+
## Error Handling
|
|
307
|
+
|
|
308
|
+
Default routes return standard HTTP error responses:
|
|
309
|
+
|
|
310
|
+
| Status | Condition |
|
|
311
|
+
| ------ | --------- |
|
|
312
|
+
| `400 Bad Request` | Validation fails, missing required fields, or invalid parameters |
|
|
313
|
+
| `404 Not Found` | Record does not exist |
|
|
314
|
+
| `500 Internal Server Error` | Database operation fails |
|
|
315
|
+
|
|
316
|
+
Error messages are returned as plain text in the response body.
|
|
317
|
+
|
|
318
|
+
## Next Steps
|
|
319
|
+
|
|
320
|
+
See the [access rights documentation](./4.access-rights.md) to learn how to control field read/write permissions.
|