@antelopejs/interface-data-api 0.2.2 → 0.4.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.
Files changed (50) hide show
  1. package/README.md +1 -1
  2. package/dist/antelope.test.js +1 -1
  3. package/dist/components.d.ts +81 -17
  4. package/dist/components.js +129 -29
  5. package/dist/components.js.map +1 -1
  6. package/dist/index.d.ts +9 -5
  7. package/dist/index.js +42 -56
  8. package/dist/index.js.map +1 -1
  9. package/dist/metadata.d.ts +14 -0
  10. package/dist/metadata.js +23 -4
  11. package/dist/metadata.js.map +1 -1
  12. package/dist/tests/components/foreign_computed.test.d.ts +1 -0
  13. package/dist/tests/components/foreign_computed.test.js +172 -0
  14. package/dist/tests/components/foreign_computed.test.js.map +1 -0
  15. package/dist/tests/components/localized_sort.test.d.ts +1 -0
  16. package/dist/tests/components/localized_sort.test.js +132 -0
  17. package/dist/tests/components/localized_sort.test.js.map +1 -0
  18. package/dist/tests/components/mandatory.test.js +12 -9
  19. package/dist/tests/components/mandatory.test.js.map +1 -1
  20. package/dist/tests/components/parsed_value.test.js +7 -3
  21. package/dist/tests/components/parsed_value.test.js.map +1 -1
  22. package/dist/tests/components/partial_edit.test.js +2 -2
  23. package/dist/tests/components/partial_edit.test.js.map +1 -1
  24. package/dist/tests/components/per_action_access.test.js +12 -2
  25. package/dist/tests/components/per_action_access.test.js.map +1 -1
  26. package/dist/tests/components/validator.test.js +3 -5
  27. package/dist/tests/components/validator.test.js.map +1 -1
  28. package/dist/tests/index/inheritance.test.js +1 -1
  29. package/dist/tests/index/inheritance.test.js.map +1 -1
  30. package/dist/tests/index/routes.test.js +45 -3
  31. package/dist/tests/index/routes.test.js.map +1 -1
  32. package/dist/tests/integration.test.js +5 -7
  33. package/dist/tests/integration.test.js.map +1 -1
  34. package/dist/tests/utils.d.ts +6 -1
  35. package/dist/tests/utils.js +3 -1
  36. package/dist/tests/utils.js.map +1 -1
  37. package/docs/1.introduction.md +26 -26
  38. package/docs/10.joined.md +36 -5
  39. package/docs/11.computed.md +6 -6
  40. package/docs/2.data-controllers.md +4 -4
  41. package/docs/3.routes.md +56 -8
  42. package/docs/4.access-rights.md +21 -6
  43. package/docs/5.validators.md +6 -4
  44. package/docs/6.listable.md +6 -2
  45. package/docs/7.foreign-keys.md +9 -10
  46. package/docs/8.filters.md +22 -10
  47. package/docs/9.modifiers.md +2 -4
  48. package/docs/assets/joined-list.svg +23 -0
  49. package/package.json +4 -4
  50. package/skills/data-api-interface/SKILL.md +9 -0
@@ -4,6 +4,19 @@
4
4
 
5
5
  The `@Access` decorator controls how fields are read and written through the API. Each field can be set to read-only, write-only, or read-write, providing fine-grained control over data exposure. Access modes can also be overridden on a per-action basis, allowing different permissions for `get`, `list`, `new`, and `edit` operations.
6
6
 
7
+ ## Separate field policy from caller permissions
8
+
9
+ `@Access` answers “may this action read or write this field?”, not “may this user access this record?”. Authenticate the caller and apply record-level policy separately. A write-only password is hidden from responses, but this decorator does not hash it.
10
+
11
+ Build the field policy in this order:
12
+
13
+ 1. Set `@Access` for each public field. Undecorated fields remain private.
14
+ 2. Add action overrides for differences between creation and editing.
15
+ 3. Add `@Listable` for the fields selected by list responses; readable alone does not define a list projection.
16
+ 4. Add validators and mandatory-field rules for writable inputs.
17
+
18
+ For example, a display name can be readable and writable, an ID readable only, and a password writable only. Test each direction: attempting to write a read-only field is ignored, while omitting a mandatory writable field is a validation concern.
19
+
7
20
  ## Access Modes
8
21
 
9
22
  The `AccessMode` enum defines three permission levels:
@@ -12,11 +25,11 @@ The `AccessMode` enum defines three permission levels:
12
25
  import { Access, AccessMode } from "@antelopejs/interface-data-api/metadata";
13
26
  ```
14
27
 
15
- | Mode | Value | Description |
16
- | ---------------------- | ----- | ----------------------------------------------------------------------------- |
17
- | `AccessMode.ReadOnly` | `1` | Field 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 |
28
+ | Mode | Value | Description |
29
+ | ---------------------- | ----- | --------------------------------------------------------------------------------------------------- |
30
+ | `AccessMode.ReadOnly` | `1` | Field is readable; list responses also require selection by the pluck mode. POST and PUT ignore it. |
31
+ | `AccessMode.WriteOnly` | `2` | Field is accepted in POST and PUT bodies but never appears in responses |
32
+ | `AccessMode.ReadWrite` | `3` | Field is both readable and writable |
20
33
 
21
34
  ## Basic Usage
22
35
 
@@ -105,6 +118,8 @@ The `@Access` decorator accepts an optional second argument to override the acce
105
118
 
106
119
  The `overrides` object maps action names (`"get"`, `"list"`, `"new"`, `"edit"`) to an `AccessMode` value. Actions not listed in the overrides use the default mode.
107
120
 
121
+ To read the access a field has in one action, outside the routes (to render a form read-only on edit, for example), call `GetEffectiveMode(field, action)` from `@antelopejs/interface-data-api/metadata` with the field's metadata (`meta.fields[name]`). It is the mode the routes apply.
122
+
108
123
  ### Example: Writable on Create, Read-Only on Edit
109
124
 
110
125
  ```typescript
@@ -141,7 +156,7 @@ In this configuration:
141
156
 
142
157
  ## Fields Without `@Access`
143
158
 
144
- 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.
159
+ Ordinary fields need `@Access` to appear in responses or accept input; `@Listable` alone does not grant access. The derived-field decorators [`@Joined`](./10.joined.md) and [`@Computed`](./11.computed.md) are exceptions: they set read-only access themselves.
145
160
 
146
161
  ## Next Steps
147
162
 
@@ -118,6 +118,8 @@ declare appointmentDate: string;
118
118
 
119
119
  ### Async Validation
120
120
 
121
+ An asynchronous lookup can give early feedback, but a check followed by an insert is not a uniqueness guarantee: concurrent requests can both pass. Enforce uniqueness in the database where supported, and handle write conflicts. `lookupByEmail` below is an application-supplied function.
122
+
121
123
  ```typescript
122
124
  // Check uniqueness against the database
123
125
  @Validator(async (value) => {
@@ -213,12 +215,12 @@ declare notes: string;
213
215
 
214
216
  ## Error Handling
215
217
 
216
- When validation or mandatory checks fail, the API returns a `400 Bad Request` response with a plain text message describing the issue.
218
+ When validation or mandatory checks fail, the API returns a `400 Bad Request` response whose JSON body names what went wrong and the fields concerned, for the client to word it and show it under each field:
217
219
 
218
- - **Mandatory failure:** `"Missing mandatory fields: email, password"`
219
- - **Validator failure:** `"Invalid field type(s): age, email"`
220
+ - **Mandatory failure:** `{ "code": "missing_mandatory_fields", "fields": ["email", "password"] }`
221
+ - **Validator failure:** `{ "code": "invalid_field_types", "fields": ["age", "email"] }`
220
222
 
221
- All fields are checked, and the error message lists every field that failed validation.
223
+ The codes are exported as `Validation.MISSING_MANDATORY_FIELDS` and `Validation.INVALID_FIELD_TYPES` (`@antelopejs/interface-data-api/components`), the body's type as `Validation.FieldsErrorBody`. All fields are checked: `fields` lists every one that failed.
222
224
 
223
225
  ## Next Steps
224
226
 
@@ -224,16 +224,20 @@ class UserAPI extends DataController(
224
224
 
225
225
  ### The `noIndex` Option
226
226
 
227
- 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.
227
+ By default, `@Sortable()` marks the field for indexed ordering. Set `noIndex: true` when it is not indexed. Both paths build an AQL `orderBy` operation; this option changes where sorting is staged, not whether rows are sorted in application memory. The provider determines how the database executes that ordering.
228
228
 
229
229
  ```typescript
230
230
  @Sortable() // Uses database index for sorting
231
231
  declare _id: string;
232
232
 
233
- @Sortable({ noIndex: true }) // In-memory sort, no index required
233
+ @Sortable({ noIndex: true }) // Non-indexed query ordering
234
234
  declare name: string;
235
235
  ```
236
236
 
237
+ ### Localized Fields
238
+
239
+ A `@Localized` field is sorted on its value in the request's locale, the one the controller's `@ModifierKey(LocalizationModifier)` field holds. A row with no value in that locale, or an empty one, sorts on its `fallbackLocale` value. This ordering is computed per row, so it never uses an index, whatever `noIndex` says.
240
+
237
241
  ### Example
238
242
 
239
243
  ```http
@@ -8,19 +8,21 @@ The `@Foreign` decorator establishes relationships between database tables in yo
8
8
 
9
9
  Apply `@Foreign` to a field that stores a foreign key value. The decorator replaces the raw ID in the response with the full referenced record.
10
10
 
11
+ Choose the response fields explicitly with `pluck`. A foreign lookup reads the target table, not its API controller: do not rely on the target controller's `@Access` policy to hide passwords or internal fields. The example below selects only the user's ID, name, and email.
12
+
13
+ The snippets use the `myapp` schema. Register it in the declaring module before serving requests, as in the [quick start](./1.introduction.md#quick-start). Controller snippets assume the corresponding model and shared imports are already declared.
14
+
11
15
  ```typescript
12
16
  import { Foreign } from "@antelopejs/interface-data-api/metadata";
13
17
  import {
14
18
  Field,
15
- Index,
16
19
  RegisterTable,
17
20
  Relation,
18
21
  Table,
19
22
  } from "@antelopejs/interface-database-decorators";
20
23
 
21
- @RegisterTable("users")
24
+ @RegisterTable("users", "myapp")
22
25
  class User extends Table {
23
- @Index({ primary: true })
24
26
  @Field("string")
25
27
  declare _id: string;
26
28
 
@@ -31,9 +33,8 @@ class User extends Table {
31
33
  declare email: string;
32
34
  }
33
35
 
34
- @RegisterTable("orders")
36
+ @RegisterTable("orders", "myapp")
35
37
  class Order extends Table {
36
- @Index({ primary: true })
37
38
  @Field("string")
38
39
  declare _id: string;
39
40
 
@@ -61,7 +62,7 @@ class OrderAPI extends DataController(
61
62
 
62
63
  @Listable()
63
64
  @Access(AccessMode.ReadOnly)
64
- @Foreign(User)
65
+ @Foreign(User, undefined, false, ["_id", "name", "email"])
65
66
  declare userId: User;
66
67
 
67
68
  @Listable()
@@ -125,9 +126,8 @@ When a field stores an array of foreign key IDs, set the `multi` parameter to `t
125
126
  The table-level `@Relation({ many: true })` is declarative metadata recording that the field holds multiple target keys; API-side resolution is controlled solely by the `multi` parameter of `@Foreign`.
126
127
 
127
128
  ```typescript
128
- @RegisterTable("carts")
129
+ @RegisterTable("carts", "myapp")
129
130
  class Cart extends Table {
130
- @Index({ primary: true })
131
131
  @Field("string")
132
132
  declare _id: string;
133
133
 
@@ -136,9 +136,8 @@ class Cart extends Table {
136
136
  declare items: string[]; // Array of CartItem IDs
137
137
  }
138
138
 
139
- @RegisterTable("cart_items")
139
+ @RegisterTable("cart_items", "myapp")
140
140
  class CartItem extends Table {
141
- @Index({ primary: true })
142
141
  @Field("string")
143
142
  declare _id: string;
144
143
 
package/docs/8.filters.md CHANGED
@@ -81,6 +81,14 @@ Combine multiple filters:
81
81
  GET /users/list?filter_status=active&filter_role=ne:admin
82
82
  ```
83
83
 
84
+ Filter one field several times to keep the rows that pass every filter (AND):
85
+
86
+ ```http
87
+ GET /users/list?filter_age=ge:18&filter_age=lt:65
88
+ ```
89
+
90
+ The route receives that field's filters as a list of `FilterValue` tuples (`FieldFilter`); a field filtered once keeps a single tuple. Read either shape with `FilterValuesOf(filter)` from `@antelopejs/interface-data-api/metadata`. An indexed equality filter only uses its index when it is the field's only filter.
91
+
84
92
  ### Numeric and Boolean Filters
85
93
 
86
94
  Parse and validate values explicitly in a custom filter. These filters support the six numeric comparisons and boolean equality/inequality; unsupported input throws an HTTP 400 response.
@@ -148,14 +156,14 @@ type FilterFunction<T, U> = (
148
156
  ) => ValueProxyOrValue<boolean>;
149
157
  ```
150
158
 
151
- | Parameter | Description |
152
- | --------- | -------------------------------------------------------------------------------------------------------------- |
153
- | `context` | The request context, extended with `this` referencing the controller instance |
154
- | `proxy` | A `ValueProxy` representing the field value with modifier transformations applied (e.g., decrypted, localized) |
155
- | `key` | The field name being filtered |
156
- | `value` | The filter value from the query parameter |
157
- | `mode` | The comparison operator from the query parameter |
158
- | `row` | The full database row as a `ValueProxy` |
159
+ | Parameter | Description |
160
+ | --------- | ------------------------------------------------------------------------------------------------------- |
161
+ | `context` | The request context, extended with `this` referencing the controller instance |
162
+ | `proxy` | A query expression produced by the field modifier's `unlockrequest()` method; see the limitations below |
163
+ | `key` | The field name being filtered |
164
+ | `value` | The filter value from the query parameter |
165
+ | `mode` | The comparison operator from the query parameter |
166
+ | `row` | The full database row as a `ValueProxy` |
159
167
 
160
168
  ### Example: Pattern Matching Filter
161
169
 
@@ -168,7 +176,9 @@ function filterByEmailDomain(
168
176
  mode: string,
169
177
  row: ValueProxy<Record<string, any>>,
170
178
  ) {
171
- return proxy.match(`@${value}$`);
179
+ assert(mode === "eq", 400, "Only equality is supported for email domains");
180
+ const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
181
+ return proxy.match(`@${escaped}$`);
172
182
  }
173
183
 
174
184
  @RegisterDataController()
@@ -199,6 +209,8 @@ Query for users with Gmail addresses:
199
209
  GET /users/list?filter_email=gmail.com
200
210
  ```
201
211
 
212
+ Import `assert` from `@antelopejs/interface-api-util`. Escaping the domain makes the dot literal rather than a regular-expression wildcard. This example deliberately rejects operators other than `eq` instead of silently treating every request as equality.
213
+
202
214
  ### The `useIndex` Parameter
203
215
 
204
216
  The `@Filter` decorator accepts an optional second parameter, `useIndex`, that controls whether the filter can use a database index for optimized lookups.
@@ -234,6 +246,6 @@ On a controller with a `status` filter, `?filter_status=inactive&filter_role=adm
234
246
 
235
247
  ## Modifier Integration
236
248
 
237
- The `proxy` parameter in custom filter functions provides the field value after modifier transformations have been applied. For example, if a field uses an encryption modifier, `proxy` contains the decrypted value. This ensures filters work correctly with modifier-protected data.
249
+ The `proxy` parameter is a database expression, not an unlocked model instance. The Data API calls the modifier's `unlockrequest()` method to build that expression. Localization can select a locale at query time, but the built-in `EncryptionModifier.unlockrequest()` returns the stored expression unchanged: it does not decrypt ciphertext in the database. A custom modifier must explicitly support query-time access before a filter can operate on its logical value.
238
250
 
239
251
  For raw database values, use the `row` parameter and access the field directly.
@@ -45,7 +45,6 @@ import {
45
45
  import {
46
46
  BasicDataModel,
47
47
  Field,
48
- Index,
49
48
  Localized,
50
49
  LocalizationModifier,
51
50
  Model,
@@ -53,9 +52,8 @@ import {
53
52
  Table,
54
53
  } from "@antelopejs/interface-database-decorators";
55
54
 
56
- @RegisterTable("content")
55
+ @RegisterTable("content", "myapp")
57
56
  class Content extends Table.with(LocalizationModifier) {
58
- @Index({ primary: true })
59
57
  @Field("string")
60
58
  declare _id: string;
61
59
 
@@ -126,7 +124,7 @@ Modifiers work automatically with foreign key relationships. When the Data API r
126
124
 
127
125
  ### Filters
128
126
 
129
- When filtering modifier-protected fields, custom filter functions receive the unlocked (transformed) value through the `proxy` parameter. This ensures filters operate on the correct data. See the [filters documentation](./8.filters.md) for details.
127
+ Custom filters receive a query expression produced by the modifier's `unlockrequest()` method, not a decrypted JavaScript value. Localization supports query-time access; the built-in encryption modifier leaves its stored representation unchanged in query expressions. Do not use it to filter by plaintext. See the [filters documentation](./8.filters.md#modifier-integration) for details.
130
128
 
131
129
  ## Learn More
132
130
 
@@ -0,0 +1,23 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 510" role="img" aria-labelledby="title desc">
2
+ <title id="title">When the default list route resolves joined fields</title>
3
+ <desc id="desc">Resolve fields needed by filters before filtering and counting. Resolve sort fields before ordering and paging. Resolve remaining selected display fields only for the page. This describes the default list route, not arbitrary Query.List composition.</desc>
4
+ <rect width="480" height="510" rx="16" fill="#f5f7fb"/>
5
+ <g font-family="system-ui, sans-serif" fill="#172b45">
6
+ <text x="24" y="36" font-size="20" font-weight="700">Joined fields in a default list request</text>
7
+ <rect x="24" y="58" width="432" height="100" rx="10" fill="#e1ecfa"/>
8
+ <text x="42" y="87" font-size="18" font-weight="700">1. Needed by a filter</text>
9
+ <text x="42" y="116" font-size="16">Resolve → filter matching rows</text>
10
+ <text x="42" y="143" font-size="16">Count these rows for total.</text>
11
+ <path d="M240 168v24m-6-6 6 6 6-6" fill="none" stroke="#526780" stroke-width="2"/>
12
+ <rect x="24" y="204" width="432" height="100" rx="10" fill="#e1ecfa"/>
13
+ <text x="42" y="233" font-size="18" font-weight="700">2. Needed only for sorting</text>
14
+ <text x="42" y="262" font-size="16">Resolve → sort matching rows</text>
15
+ <text x="42" y="289" font-size="16">Then select the requested page.</text>
16
+ <path d="M240 314v24m-6-6 6 6 6-6" fill="none" stroke="#526780" stroke-width="2"/>
17
+ <rect x="24" y="350" width="432" height="100" rx="10" fill="#d9eee8"/>
18
+ <text x="42" y="379" font-size="18" font-weight="700">3. Needed only for display</text>
19
+ <text x="42" y="408" font-size="16">Resolve selected fields on the page</text>
20
+ <text x="42" y="435" font-size="16">Return results with pagination metadata.</text>
21
+ <text x="24" y="484" font-size="15">A field resolved earlier is not deferred again.</text>
22
+ </g>
23
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/interface-data-api",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "AntelopeJS interface that generates CRUD REST endpoints from database tables, with access rules, validation and pagination",
5
5
  "keywords": [
6
6
  "antelopejs",
@@ -67,8 +67,8 @@
67
67
  "@antelopejs/interface-api": "^0.0.15",
68
68
  "@antelopejs/interface-api-util": "^0.1.3",
69
69
  "@antelopejs/interface-core": "^0.2.4",
70
- "@antelopejs/interface-database": "^0.1.10",
71
- "@antelopejs/interface-database-decorators": "^0.1.9",
70
+ "@antelopejs/interface-database": "^0.1.11",
71
+ "@antelopejs/interface-database-decorators": "^0.1.10",
72
72
  "@antelopejs/tooling-configs": "^0.0.9",
73
73
  "@types/chai": "^4.3.20",
74
74
  "@types/mocha": "^10.0.10",
@@ -91,7 +91,7 @@
91
91
  "@antelopejs/interface-api-util": ">=0.1.3 <1.0.0",
92
92
  "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
93
93
  "@antelopejs/interface-database": ">=0.1.10 <1.0.0",
94
- "@antelopejs/interface-database-decorators": ">=0.1.9 <1.0.0"
94
+ "@antelopejs/interface-database-decorators": ">=0.1.10 <1.0.0"
95
95
  },
96
96
  "packageManager": "pnpm@10.6.5",
97
97
  "antelopeJs": {
@@ -121,6 +121,8 @@ This exposes `GET /tasks/get?id=`, `GET /tasks/list`, `POST /tasks/new`, `PUT /t
121
121
  - Filters come from `filter_<name>=<mode>:<value>` query params (plain value means `eq`); modes are
122
122
  `eq|ne|gt|ge|lt|le` (the default filter compares the raw query-string value — pass a custom filter
123
123
  function to cast for numeric ranges). Only fields declared with `@Filter()` are filterable.
124
+ Repeating a field's filter (`filter_age=ge:18&filter_age=lt:65`) keeps the rows passing them all;
125
+ the route then receives a list of tuples for it (`FieldFilter`, read with `FilterValuesOf`).
124
126
  - `@Joined` and `@Computed` fields are materialized in-database, so `@Sortable` and `@Filter()` work
125
127
  efficiently on them — but those decorators must still be applied explicitly. Such fields are
126
128
  forced read-only and to non-indexed sorting.
@@ -128,6 +130,13 @@ This exposes `GET /tasks/get?id=`, `GET /tasks/list`, `POST /tasks/new`, `PUT /t
128
130
  of `DefaultRoutes.All`; use `DefaultRoutes.WithOptions(route, options, endpoint)` to rename an
129
131
  endpoint or preset parameter options.
130
132
  - `@Access` accepts per-action overrides, e.g. `@Access(AccessMode.ReadOnly, { edit: AccessMode.ReadWrite })`.
133
+ `GetEffectiveMode(field, action)` reads the mode a field has in one action.
134
+ - A custom list route reads its page with `Query.ListPage(db, meta, query, options)` (`offset`,
135
+ `limit`, `filters`, `sortKey`, `pluck`, `noForeign`, `deferredJoined`): joined, then computed, then
136
+ foreign, then pluck.
137
+ - `new` answers the created row as `get` reads it, with its `_id`; `edit` answers the stored row after
138
+ the update. Both accept a body already parsed (`Validation.WrittenBody`), so a wrapping route can
139
+ amend it before the write.
131
140
  - `edit` is a partial update: a field absent from the body is left unchanged, `null` clears it, and
132
141
  setters only run for the fields present. `@Mandatory("edit")` rejects a field cleared by the body
133
142
  (`null`, `""`, `[]`), not an absent one. `new` still writes every writable field and runs every setter.