@antelopejs/interface-data-api 0.3.0 → 0.5.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 (124) hide show
  1. package/README.md +1 -1
  2. package/dist/components.d.ts +32 -9
  3. package/dist/components.js +113 -628
  4. package/dist/components.js.map +1 -1
  5. package/dist/index.d.ts +41 -36
  6. package/dist/index.js +94 -92
  7. package/dist/index.js.map +1 -1
  8. package/dist/internal/computed.d.ts +6 -0
  9. package/dist/internal/computed.js +43 -0
  10. package/dist/internal/computed.js.map +1 -0
  11. package/dist/internal/controller-types.d.ts +19 -0
  12. package/dist/internal/controller-types.js +4 -0
  13. package/dist/internal/controller-types.js.map +1 -0
  14. package/dist/internal/default-filter.d.ts +4 -0
  15. package/dist/internal/default-filter.js +19 -0
  16. package/dist/internal/default-filter.js.map +1 -0
  17. package/dist/internal/deferred-joined-stream.d.ts +11 -0
  18. package/dist/internal/deferred-joined-stream.js +112 -0
  19. package/dist/internal/deferred-joined-stream.js.map +1 -0
  20. package/dist/internal/field-types.d.ts +183 -0
  21. package/dist/internal/field-types.js +4 -0
  22. package/dist/internal/field-types.js.map +1 -0
  23. package/dist/internal/filters.d.ts +4 -0
  24. package/dist/internal/filters.js +15 -0
  25. package/dist/internal/filters.js.map +1 -0
  26. package/dist/internal/foreign.d.ts +6 -0
  27. package/dist/internal/foreign.js +116 -0
  28. package/dist/internal/foreign.js.map +1 -0
  29. package/dist/internal/guards.d.ts +20 -0
  30. package/dist/internal/guards.js +74 -0
  31. package/dist/internal/guards.js.map +1 -0
  32. package/dist/internal/joined.d.ts +28 -0
  33. package/dist/internal/joined.js +127 -0
  34. package/dist/internal/joined.js.map +1 -0
  35. package/dist/internal/list.d.ts +16 -0
  36. package/dist/internal/list.js +178 -0
  37. package/dist/internal/list.js.map +1 -0
  38. package/dist/internal/meta-records.d.ts +17 -0
  39. package/dist/internal/meta-records.js +73 -0
  40. package/dist/internal/meta-records.js.map +1 -0
  41. package/dist/internal/properties.d.ts +4 -0
  42. package/dist/internal/properties.js +43 -0
  43. package/dist/internal/properties.js.map +1 -0
  44. package/dist/internal/table-reference.d.ts +11 -0
  45. package/dist/internal/table-reference.js +46 -0
  46. package/dist/internal/table-reference.js.map +1 -0
  47. package/dist/internal/tables.d.ts +18 -0
  48. package/dist/internal/tables.js +37 -0
  49. package/dist/internal/tables.js.map +1 -0
  50. package/dist/internal/validation.d.ts +11 -0
  51. package/dist/internal/validation.js +88 -0
  52. package/dist/internal/validation.js.map +1 -0
  53. package/dist/metadata.d.ts +27 -228
  54. package/dist/metadata.js +61 -188
  55. package/dist/metadata.js.map +1 -1
  56. package/dist/tests/components/access_control.test.js +20 -20
  57. package/dist/tests/components/access_control.test.js.map +1 -1
  58. package/dist/tests/components/computed.test.js +28 -30
  59. package/dist/tests/components/computed.test.js.map +1 -1
  60. package/dist/tests/components/cross_instance.test.js +8 -6
  61. package/dist/tests/components/cross_instance.test.js.map +1 -1
  62. package/dist/tests/components/deferred_joined.test.js +56 -53
  63. package/dist/tests/components/deferred_joined.test.js.map +1 -1
  64. package/dist/tests/components/filter_options.test.js +8 -7
  65. package/dist/tests/components/filter_options.test.js.map +1 -1
  66. package/dist/tests/components/foreign_computed.test.js +14 -16
  67. package/dist/tests/components/foreign_computed.test.js.map +1 -1
  68. package/dist/tests/components/foreign_joined.test.js +19 -27
  69. package/dist/tests/components/foreign_joined.test.js.map +1 -1
  70. package/dist/tests/components/joined.test.js +41 -45
  71. package/dist/tests/components/joined.test.js.map +1 -1
  72. package/dist/tests/components/listable.test.js +59 -58
  73. package/dist/tests/components/listable.test.js.map +1 -1
  74. package/dist/tests/components/localized_sort.test.d.ts +1 -0
  75. package/dist/tests/components/localized_sort.test.js +131 -0
  76. package/dist/tests/components/localized_sort.test.js.map +1 -0
  77. package/dist/tests/components/mandatory.test.js +33 -28
  78. package/dist/tests/components/mandatory.test.js.map +1 -1
  79. package/dist/tests/components/modifier_key.test.js +19 -18
  80. package/dist/tests/components/modifier_key.test.js.map +1 -1
  81. package/dist/tests/components/parse_body.test.d.ts +1 -0
  82. package/dist/tests/components/parse_body.test.js +75 -0
  83. package/dist/tests/components/parse_body.test.js.map +1 -0
  84. package/dist/tests/components/parsed_value.test.js +27 -21
  85. package/dist/tests/components/parsed_value.test.js.map +1 -1
  86. package/dist/tests/components/partial_edit.test.js +33 -32
  87. package/dist/tests/components/partial_edit.test.js.map +1 -1
  88. package/dist/tests/components/per_action_access.test.js +12 -12
  89. package/dist/tests/components/per_action_access.test.js.map +1 -1
  90. package/dist/tests/components/validator.test.js +45 -37
  91. package/dist/tests/components/validator.test.js.map +1 -1
  92. package/dist/tests/helpers/other-instance.d.ts +11 -1
  93. package/dist/tests/helpers/other-instance.js +27 -5
  94. package/dist/tests/helpers/other-instance.js.map +1 -1
  95. package/dist/tests/index/get_guard.test.js +6 -6
  96. package/dist/tests/index/get_guard.test.js.map +1 -1
  97. package/dist/tests/index/inheritance.test.js +10 -9
  98. package/dist/tests/index/inheritance.test.js.map +1 -1
  99. package/dist/tests/index/routes.test.js +40 -44
  100. package/dist/tests/index/routes.test.js.map +1 -1
  101. package/dist/tests/integration.test.d.ts +1 -1
  102. package/dist/tests/integration.test.js +53 -729
  103. package/dist/tests/integration.test.js.map +1 -1
  104. package/dist/tests/internal/http.d.ts +23 -0
  105. package/dist/tests/internal/http.js +35 -0
  106. package/dist/tests/internal/http.js.map +1 -0
  107. package/dist/tests/internal/shop-api.d.ts +1 -0
  108. package/dist/tests/internal/shop-api.js +331 -0
  109. package/dist/tests/internal/shop-api.js.map +1 -0
  110. package/dist/tests/internal/shop-schema.d.ts +154 -0
  111. package/dist/tests/internal/shop-schema.js +387 -0
  112. package/dist/tests/internal/shop-schema.js.map +1 -0
  113. package/dist/tests/utils.d.ts +5 -5
  114. package/dist/tests/utils.js +1 -3
  115. package/dist/tests/utils.js.map +1 -1
  116. package/dist/tests/utils.test.js +10 -11
  117. package/dist/tests/utils.test.js.map +1 -1
  118. package/docs/3.routes.md +8 -3
  119. package/docs/5.validators.md +16 -7
  120. package/docs/6.listable.md +4 -0
  121. package/docs/8.filters.md +7 -3
  122. package/package.json +17 -6
  123. package/skills/data-api-interface/SKILL.md +4 -0
  124. package/skills/data-api-interface/cases.json +99 -0
package/docs/3.routes.md CHANGED
@@ -291,8 +291,13 @@ class UserAPI extends DataController(User, routes, Controller("/users")) {
291
291
 
292
292
  The `new` and `edit` routes take the raw request body (`Buffer`) or a body already parsed (`Validation.WrittenBody`). A route wrapping them can parse the body, amend it (stamp a field, drop one) and hand the object on: the route validates and writes it as handed, without a second serialization.
293
293
 
294
+ A parsed body is `FieldValues`: fields by name, each holding a `FieldValue` (a string, number, boolean, `null`, `Date`, or an array or plain object of those). `Validation.ParseBody` answers 400 for anything else, whether it comes from a `Buffer` or from a body already parsed: a function, a class instance, a `bigint`, a cycle or an `undefined` array element. The default routes run with the controller as `this`, so a wrapping function declares `this: DataControllerInstance` and calls `route.func.call(this, ...)`.
295
+
294
296
  ```ts docs-check
295
- import { DefaultRoutes } from "@antelopejs/interface-data-api";
297
+ import {
298
+ DefaultRoutes,
299
+ type DataControllerInstance,
300
+ } from "@antelopejs/interface-data-api";
296
301
  import {
297
302
  type Parameters,
298
303
  Validation,
@@ -302,7 +307,7 @@ import type { RequestContext } from "@antelopejs/interface-api";
302
307
  const stampedNew = {
303
308
  ...DefaultRoutes.New,
304
309
  func: function (
305
- this: unknown,
310
+ this: DataControllerInstance,
306
311
  ctx: RequestContext,
307
312
  params: Parameters.NewParameters,
308
313
  body: Buffer,
@@ -335,7 +340,7 @@ const routes = {
335
340
  };
336
341
  ```
337
342
 
338
- The `GetGuard` callback receives `(ctx, current, params)`. Declare it as a regular function to access the controller through `this`.
343
+ The `GetGuard` callback receives `(ctx, current, params)`. Declare it as a regular function to access the controller through `this`, typed `DataControllerInstance`. `current` is `FieldValues`.
339
344
 
340
345
  | Argument | Contents | Use it for |
341
346
  | --------- | --------------------------------------------------------------------------------------- | ---------------------------------------------- |
@@ -36,17 +36,26 @@ class UserAPI extends DataController(
36
36
  ### Validation Function Signature
37
37
 
38
38
  ```typescript
39
+ type FieldValue =
40
+ string | number | boolean | null | Date | FieldValue[] | FieldValues;
41
+
42
+ interface FieldValues {
43
+ [field: string]: FieldValue | undefined;
44
+ }
45
+
39
46
  type FieldValidator = (
40
- value: unknown,
47
+ value: FieldValue | undefined,
41
48
  ) => FieldValidationResult | Promise<FieldValidationResult>;
42
49
 
43
50
  type FieldValidationResult =
44
- boolean | { success: true; data: unknown } | { success: false };
51
+ | boolean
52
+ | { success: true; data: FieldValue | undefined }
53
+ | { success: false };
45
54
  ```
46
55
 
47
56
  The function:
48
57
 
49
- - Receives the raw field value from the request body.
58
+ - Receives the raw field value from the request body: JSON, or a `Date` or `undefined` when a route wrapping `new` or `edit` amended the parsed body. `FieldValue | undefined` has to be narrowed before use.
50
59
  - Returns `true` when the value is valid: the raw value is written.
51
60
  - Returns `{ success: true, data }` when the value is valid: `data` is written instead of the raw value.
52
61
  - Returns `false` or `{ success: false }` when the value is invalid, which triggers a `400 Bad Request` response.
@@ -215,12 +224,12 @@ declare notes: string;
215
224
 
216
225
  ## Error Handling
217
226
 
218
- When validation or mandatory checks fail, the API returns a `400 Bad Request` response with a plain text message describing the issue.
227
+ 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:
219
228
 
220
- - **Mandatory failure:** `"Missing mandatory fields: email, password"`
221
- - **Validator failure:** `"Invalid field type(s): age, email"`
229
+ - **Mandatory failure:** `{ "code": "missing_mandatory_fields", "fields": ["email", "password"] }`
230
+ - **Validator failure:** `{ "code": "invalid_field_types", "fields": ["age", "email"] }`
222
231
 
223
- All fields are checked, and the error message lists every field that failed validation.
232
+ 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.
224
233
 
225
234
  ## Next Steps
226
235
 
@@ -234,6 +234,10 @@ declare _id: string;
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
package/docs/8.filters.md CHANGED
@@ -150,9 +150,11 @@ type FilterFunction<T, U> = (
150
150
  context: RequestContext & { this: T },
151
151
  proxy: ValueProxy<any>,
152
152
  key: string,
153
- value: string,
154
- mode: "eq" | "ne" | "gt" | "ge" | "lt" | "le",
155
- row: ValueProxy<U>,
153
+ ...rest: [
154
+ value: string,
155
+ mode: "eq" | "ne" | "gt" | "ge" | "lt" | "le",
156
+ row: ValueProxy<U>,
157
+ ]
156
158
  ) => ValueProxyOrValue<boolean>;
157
159
  ```
158
160
 
@@ -165,6 +167,8 @@ type FilterFunction<T, U> = (
165
167
  | `mode` | The comparison operator from the query parameter |
166
168
  | `row` | The full database row as a `ValueProxy` |
167
169
 
170
+ `value`, `mode` and `row` are one rest tuple, so that the function type stays within five parameters. A function that declares them as separate parameters is assignable to it, and `Parameters<FilterFunction<T, U>>` lists all six.
171
+
168
172
  ### Example: Pattern Matching Filter
169
173
 
170
174
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/interface-data-api",
3
- "version": "0.3.0",
3
+ "version": "0.5.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",
@@ -60,16 +60,26 @@
60
60
  "release": "pnpm run lint && pnpm run prepack && release-it",
61
61
  "test": "pnpm run build && ajs module test .",
62
62
  "format:check": "oxfmt --check .",
63
- "knip": "knip"
63
+ "knip": "knip",
64
+ "prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi",
65
+ "checks": "antelopejs-checks",
66
+ "size": "antelopejs-check-size",
67
+ "ratchet": "antelopejs-ratchet",
68
+ "sync": "antelopejs-sync",
69
+ "ratchet:update": "antelopejs-ratchet --update",
70
+ "sync:check": "antelopejs-sync --check",
71
+ "typecheck": "tsc --noEmit",
72
+ "suppressions": "antelopejs-suppressions",
73
+ "suppressions:update": "antelopejs-suppressions --update"
64
74
  },
65
75
  "devDependencies": {
66
76
  "@antelopejs/core": "^1.16.0",
67
77
  "@antelopejs/interface-api": "^0.0.15",
68
78
  "@antelopejs/interface-api-util": "^0.1.3",
69
79
  "@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",
80
+ "@antelopejs/interface-database": "^0.1.11",
81
+ "@antelopejs/interface-database-decorators": "^0.1.10",
82
+ "@antelopejs/tooling-configs": "^0.2.11",
73
83
  "@types/chai": "^4.3.20",
74
84
  "@types/mocha": "^10.0.10",
75
85
  "@types/node": "^22.19.15",
@@ -77,6 +87,7 @@
77
87
  "chai": "^4.5.0",
78
88
  "eslint-plugin-perfectionist": "^5.11.0",
79
89
  "knip": "^6.34.0",
90
+ "lefthook": "^2.2.2",
80
91
  "mongodb-memory-server-core": "^10.4.3",
81
92
  "oxfmt": "^0.66.0",
82
93
  "oxlint": "1.81.0",
@@ -91,7 +102,7 @@
91
102
  "@antelopejs/interface-api-util": ">=0.1.3 <1.0.0",
92
103
  "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
93
104
  "@antelopejs/interface-database": ">=0.1.10 <1.0.0",
94
- "@antelopejs/interface-database-decorators": ">=0.1.9 <1.0.0"
105
+ "@antelopejs/interface-database-decorators": ">=0.1.10 <1.0.0"
95
106
  },
96
107
  "packageManager": "pnpm@10.6.5",
97
108
  "antelopeJs": {
@@ -140,6 +140,10 @@ This exposes `GET /tasks/get?id=`, `GET /tasks/list`, `POST /tasks/new`, `PUT /t
140
140
  - `edit` is a partial update: a field absent from the body is left unchanged, `null` clears it, and
141
141
  setters only run for the fields present. `@Mandatory("edit")` rejects a field cleared by the body
142
142
  (`null`, `""`, `[]`), not an absent one. `new` still writes every writable field and runs every setter.
143
+ - A parsed request body is `FieldValues` (fields by name; a `FieldValue` is JSON or a `Date`). A
144
+ `@Validator` receives `FieldValue | undefined`, and `Validation.ParseBody` answers 400 for a body that
145
+ holds anything else, even one a wrapping route already parsed. The default routes run with the
146
+ controller as `this` (`DataControllerInstance`); a wrapper calls `route.func.call(this, ...)`.
143
147
  - A `@Validator` may return a parse result `{ success, data }` (e.g. zod's `safeParseAsync(value)`)
144
148
  instead of a boolean: `data` is then written in place of the raw value.
145
149
 
@@ -0,0 +1,99 @@
1
+ {
2
+ "version": 1,
3
+ "cases": [
4
+ {
5
+ "id": "data-api-interface-crud-endpoints",
6
+ "area": "framework",
7
+ "style": "clean",
8
+ "prompt": "Expose my orders table as REST endpoints for get, list, create, update and delete. Only the status column should be sortable and customer filterable.",
9
+ "expect": {
10
+ "skills": ["data-api-interface"],
11
+ "acceptable": [
12
+ "api-interface",
13
+ "database-decorators-interface",
14
+ "database-interface",
15
+ "ajs-consume-interface",
16
+ "antelopejs"
17
+ ],
18
+ "mustMention": [
19
+ "DataController",
20
+ "RegisterDataController",
21
+ "Sortable",
22
+ "Filter"
23
+ ]
24
+ }
25
+ },
26
+ {
27
+ "id": "data-api-interface-crud-casual",
28
+ "area": "framework",
29
+ "style": "casual",
30
+ "prompt": "can i get a crud api straight from my table without writing every route",
31
+ "expect": {
32
+ "skills": ["data-api-interface"],
33
+ "acceptable": [
34
+ "api-interface",
35
+ "database-decorators-interface",
36
+ "database-interface",
37
+ "antelopejs"
38
+ ],
39
+ "mustMention": ["DataController", "DefaultRoutes"]
40
+ }
41
+ },
42
+ {
43
+ "id": "data-api-interface-field-hidden",
44
+ "area": "framework",
45
+ "style": "clean",
46
+ "prompt": "The list endpoint I generated from my table never returns the email field and I cannot write to it either. Why?",
47
+ "expect": {
48
+ "skills": ["data-api-interface"],
49
+ "acceptable": [
50
+ "api-interface",
51
+ "database-decorators-interface",
52
+ "ajs-consume-interface"
53
+ ],
54
+ "mustMention": ["Access"]
55
+ }
56
+ },
57
+ {
58
+ "id": "data-api-interface-sort-400-casual",
59
+ "area": "framework",
60
+ "style": "casual",
61
+ "prompt": "sorting the list by createdAt gives me a 400",
62
+ "expect": {
63
+ "skills": ["data-api-interface"],
64
+ "acceptable": ["api-interface", "database-decorators-interface"],
65
+ "mustMention": ["Sortable"]
66
+ }
67
+ },
68
+ {
69
+ "id": "data-api-interface-table-neighbour",
70
+ "area": "framework",
71
+ "style": "clean",
72
+ "prompt": "Add a hashed passphrase and an encrypted supplier code to my chef table class, plus created and updated timestamps.",
73
+ "expect": {
74
+ "skills": ["database-decorators-interface"],
75
+ "acceptable": [
76
+ "database-interface",
77
+ "antelopejs",
78
+ "ajs-consume-interface"
79
+ ],
80
+ "avoid": ["data-api-interface"]
81
+ }
82
+ },
83
+ {
84
+ "id": "data-api-interface-wrap-new-stamp",
85
+ "area": "framework",
86
+ "style": "clean",
87
+ "prompt": "I want the generated create route to stamp a source field on every body before it is validated and written. How do I wrap it, and what type does the parsed body have?",
88
+ "expect": {
89
+ "skills": ["data-api-interface"],
90
+ "acceptable": [
91
+ "api-interface",
92
+ "database-decorators-interface",
93
+ "ajs-consume-interface"
94
+ ],
95
+ "mustMention": ["ParseBody", "WrittenBody", "FieldValues"]
96
+ }
97
+ }
98
+ ]
99
+ }