@antelopejs/interface-data-api 0.2.1 → 0.3.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 (48) hide show
  1. package/README.md +131 -21
  2. package/dist/antelope.test.js +1 -1
  3. package/dist/components.d.ts +64 -15
  4. package/dist/components.js +91 -23
  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 +23 -3
  10. package/dist/metadata.js +32 -7
  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/mandatory.test.js +3 -5
  16. package/dist/tests/components/mandatory.test.js.map +1 -1
  17. package/dist/tests/components/parsed_value.test.js +2 -2
  18. package/dist/tests/components/parsed_value.test.js.map +1 -1
  19. package/dist/tests/components/partial_edit.test.js +2 -2
  20. package/dist/tests/components/partial_edit.test.js.map +1 -1
  21. package/dist/tests/components/per_action_access.test.js +12 -2
  22. package/dist/tests/components/per_action_access.test.js.map +1 -1
  23. package/dist/tests/components/validator.test.js +3 -5
  24. package/dist/tests/components/validator.test.js.map +1 -1
  25. package/dist/tests/index/inheritance.test.js +1 -1
  26. package/dist/tests/index/inheritance.test.js.map +1 -1
  27. package/dist/tests/index/routes.test.js +45 -3
  28. package/dist/tests/index/routes.test.js.map +1 -1
  29. package/dist/tests/integration.test.js +5 -7
  30. package/dist/tests/integration.test.js.map +1 -1
  31. package/dist/tests/utils.d.ts +6 -1
  32. package/dist/tests/utils.js +3 -1
  33. package/dist/tests/utils.js.map +1 -1
  34. package/docs/1.introduction.md +26 -26
  35. package/docs/10.joined.md +36 -5
  36. package/docs/11.computed.md +6 -6
  37. package/docs/2.data-controllers.md +4 -4
  38. package/docs/3.routes.md +85 -11
  39. package/docs/4.access-rights.md +21 -6
  40. package/docs/5.validators.md +2 -0
  41. package/docs/6.listable.md +5 -2
  42. package/docs/7.foreign-keys.md +9 -10
  43. package/docs/8.filters.md +96 -20
  44. package/docs/9.modifiers.md +2 -4
  45. package/docs/assets/joined-list.svg +23 -0
  46. package/docs/tsconfig.json +12 -0
  47. package/package.json +21 -15
  48. package/skills/data-api-interface/SKILL.md +9 -0
package/docs/8.filters.md CHANGED
@@ -8,7 +8,7 @@ The `@Filter` decorator adds filtering capabilities to list endpoints. Filters a
8
8
 
9
9
  ### Default Filter
10
10
 
11
- Apply `@Filter()` without arguments to enable standard comparison filtering on a field.
11
+ Apply `@Filter()` without arguments to enable standard comparison filtering on a string field. Query filter values remain strings: TypeScript annotations, `@Field`, and write validators do not parse them into numbers or booleans.
12
12
 
13
13
  ```typescript
14
14
  import { Filter } from "@antelopejs/interface-data-api/metadata";
@@ -30,12 +30,12 @@ class UserAPI extends DataController(
30
30
  @Listable()
31
31
  @Access(AccessMode.ReadWrite)
32
32
  @Filter()
33
- declare age: number;
33
+ declare status: string;
34
34
 
35
35
  @Listable()
36
36
  @Access(AccessMode.ReadWrite)
37
37
  @Filter()
38
- declare isActive: boolean;
38
+ declare role: string;
39
39
  }
40
40
  ```
41
41
 
@@ -63,24 +63,82 @@ When no operator is specified, `eq` (equal) is used by default.
63
63
 
64
64
  ### Examples
65
65
 
66
- Retrieve users older than 30:
66
+ Retrieve active users:
67
67
 
68
68
  ```http
69
- GET /users/list?filter_age=gt:30
69
+ GET /users/list?filter_status=active
70
70
  ```
71
71
 
72
- Retrieve active users:
72
+ Retrieve users whose role is not admin:
73
+
74
+ ```http
75
+ GET /users/list?filter_role=ne:admin
76
+ ```
77
+
78
+ Combine multiple filters:
73
79
 
74
80
  ```http
75
- GET /users/list?filter_isActive=true
81
+ GET /users/list?filter_status=active&filter_role=ne:admin
76
82
  ```
77
83
 
78
- Combine multiple filters (users under 25 who are active):
84
+ Filter one field several times to keep the rows that pass every filter (AND):
79
85
 
80
86
  ```http
81
- GET /users/list?filter_age=lt:25&filter_isActive=true
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
+
92
+ ### Numeric and Boolean Filters
93
+
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.
95
+
96
+ ```ts docs-check
97
+ import { assert } from "@antelopejs/interface-api-util";
98
+ import type { FilterFunction } from "@antelopejs/interface-data-api/metadata";
99
+
100
+ export const numberFilter: FilterFunction<Record<string, unknown>> = (
101
+ _context,
102
+ proxy,
103
+ _key,
104
+ raw,
105
+ mode,
106
+ ) => {
107
+ const value = Number(raw);
108
+ assert(raw.trim() !== "" && Number.isFinite(value), 400, "Invalid number");
109
+ assert(
110
+ ["eq", "ne", "gt", "ge", "lt", "le"].includes(mode),
111
+ 400,
112
+ "Invalid comparison",
113
+ );
114
+ return proxy[mode](value);
115
+ };
116
+
117
+ export const booleanFilter: FilterFunction<Record<string, unknown>> = (
118
+ _context,
119
+ proxy,
120
+ _key,
121
+ raw,
122
+ mode,
123
+ ) => {
124
+ assert(raw === "true" || raw === "false", 400, "Invalid boolean");
125
+ assert(mode === "eq" || mode === "ne", 400, "Invalid comparison");
126
+ return proxy[mode](raw === "true");
127
+ };
82
128
  ```
83
129
 
130
+ Apply the functions to the corresponding fields in your controller:
131
+
132
+ ```typescript
133
+ @Filter(numberFilter, false)
134
+ declare age: number;
135
+
136
+ @Filter(booleanFilter, false)
137
+ declare isActive: boolean;
138
+ ```
139
+
140
+ With these custom filters, `?filter_age=gt:30&filter_isActive=true` compares against `30` and `true`, not `"30"` and `"true"`. Keep `useIndex` false for callbacks that parse values: indexed equality uses the raw string directly and bypasses the filter callback.
141
+
84
142
  ## Custom Filters
85
143
 
86
144
  For filtering logic beyond simple comparisons, pass a custom filter function to `@Filter`.
@@ -98,14 +156,14 @@ type FilterFunction<T, U> = (
98
156
  ) => ValueProxyOrValue<boolean>;
99
157
  ```
100
158
 
101
- | Parameter | Description |
102
- | --------- | -------------------------------------------------------------------------------------------------------------- |
103
- | `context` | The request context, extended with `this` referencing the controller instance |
104
- | `proxy` | A `ValueProxy` representing the field value with modifier transformations applied (e.g., decrypted, localized) |
105
- | `key` | The field name being filtered |
106
- | `value` | The filter value from the query parameter |
107
- | `mode` | The comparison operator from the query parameter |
108
- | `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` |
109
167
 
110
168
  ### Example: Pattern Matching Filter
111
169
 
@@ -118,7 +176,9 @@ function filterByEmailDomain(
118
176
  mode: string,
119
177
  row: ValueProxy<Record<string, any>>,
120
178
  ) {
121
- 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}$`);
122
182
  }
123
183
 
124
184
  @RegisterDataController()
@@ -138,7 +198,7 @@ class UserAPI extends DataController(
138
198
 
139
199
  @Listable()
140
200
  @Access(AccessMode.ReadWrite)
141
- @Filter()
201
+ @Filter(numberFilter, false)
142
202
  declare age: number;
143
203
  }
144
204
  ```
@@ -149,6 +209,8 @@ Query for users with Gmail addresses:
149
209
  GET /users/list?filter_email=gmail.com
150
210
  ```
151
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
+
152
214
  ### The `useIndex` Parameter
153
215
 
154
216
  The `@Filter` decorator accepts an optional second parameter, `useIndex`, that controls whether the filter can use a database index for optimized lookups.
@@ -168,8 +230,22 @@ When `useIndex` is not specified:
168
230
 
169
231
  Index-optimized filters with an `eq` operator perform an indexed lookup instead of a full table scan, which significantly improves performance on large datasets.
170
232
 
233
+ ## Mandatory Route Filters
234
+
235
+ Use `DefaultRoutes.WithOptions` to constrain a list route. The `filters` map contains `[stringValue, comparison]` tuples for fields declared with `@Filter`. Request filters for other fields are retained, but a route filter wins when both specify the same field.
236
+
237
+ ```ts docs-check
238
+ import { DefaultRoutes } from "@antelopejs/interface-data-api";
239
+
240
+ const activeUsers = DefaultRoutes.WithOptions(DefaultRoutes.List, {
241
+ filters: { status: ["active", "eq"] },
242
+ });
243
+ ```
244
+
245
+ On a controller with a `status` filter, `?filter_status=inactive&filter_role=admin` still restricts `status` to `"active"` while retaining the requested `role`. Route filters are list-query constraints, not authorization for separate get/edit/delete routes.
246
+
171
247
  ## Modifier Integration
172
248
 
173
- 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.
174
250
 
175
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>
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "rootDir": "..",
7
+ "composite": false,
8
+ "incremental": false,
9
+ "noEmit": true
10
+ },
11
+ "include": ["../src", "./**/*.ts"]
12
+ }
package/package.json CHANGED
@@ -1,13 +1,19 @@
1
1
  {
2
2
  "name": "@antelopejs/interface-data-api",
3
- "version": "0.2.1",
4
- "description": "AntelopeJS interface for data API with CRUD controllers and route generation",
3
+ "version": "0.3.0",
4
+ "description": "AntelopeJS interface that generates CRUD REST endpoints from database tables, with access rules, validation and pagination",
5
5
  "keywords": [
6
6
  "antelopejs",
7
+ "api",
7
8
  "crud",
8
9
  "data-api",
9
- "interface"
10
+ "interface",
11
+ "rest"
10
12
  ],
13
+ "homepage": "https://interfaces.antelopejs.com/docs/data-api",
14
+ "bugs": {
15
+ "url": "https://github.com/AntelopeJS/interface-data-api/issues"
16
+ },
11
17
  "license": "Apache-2.0",
12
18
  "repository": {
13
19
  "type": "git",
@@ -57,13 +63,13 @@
57
63
  "knip": "knip"
58
64
  },
59
65
  "devDependencies": {
60
- "@antelopejs/core": "^1.13.4",
61
- "@antelopejs/interface-api": "^0.0.13",
62
- "@antelopejs/interface-api-util": "^0.1.2",
63
- "@antelopejs/interface-core": "^0.1.1",
64
- "@antelopejs/interface-database": "^0.1.9",
65
- "@antelopejs/interface-database-decorators": "^0.1.8",
66
- "@antelopejs/tooling-configs": "^0.0.8",
66
+ "@antelopejs/core": "^1.16.0",
67
+ "@antelopejs/interface-api": "^0.0.15",
68
+ "@antelopejs/interface-api-util": "^0.1.3",
69
+ "@antelopejs/interface-core": "^0.2.4",
70
+ "@antelopejs/interface-database": "^0.1.10",
71
+ "@antelopejs/interface-database-decorators": "^0.1.9",
72
+ "@antelopejs/tooling-configs": "^0.0.9",
67
73
  "@types/chai": "^4.3.20",
68
74
  "@types/mocha": "^10.0.10",
69
75
  "@types/node": "^22.19.15",
@@ -81,11 +87,11 @@
81
87
  "typescript": "^5.9.3"
82
88
  },
83
89
  "peerDependencies": {
84
- "@antelopejs/interface-api": ">=0.0.13 <1.0.0",
85
- "@antelopejs/interface-api-util": ">=0.1.2 <1.0.0",
86
- "@antelopejs/interface-core": ">=0.1.1 <1.0.0",
87
- "@antelopejs/interface-database": ">=0.1.9 <1.0.0",
88
- "@antelopejs/interface-database-decorators": ">=0.1.8 <1.0.0"
90
+ "@antelopejs/interface-api": ">=0.0.15 <1.0.0",
91
+ "@antelopejs/interface-api-util": ">=0.1.3 <1.0.0",
92
+ "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
93
+ "@antelopejs/interface-database": ">=0.1.10 <1.0.0",
94
+ "@antelopejs/interface-database-decorators": ">=0.1.9 <1.0.0"
89
95
  },
90
96
  "packageManager": "pnpm@10.6.5",
91
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.