@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.
Files changed (40) hide show
  1. package/dist/components.d.ts +3 -3
  2. package/dist/components.js +55 -5
  3. package/dist/components.js.map +1 -1
  4. package/dist/index.js +17 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/tests/components/access_control.test.js +13 -0
  7. package/dist/tests/components/access_control.test.js.map +1 -1
  8. package/dist/tests/components/computed.test.js +10 -0
  9. package/dist/tests/components/computed.test.js.map +1 -1
  10. package/dist/tests/components/foreign_joined.test.js +18 -0
  11. package/dist/tests/components/foreign_joined.test.js.map +1 -1
  12. package/dist/tests/components/joined.test.js +105 -0
  13. package/dist/tests/components/joined.test.js.map +1 -1
  14. package/dist/tests/components/listable.test.js +25 -0
  15. package/dist/tests/components/listable.test.js.map +1 -1
  16. package/dist/tests/components/mandatory.test.js +21 -0
  17. package/dist/tests/components/mandatory.test.js.map +1 -1
  18. package/dist/tests/components/modifier_key.test.js +13 -0
  19. package/dist/tests/components/modifier_key.test.js.map +1 -1
  20. package/dist/tests/components/per_action_access.test.js +13 -0
  21. package/dist/tests/components/per_action_access.test.js.map +1 -1
  22. package/dist/tests/components/validator.test.js +25 -0
  23. package/dist/tests/components/validator.test.js.map +1 -1
  24. package/dist/tests/index/routes.test.js +13 -0
  25. package/dist/tests/index/routes.test.js.map +1 -1
  26. package/dist/tests/integration.test.js +169 -0
  27. package/dist/tests/integration.test.js.map +1 -1
  28. package/docs/1.introduction.md +128 -0
  29. package/docs/10.joined.md +146 -0
  30. package/docs/11.computed.md +152 -0
  31. package/docs/2.data-controllers.md +120 -0
  32. package/docs/3.routes.md +320 -0
  33. package/docs/4.access-rights.md +148 -0
  34. package/docs/5.validators.md +204 -0
  35. package/docs/6.listable.md +257 -0
  36. package/docs/7.foreign-keys.md +285 -0
  37. package/docs/8.filters.md +175 -0
  38. package/docs/9.modifiers.md +137 -0
  39. package/package.json +9 -4
  40. package/skills/data-api-interface/SKILL.md +91 -0
@@ -0,0 +1,148 @@
1
+ # Access Rights
2
+
3
+ ## Overview
4
+
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
+
7
+ ## Access Modes
8
+
9
+ The `AccessMode` enum defines three permission levels:
10
+
11
+ ```typescript
12
+ import { Access, AccessMode } from "@antelopejs/interface-data-api/metadata";
13
+ ```
14
+
15
+ | Mode | Value | Description |
16
+ | ---- | ----- | ----------- |
17
+ | `AccessMode.ReadOnly` | `1` | Field appears in GET and LIST responses but is ignored in POST and PUT bodies |
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 |
20
+
21
+ ## Basic Usage
22
+
23
+ ### Read-Only Fields
24
+
25
+ Read-only fields appear in API responses but cannot be modified through create or update requests. Values sent for these fields in request bodies are silently ignored.
26
+
27
+ ```typescript
28
+ @RegisterDataController()
29
+ class UserAPI extends DataController(
30
+ User,
31
+ DefaultRoutes.All,
32
+ Controller("/users"),
33
+ ) {
34
+ @ModelReference()
35
+ @Model(UserModel, "my-database")
36
+ declare userModel: UserModel;
37
+
38
+ @Access(AccessMode.ReadOnly)
39
+ declare _id: string;
40
+
41
+ @Access(AccessMode.ReadOnly)
42
+ declare createdAt: string;
43
+
44
+ @Access(AccessMode.ReadWrite)
45
+ declare name: string;
46
+ }
47
+ ```
48
+
49
+ A PUT request to `/users/edit?id=user-123` with `{ "createdAt": "2025-01-01", "name": "Alice" }` updates only `name`. The `createdAt` field is ignored.
50
+
51
+ ### Write-Only Fields
52
+
53
+ Write-only fields accept input during create and update operations but are never included in API responses. This is useful for sensitive data like passwords.
54
+
55
+ ```typescript
56
+ @RegisterDataController()
57
+ class UserAPI extends DataController(
58
+ User,
59
+ DefaultRoutes.All,
60
+ Controller("/users"),
61
+ ) {
62
+ @ModelReference()
63
+ @Model(UserModel, "my-database")
64
+ declare userModel: UserModel;
65
+
66
+ @Access(AccessMode.ReadWrite)
67
+ declare name: string;
68
+
69
+ @Access(AccessMode.ReadWrite)
70
+ declare email: string;
71
+
72
+ @Access(AccessMode.WriteOnly)
73
+ declare password: string;
74
+ }
75
+ ```
76
+
77
+ A GET request to `/users/get?id=user-123` returns `name` and `email` but never includes `password`:
78
+
79
+ ```json
80
+ {
81
+ "name": "Bob",
82
+ "email": "bob@example.com"
83
+ }
84
+ ```
85
+
86
+ ### Read-Write Fields
87
+
88
+ Read-write fields are fully accessible: they appear in responses and accept input in request bodies.
89
+
90
+ ```typescript
91
+ @Access(AccessMode.ReadWrite)
92
+ declare email: string;
93
+
94
+ @Access(AccessMode.ReadWrite)
95
+ declare name: string;
96
+ ```
97
+
98
+ ## Per-Action Overrides
99
+
100
+ The `@Access` decorator accepts an optional second argument to override the access mode for specific actions. This allows a field to behave differently depending on the operation being performed.
101
+
102
+ ```typescript
103
+ @Access(defaultMode: AccessMode, overrides?: Record<string, AccessMode>)
104
+ ```
105
+
106
+ 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
+
108
+ ### Example: Writable on Create, Read-Only on Edit
109
+
110
+ ```typescript
111
+ @RegisterDataController()
112
+ class UserAPI extends DataController(
113
+ User,
114
+ DefaultRoutes.All,
115
+ Controller("/users"),
116
+ ) {
117
+ @ModelReference()
118
+ @Model(UserModel, "my-database")
119
+ declare userModel: UserModel;
120
+
121
+ @Access(AccessMode.ReadOnly)
122
+ declare _id: string;
123
+
124
+ @Access(AccessMode.ReadWrite)
125
+ declare name: string;
126
+
127
+ // role can be set when creating, but cannot be changed during edit
128
+ @Access(AccessMode.ReadWrite, { edit: AccessMode.ReadOnly })
129
+ declare role: string;
130
+
131
+ // age is read-only by default, but writable during creation
132
+ @Access(AccessMode.ReadOnly, { new: AccessMode.ReadWrite })
133
+ declare age: number;
134
+ }
135
+ ```
136
+
137
+ In this configuration:
138
+
139
+ - `role` is readable and writable in all operations except `edit`, where it becomes read-only.
140
+ - `age` is read-only by default but can be set during `new` (create) operations.
141
+
142
+ ## Fields Without `@Access`
143
+
144
+ Fields declared without the `@Access` decorator are not exposed through the API at all. They do not appear in responses and are not accepted in request bodies. To include a field in the API, you must explicitly set its access mode.
145
+
146
+ ## Next Steps
147
+
148
+ See the [validators documentation](./5.validators.md) to learn how to enforce validation rules on field values.
@@ -0,0 +1,204 @@
1
+ # Validators
2
+
3
+ ## Overview
4
+
5
+ Validators provide field-level data integrity checks that run automatically during create and update operations. The `@Validator` decorator attaches a custom validation function to a field, while `@Mandatory` ensures that specific fields are present in request bodies for given operations. The `@Optional` decorator explicitly marks a field as not mandatory.
6
+
7
+ ## The `@Validator` Decorator
8
+
9
+ ### Basic Usage
10
+
11
+ The `@Validator` decorator accepts a function that receives the field value and returns `true` if valid or `false` if invalid. The function can also return a `Promise<boolean>` for asynchronous validation.
12
+
13
+ ```typescript
14
+ import { Validator } from "@antelopejs/interface-data-api/metadata";
15
+
16
+ @RegisterDataController()
17
+ class UserAPI extends DataController(
18
+ User,
19
+ DefaultRoutes.All,
20
+ Controller("/users"),
21
+ ) {
22
+ @ModelReference()
23
+ @Model(UserModel, "my-database")
24
+ declare userModel: UserModel;
25
+
26
+ @Access(AccessMode.ReadWrite)
27
+ @Validator((value) => typeof value === "string" && value.trim().length > 0)
28
+ declare name: string;
29
+
30
+ @Access(AccessMode.ReadWrite)
31
+ @Validator((value) => typeof value === "number" && value >= 0 && value <= 120)
32
+ declare age: number;
33
+ }
34
+ ```
35
+
36
+ ### Validation Function Signature
37
+
38
+ ```typescript
39
+ type ValidatorFunction = (value: unknown) => boolean | Promise<boolean>;
40
+ ```
41
+
42
+ The function:
43
+
44
+ - Receives the raw field value from the request body.
45
+ - Returns `true` when the value is valid.
46
+ - Returns `false` when the value is invalid, which triggers a `400 Bad Request` response.
47
+ - Should handle unexpected types gracefully (e.g., check `typeof` before accessing properties).
48
+
49
+ ## Common Validation Patterns
50
+
51
+ ### String Validation
52
+
53
+ ```typescript
54
+ // Minimum length
55
+ @Validator((value) => typeof value === "string" && value.length >= 2)
56
+ declare firstName: string;
57
+
58
+ // Email format
59
+ const emailRegex = /^[\w](\.?[\w-]+)*@(?:[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,}$/;
60
+ @Validator((value) => typeof value === "string" && emailRegex.test(value))
61
+ declare email: string;
62
+
63
+ // Enum-like constraint
64
+ const VALID_ROLES = ["admin", "editor", "viewer"];
65
+ @Validator((value) => typeof value === "string" && VALID_ROLES.includes(value))
66
+ declare role: string;
67
+ ```
68
+
69
+ ### Number Validation
70
+
71
+ ```typescript
72
+ // Range
73
+ @Validator((value) => typeof value === "number" && value >= 0 && value <= 120)
74
+ declare age: number;
75
+
76
+ // Integer only
77
+ @Validator((value) => typeof value === "number" && Number.isInteger(value))
78
+ declare quantity: number;
79
+
80
+ // Positive
81
+ @Validator((value) => typeof value === "number" && value > 0)
82
+ declare price: number;
83
+ ```
84
+
85
+ ### Date Validation
86
+
87
+ ```typescript
88
+ // Valid date string
89
+ @Validator((value) => typeof value === "string" && !isNaN(Date.parse(value)))
90
+ declare birthDate: string;
91
+
92
+ // Future date only
93
+ @Validator((value) =>
94
+ typeof value === "string" &&
95
+ !isNaN(Date.parse(value)) &&
96
+ new Date(value) > new Date()
97
+ )
98
+ declare appointmentDate: string;
99
+ ```
100
+
101
+ ### Async Validation
102
+
103
+ ```typescript
104
+ // Check uniqueness against the database
105
+ @Validator(async (value) => {
106
+ if (typeof value !== "string") return false;
107
+ const existing = await lookupByEmail(value);
108
+ return !existing;
109
+ })
110
+ declare email: string;
111
+ ```
112
+
113
+ ## The `@Mandatory` Decorator
114
+
115
+ The `@Mandatory` decorator specifies which operations require a field to be present in the request body. Unlike `@Validator`, which checks field content, `@Mandatory` only verifies that the field exists in the payload.
116
+
117
+ ```typescript
118
+ import { Mandatory } from "@antelopejs/interface-data-api/metadata";
119
+ ```
120
+
121
+ ### Usage
122
+
123
+ Pass one or more operation names as arguments. Common operation names are `"new"` (create) and `"edit"` (update).
124
+
125
+ ```typescript
126
+ @RegisterDataController()
127
+ class UserAPI extends DataController(
128
+ User,
129
+ DefaultRoutes.All,
130
+ Controller("/users"),
131
+ ) {
132
+ @ModelReference()
133
+ @Model(UserModel, "my-database")
134
+ declare userModel: UserModel;
135
+
136
+ @Access(AccessMode.ReadWrite)
137
+ @Mandatory("new", "edit")
138
+ declare email: string;
139
+
140
+ @Access(AccessMode.WriteOnly)
141
+ @Mandatory("new")
142
+ declare password: string;
143
+
144
+ @Access(AccessMode.ReadWrite)
145
+ @Mandatory("edit")
146
+ declare status: string;
147
+
148
+ @Access(AccessMode.ReadWrite)
149
+ declare notes: string; // Not mandatory for any operation
150
+ }
151
+ ```
152
+
153
+ In this example:
154
+
155
+ - `email` must be provided in both create and update requests.
156
+ - `password` is required only when creating a new record.
157
+ - `status` is required only when editing an existing record.
158
+ - `notes` is always optional.
159
+
160
+ ### Skip Mandatory Validation
161
+
162
+ Use `DefaultRoutes.WithOptions` with the `noMandatory` option to create an endpoint that skips mandatory field checks.
163
+
164
+ ```typescript
165
+ const routes = {
166
+ edit: DefaultRoutes.Edit,
167
+ patchEdit: DefaultRoutes.WithOptions(DefaultRoutes.Edit, {
168
+ noMandatory: "true",
169
+ }),
170
+ };
171
+
172
+ @RegisterDataController()
173
+ class UserAPI extends DataController(User, routes, Controller("/users")) {
174
+ @Access(AccessMode.ReadWrite)
175
+ @Mandatory("edit")
176
+ declare status: string;
177
+ }
178
+ ```
179
+
180
+ Requests to `/users/edit` require `status`, while requests to `/users/patchEdit` do not.
181
+
182
+ ## The `@Optional` Decorator
183
+
184
+ The `@Optional` decorator explicitly marks a field as not mandatory for any operation. Use it on fields that have no other decorators but still need to be recognized by the Data API metadata system.
185
+
186
+ ```typescript
187
+ import { Optional } from "@antelopejs/interface-data-api/metadata";
188
+
189
+ @Optional()
190
+ declare notes: string;
191
+ ```
192
+
193
+ ## Error Handling
194
+
195
+ When validation or mandatory checks fail, the API returns a `400 Bad Request` response with a plain text message describing the issue.
196
+
197
+ - **Mandatory failure:** `"Missing mandatory fields: email, password"`
198
+ - **Validator failure:** `"Invalid field type(s): age, email"`
199
+
200
+ All fields are checked, and the error message lists every field that failed validation.
201
+
202
+ ## Next Steps
203
+
204
+ See the [listable fields documentation](./6.listable.md) to learn how to control field visibility in list operations and configure pagination.
@@ -0,0 +1,257 @@
1
+ # Listable Fields
2
+
3
+ ## Overview
4
+
5
+ The `@Listable` decorator controls which fields are included in list endpoint responses. By default, list operations only return fields explicitly marked as listable, keeping payloads lightweight. The decorator supports multiple pluck modes for different levels of detail, and list responses include built-in pagination metadata.
6
+
7
+ The `@Sortable` decorator marks fields that can be used as a sorting key in list queries.
8
+
9
+ ## Basic Usage
10
+
11
+ Fields decorated with `@Listable()` are included in list responses. Fields without this decorator are excluded from list results but still appear in single-record `get` responses (provided they have read access).
12
+
13
+ ```typescript
14
+ import {
15
+ Access,
16
+ AccessMode,
17
+ Listable,
18
+ ModelReference,
19
+ } from "@antelopejs/interface-data-api/metadata";
20
+
21
+ @RegisterDataController()
22
+ class UserAPI extends DataController(
23
+ User,
24
+ DefaultRoutes.All,
25
+ Controller("/users"),
26
+ ) {
27
+ @ModelReference()
28
+ @Model(UserModel, "my-database")
29
+ declare userModel: UserModel;
30
+
31
+ @Listable()
32
+ @Access(AccessMode.ReadOnly)
33
+ declare _id: string;
34
+
35
+ @Listable()
36
+ @Access(AccessMode.ReadOnly)
37
+ declare email: string;
38
+
39
+ @Access(AccessMode.ReadOnly)
40
+ declare biography: string; // Excluded from list, included in get
41
+ }
42
+ ```
43
+
44
+ A request to `GET /users/list` returns `_id` and `email` for each record. A request to `GET /users/get?id=user-123` returns all three fields.
45
+
46
+ ## Pluck Modes
47
+
48
+ Pluck modes allow a single controller to serve multiple list views with different field selections. Each `@Listable` call can target a named mode.
49
+
50
+ ### Default Mode
51
+
52
+ Calling `@Listable()` without a mode name registers the field under the default `"list"` mode.
53
+
54
+ ```typescript
55
+ @Listable()
56
+ declare name: string;
57
+ ```
58
+
59
+ ### Custom Modes
60
+
61
+ Pass `true` and a mode name to register a field under a specific pluck mode.
62
+
63
+ ```typescript
64
+ @Listable() // Included in default "list" mode
65
+ @Listable(true, "detailed") // Also included in "detailed" mode
66
+ declare name: string;
67
+
68
+ @Listable(true, "detailed") // Only in "detailed" mode
69
+ declare internalNotes: string;
70
+
71
+ @Listable() // Only in default "list" mode
72
+ declare email: string;
73
+ ```
74
+
75
+ ### Exclude from a Mode
76
+
77
+ Pass `false` to explicitly exclude a field from a mode.
78
+
79
+ ```typescript
80
+ @Listable()
81
+ @Listable(false, "minimal") // Excluded from "minimal" mode
82
+ declare description: string;
83
+ ```
84
+
85
+ ### Access Pluck Modes via Routes
86
+
87
+ Define separate routes for each pluck mode using `DefaultRoutes.WithOptions` with the `pluckMode` option.
88
+
89
+ ```typescript
90
+ const routes = {
91
+ list: DefaultRoutes.List,
92
+ detailed: DefaultRoutes.WithOptions(DefaultRoutes.List, {
93
+ pluckMode: "detailed",
94
+ }),
95
+ };
96
+
97
+ @RegisterDataController()
98
+ class UserAPI extends DataController(User, routes, Controller("/users")) {
99
+ @ModelReference()
100
+ @Model(UserModel, "my-database")
101
+ declare userModel: UserModel;
102
+
103
+ @Listable()
104
+ @Access(AccessMode.ReadOnly)
105
+ declare name: string;
106
+
107
+ @Listable(true, "detailed")
108
+ @Access(AccessMode.ReadOnly)
109
+ declare internalNotes: string;
110
+ }
111
+ ```
112
+
113
+ - `GET /users/list` returns only `name`.
114
+ - `GET /users/detailed` returns both `name` and `internalNotes`.
115
+
116
+ ### Listable with Getters
117
+
118
+ When a field uses a getter (computed property), the `@Listable` decorator must specify which database fields the getter depends on. This ensures those fields are fetched from the database.
119
+
120
+ ```typescript
121
+ @Listable(["firstName", "lastName"])
122
+ @Access(AccessMode.ReadOnly)
123
+ get fullName() {
124
+ return `${this.firstName} ${this.lastName}`;
125
+ }
126
+ ```
127
+
128
+ ## Pagination
129
+
130
+ List responses include pagination metadata. The response structure is:
131
+
132
+ ```typescript
133
+ {
134
+ results: T[]; // Records for the current page
135
+ total: number; // Total number of matching records
136
+ offset: number; // Number of records skipped
137
+ limit: number; // Maximum records per page
138
+ }
139
+ ```
140
+
141
+ ### Query Parameters
142
+
143
+ | Parameter | Default | Description |
144
+ | --------- | ------- | ----------- |
145
+ | `limit` | `10` | Maximum number of records to return |
146
+ | `offset` | `0` | Number of records to skip |
147
+
148
+ ### Example
149
+
150
+ ```http
151
+ GET /users/list?limit=2&offset=4
152
+ ```
153
+
154
+ ```json
155
+ {
156
+ "results": [
157
+ { "_id": "user-5", "name": "Eve" },
158
+ { "_id": "user-6", "name": "Frank" }
159
+ ],
160
+ "total": 100,
161
+ "offset": 4,
162
+ "limit": 2
163
+ }
164
+ ```
165
+
166
+ ### Limit Maximum Page Size
167
+
168
+ Use the `maxPage` route option to cap the `limit` parameter. If a request specifies a `limit` larger than `maxPage`, the value is clamped to `maxPage`.
169
+
170
+ ```typescript
171
+ const routes = {
172
+ list: DefaultRoutes.WithOptions(DefaultRoutes.List, { maxPage: 25 }),
173
+ };
174
+
175
+ @RegisterDataController()
176
+ class UserAPI extends DataController(User, routes, Controller("/users")) {
177
+ // A request with ?limit=100 returns at most 25 records
178
+ }
179
+ ```
180
+
181
+ ## Sorting with `@Sortable`
182
+
183
+ The `@Sortable` decorator marks a field as eligible for use as a sorting key in list operations.
184
+
185
+ ```typescript
186
+ import { Sortable } from "@antelopejs/interface-data-api/metadata";
187
+
188
+ @RegisterDataController()
189
+ class UserAPI extends DataController(
190
+ User,
191
+ DefaultRoutes.All,
192
+ Controller("/users"),
193
+ ) {
194
+ @ModelReference()
195
+ @Model(UserModel, "my-database")
196
+ declare userModel: UserModel;
197
+
198
+ @Listable()
199
+ @Access(AccessMode.ReadOnly)
200
+ @Sortable()
201
+ declare _id: string;
202
+
203
+ @Listable()
204
+ @Access(AccessMode.ReadOnly)
205
+ @Sortable({ noIndex: true })
206
+ declare name: string;
207
+
208
+ @Listable()
209
+ @Access(AccessMode.ReadOnly)
210
+ @Sortable({ noIndex: true })
211
+ declare age: number;
212
+ }
213
+ ```
214
+
215
+ ### Sort Query Parameters
216
+
217
+ | Parameter | Description |
218
+ | --------- | ----------- |
219
+ | `sortKey` | The field name to sort by. Must be decorated with `@Sortable`. |
220
+ | `sortDirection` | `"asc"` (ascending) or `"desc"` (descending). Defaults to `"asc"`. |
221
+
222
+ ### The `noIndex` Option
223
+
224
+ By default, `@Sortable()` assumes the field has a database index and uses indexed ordering. Set `noIndex: true` when the field is not indexed, so the framework performs an in-memory sort instead.
225
+
226
+ ```typescript
227
+ @Sortable() // Uses database index for sorting
228
+ declare _id: string;
229
+
230
+ @Sortable({ noIndex: true }) // In-memory sort, no index required
231
+ declare name: string;
232
+ ```
233
+
234
+ ### Example
235
+
236
+ ```http
237
+ GET /users/list?sortKey=age&sortDirection=desc&limit=3
238
+ ```
239
+
240
+ ```json
241
+ {
242
+ "results": [
243
+ { "_id": "user-1", "name": "Senior", "age": 65 },
244
+ { "_id": "user-2", "name": "Adult", "age": 30 },
245
+ { "_id": "user-3", "name": "Young", "age": 18 }
246
+ ],
247
+ "total": 3,
248
+ "offset": 0,
249
+ "limit": 3
250
+ }
251
+ ```
252
+
253
+ If `sortKey` refers to a field that is not marked with `@Sortable`, the API returns a `400 Bad Request` error.
254
+
255
+ ## Next Steps
256
+
257
+ See the [foreign keys documentation](./7.foreign-keys.md) to learn how to establish relationships between tables.