@antelopejs/interface-database-decorators 0.1.11 → 0.2.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 (88) hide show
  1. package/README.md +17 -13
  2. package/dist/antelope.test.js +1 -1
  3. package/dist/common.d.ts +48 -2
  4. package/dist/common.js +51 -5
  5. package/dist/common.js.map +1 -1
  6. package/dist/database.js +18 -3
  7. package/dist/database.js.map +1 -1
  8. package/dist/internal/bound-schema.d.ts +8 -0
  9. package/dist/internal/bound-schema.js +14 -0
  10. package/dist/internal/bound-schema.js.map +1 -0
  11. package/dist/model.d.ts +21 -8
  12. package/dist/model.js +34 -15
  13. package/dist/model.js.map +1 -1
  14. package/dist/modifiers/autodate.d.ts +2 -2
  15. package/dist/modifiers/autodate.js.map +1 -1
  16. package/dist/modifiers/common.d.ts +37 -20
  17. package/dist/modifiers/common.js +81 -68
  18. package/dist/modifiers/common.js.map +1 -1
  19. package/dist/modifiers/encryption.js +26 -14
  20. package/dist/modifiers/encryption.js.map +1 -1
  21. package/dist/modifiers/hash.d.ts +46 -4
  22. package/dist/modifiers/hash.js +175 -14
  23. package/dist/modifiers/hash.js.map +1 -1
  24. package/dist/modifiers/internal/kinds.d.ts +15 -1
  25. package/dist/modifiers/internal/kinds.js +35 -0
  26. package/dist/modifiers/internal/kinds.js.map +1 -1
  27. package/dist/modifiers/internal/records.d.ts +40 -5
  28. package/dist/modifiers/internal/records.js +70 -1
  29. package/dist/modifiers/internal/records.js.map +1 -1
  30. package/dist/modifiers/localization.js +12 -7
  31. package/dist/modifiers/localization.js.map +1 -1
  32. package/dist/relation.d.ts +5 -0
  33. package/dist/relation.js +11 -0
  34. package/dist/relation.js.map +1 -1
  35. package/dist/schema.js +5 -6
  36. package/dist/schema.js.map +1 -1
  37. package/dist/table.d.ts +3 -2
  38. package/dist/table.js +29 -13
  39. package/dist/table.js.map +1 -1
  40. package/dist/tests/cross_instance.test.js +5 -2
  41. package/dist/tests/cross_instance.test.js.map +1 -1
  42. package/dist/tests/database.test.js +42 -2
  43. package/dist/tests/database.test.js.map +1 -1
  44. package/dist/tests/hash_upgrade.test.d.ts +1 -0
  45. package/dist/tests/hash_upgrade.test.js +132 -0
  46. package/dist/tests/hash_upgrade.test.js.map +1 -0
  47. package/dist/tests/helpers/hash-rows.d.ts +40 -0
  48. package/dist/tests/helpers/hash-rows.js +41 -0
  49. package/dist/tests/helpers/hash-rows.js.map +1 -0
  50. package/dist/tests/helpers/other-instance.js.map +1 -1
  51. package/dist/tests/integration.test.js +15 -4
  52. package/dist/tests/integration.test.js.map +1 -1
  53. package/dist/tests/metadata_inheritance.test.d.ts +1 -0
  54. package/dist/tests/metadata_inheritance.test.js +212 -0
  55. package/dist/tests/metadata_inheritance.test.js.map +1 -0
  56. package/dist/tests/modifiers/encryption_date_marker.test.d.ts +1 -0
  57. package/dist/tests/modifiers/encryption_date_marker.test.js +78 -0
  58. package/dist/tests/modifiers/encryption_date_marker.test.js.map +1 -0
  59. package/dist/tests/modifiers/hash.test.js +295 -0
  60. package/dist/tests/modifiers/hash.test.js.map +1 -1
  61. package/dist/tests/modifiers/inherited_names.test.d.ts +1 -0
  62. package/dist/tests/modifiers/inherited_names.test.js +96 -0
  63. package/dist/tests/modifiers/inherited_names.test.js.map +1 -0
  64. package/dist/tests/own_key_lookups.test.d.ts +1 -0
  65. package/dist/tests/own_key_lookups.test.js +49 -0
  66. package/dist/tests/own_key_lookups.test.js.map +1 -0
  67. package/dist/tests/partial_update.test.d.ts +1 -0
  68. package/dist/tests/partial_update.test.js +55 -0
  69. package/dist/tests/partial_update.test.js.map +1 -0
  70. package/dist/tests/row_forms.test.d.ts +1 -0
  71. package/dist/tests/row_forms.test.js +59 -0
  72. package/dist/tests/row_forms.test.js.map +1 -0
  73. package/dist/tests/stored_metadata.test.d.ts +1 -0
  74. package/dist/tests/stored_metadata.test.js +91 -0
  75. package/dist/tests/stored_metadata.test.js.map +1 -0
  76. package/dist/tests/subclass_schema_binding.test.d.ts +1 -0
  77. package/dist/tests/subclass_schema_binding.test.js +115 -0
  78. package/dist/tests/subclass_schema_binding.test.js.map +1 -0
  79. package/dist/tests/table.test.js +16 -0
  80. package/dist/tests/table.test.js.map +1 -1
  81. package/docs/1.introduction.md +24 -20
  82. package/docs/2.table-definitions.md +63 -41
  83. package/docs/3.table-modifiers.md +128 -98
  84. package/docs/4.data-models.md +63 -52
  85. package/docs/5.parameter-decoration.md +28 -24
  86. package/docs/tsconfig.json +3 -0
  87. package/package.json +10 -9
  88. package/skills/database-decorators-interface/SKILL.md +6 -3
@@ -1,12 +1,16 @@
1
- # Data Models
1
+ ---
2
+ title: Data models
3
+ description: Reference for BasicDataModel, its CRUD methods and return values, the data conversions, validation, and GetModel.
4
+ mode: reference
5
+ ---
2
6
 
3
- Data models provide a high-level interface for interacting with database tables. They handle conversion between database records and TypeScript class instances, and expose methods for common CRUD operations.
7
+ A data model reads and writes one table. It converts between stored records and instances of the table class, and has methods for the common CRUD operations.
4
8
 
5
- ## BasicDataModel
9
+ ## `BasicDataModel`
6
10
 
7
- The `BasicDataModel` function creates a model class for a specific table. It wraps the underlying AQL table with methods that automatically apply modifier transformations.
11
+ The `BasicDataModel` function creates a model class for a table. The class wraps the AQL table with methods that apply the modifier transformations.
8
12
 
9
- ```typescript
13
+ ```typescript fragment
10
14
  import { BasicDataModel } from "@antelopejs/interface-database-decorators";
11
15
  import { User } from "./user.table";
12
16
 
@@ -15,27 +19,28 @@ const UserModel = BasicDataModel(User, "users");
15
19
 
16
20
  ### Parameters
17
21
 
18
- | Parameter | Type | Description |
19
- | ----------- | ------------------ | --------------------------------------------------------------------- |
20
- | `dataType` | `Constructible<T>` | The Table class representing your table |
21
- | `tableName` | `string` | The table name in the database (optional if `@RegisterTable` is used) |
22
+ | Parameter | Type | Description |
23
+ | ----------- | ------------------ | ----------------------------------------------------------------------- |
24
+ | `dataType` | `Constructible<T>` | The table class. |
25
+ | `tableName` | `string` | The table name in the database. Optional when `@RegisterTable` sets it. |
22
26
 
23
- When the table class has been decorated with `@RegisterTable`, the `tableName` parameter can be omitted.
27
+ When the table class has `@RegisterTable`, you can omit `tableName`. Without either, `BasicDataModel` throws `tableName must be provided or set via @RegisterTable`.
24
28
 
25
- ## Instantiate a Model
29
+ ## Instantiate a model
26
30
 
27
- A model instance requires a `SchemaInstance` from the database interface:
31
+ A model instance needs a `SchemaInstance` from the database interface:
28
32
 
29
- ```typescript
33
+ ```typescript fragment
30
34
  import { Schema } from "@antelopejs/interface-database";
31
35
 
32
- const schema = Schema.get("myapp")!;
36
+ const schema = Schema.get("myapp");
37
+ if (!schema) throw new Error("Schema myapp is not registered");
33
38
  const db = schema.instance();
34
39
 
35
40
  const userModel = new UserModel(db);
36
41
  ```
37
42
 
38
- ## CRUD Operations
43
+ ## Read and write rows
39
44
 
40
45
  For a model created with `BasicDataModel(User)`, the return values are:
41
46
 
@@ -47,11 +52,11 @@ For a model created with `BasicDataModel(User)`, the return values are:
47
52
  | `update(...)` | `Promise<number>` | Number of modified documents. |
48
53
  | `delete(id)` | `Promise<number>` | Number of deleted documents. |
49
54
 
50
- Use `model.table` to compose AQL queries before execution. The static `fromDatabase()` conversion can return `undefined`; `toDatabase()` returns a plain `Record<string, any>`, not a saved record or a write result.
55
+ Use `model.table` to compose an AQL query before you run it. The static `fromDatabase()` conversion returns `undefined` when the record is `null` or `undefined`, as `table.get` gives for a missing ID. The static `toDatabase()` returns a `StoredRow<T>`, the record to store, not a saved record or a write result.
51
56
 
52
57
  ### Read
53
58
 
54
- ```typescript
59
+ ```typescript fragment
55
60
  // Get by primary key
56
61
  const user = await userModel.get("user-123");
57
62
 
@@ -64,7 +69,7 @@ const allUsers = await userModel.getAll();
64
69
 
65
70
  ### Create
66
71
 
67
- ```typescript
72
+ ```typescript fragment
68
73
  // Insert a single document
69
74
  const ids = await userModel.insert({
70
75
  email: "alice@example.com",
@@ -80,7 +85,7 @@ const batchIds = await userModel.insert([
80
85
 
81
86
  ### Update
82
87
 
83
- ```typescript
88
+ ```typescript fragment
84
89
  // Update by ID
85
90
  await userModel.update("user-123", { name: "Updated Name" });
86
91
 
@@ -88,17 +93,17 @@ await userModel.update("user-123", { name: "Updated Name" });
88
93
  await userModel.update({ _id: "user-123", name: "Updated Name" });
89
94
  ```
90
95
 
91
- > **Important:** When calling `update` without a separate ID argument, the object must include the primary key field.
96
+ When you call `update` without a separate ID argument, the object must include the primary key. Its type requires `_id`, and a call without it throws `Missing primary key in object update.`
92
97
 
93
98
  ### Delete
94
99
 
95
- ```typescript
100
+ ```typescript fragment
96
101
  await userModel.delete("user-123");
97
102
  ```
98
103
 
99
- ## Data Conversion
104
+ ## Data conversion
100
105
 
101
- Data models include static methods for converting between different data representations:
106
+ A model class has static methods that convert between the forms of a row:
102
107
 
103
108
  | Starting value | Conversion | Result | Persists data? |
104
109
  | ----------------------- | --------------- | ------------------------------------------------- | -------------- |
@@ -106,15 +111,15 @@ Data models include static methods for converting between different data represe
106
111
  | Raw database record | `fromDatabase` | Table instance with read modifier events applied | No |
107
112
  | Table instance | `toDatabase` | Plain storage representation | No |
108
113
 
109
- Use `insert` or `update` to perform a write; conversion alone does not save anything. Reads through `get`, `getBy`, and `getAll` apply `fromDatabase` for you. A custom query through `model.table` returns AQL results, so apply conversion explicitly when you need model instances.
114
+ Use `insert` or `update` to write. A conversion alone saves nothing. Reads through `get`, `getBy`, and `getAll` apply `fromDatabase` for you. A custom query through `model.table` returns AQL results, so apply conversion explicitly when you need model instances.
110
115
 
111
- ![Model writes convert input, trigger insert or update events, convert to storage, then execute AQL. Model reads convert fetched records; direct model.table queries bypass model conversion.](./assets/model-conversion.svg){width="480"}
116
+ ![Model writes convert input, trigger insert or update events, convert to storage, then execute AQL. Model reads convert fetched records. Direct model.table queries bypass model conversion.](./assets/model-conversion.svg "illustration"){width="480"}
112
117
 
113
118
  ### `fromPlainData(obj)`
114
119
 
115
- Convert a plain JavaScript object into a Table class instance. Modifier transformations (like encryption and hashing) are applied during this conversion.
120
+ Converts a plain JavaScript object into an instance of the table class. Each key is assigned, so modifiers such as encryption and hashing transform the values during this conversion. An instance of the table class is returned as it is.
116
121
 
117
- ```typescript
122
+ ```typescript fragment
118
123
  const user = UserModel.fromPlainData({
119
124
  _id: "user-123",
120
125
  email: "user@example.com",
@@ -122,56 +127,62 @@ const user = UserModel.fromPlainData({
122
127
  });
123
128
  ```
124
129
 
130
+ The object is a `PlainData<T>`. Every field is optional and may be partial, and a field under a container modifier may hold its value per key, as a localized field holds one text per locale. `toPlainData` gives the reverse form, a `PlainRow<T>`.
131
+
125
132
  ### `fromDatabase(obj)`
126
133
 
127
- Convert a raw database record into a Table class instance. This attaches the class prototype and triggers the `fromDatabase` modifier event.
134
+ Converts a stored record into an instance of the table class. It copies the record, sets the class prototype on the copy, and triggers the `fromDatabase` modifier event. It returns `undefined` when the record is `null` or `undefined`, as `table.get` gives for a missing ID. An empty object still gives an instance.
128
135
 
129
- ```typescript
136
+ ```typescript fragment
130
137
  const user = UserModel.fromDatabase(rawDbRecord);
131
138
  ```
132
139
 
133
140
  ### `toDatabase(obj)`
134
141
 
135
- Convert a Table class instance into a plain object suitable for database storage. Modifier events like `toDatabase` are triggered.
142
+ Converts an instance of the table class, or plain data, into the record to store, and triggers the `toDatabase` modifier event. The result is a `StoredRow<T>`. It holds the fields that are set, has no prototype, and keeps its modified fields locked in `_internal` instead of at the top level.
136
143
 
137
- ```typescript
144
+ ```typescript fragment
138
145
  const dbReady = UserModel.toDatabase(userInstance);
139
146
  ```
140
147
 
141
148
  ## Validation
142
149
 
143
- `Model.validate(obj)` runs every io-ts codec attached via `@Field` against the matching property of `obj`. String-token fields and properties without `@Field` are skipped.
150
+ `Model.validate(obj)` runs every io-ts codec that `@Field` attached against the matching property of `obj`. It skips string-token fields and properties without `@Field`. A failure's `message` is the first message the codec reports, or `invalid <field>` when the codec reports none, as io-ts's built-in codecs do.
144
151
 
145
- ```typescript
152
+ ```typescript fragment
146
153
  const result = UserModel.validate({ name: "Alice", age: 30 });
147
154
  if (!result.ok) {
148
- console.error(result.errors); // [{ field: "age", message: "not number" }, ...]
155
+ console.error(result.errors); // [{ field: "age", message: "invalid age" }]
149
156
  }
150
157
  ```
151
158
 
152
- Pass `{ validate: true }` to `insert` or `update` to gate the write on validation. On failure the call throws an `Error` whose `errors` property holds the per-field details; on success the original object passes through unchanged.
159
+ Pass `{ validate: true }` to `insert` or `update` to validate before the write. On failure the call throws an `Error` with the message `validation failed: <field> (<message>), ...` and an `errors` property that holds the details per field. On success the write goes on with the object unchanged.
153
160
 
154
- ```typescript
161
+ ```typescript fragment
155
162
  await userModel.insert({ name: "Alice", age: 30 }, { validate: true });
156
163
  await userModel.update("user-123", { name: "Bob" }, { validate: true });
157
164
  ```
158
165
 
159
- `insert` validates every annotated codec field. `update` is partial: only fields present on the patch object are checked, so omitting fields you don't intend to modify is fine. Use `Model.validate(obj, { partial: true })` for the same patch semantics outside of `update`.
166
+ `insert` validates every codec field of each row. `update` validates only the fields present on the patch object, so you can leave out the fields you do not change. For the same patch check outside `update`, use `Model.validate(obj, { partial: true })`.
160
167
 
161
- ## Extend BasicDataModel
168
+ ## Extend `BasicDataModel`
162
169
 
163
- Extend the generated model class to add custom query methods:
170
+ Extend the generated model class to add custom query methods.
164
171
 
165
- The `register` method below checks for an existing email to provide early feedback. It does not guarantee uniqueness under concurrent writes; enforce that constraint in the database where supported and handle conflicts.
172
+ The `register` method below checks for an existing email to report a duplicate early. It does not guarantee uniqueness under concurrent writes. Enforce that constraint in the database with a unique index where the database module supports it, and handle the conflict. `findActive` converts each row with `fromDatabase`, which returns `undefined` only when the record is `null` or `undefined`, and the filter drops a missing row.
166
173
 
167
- ```typescript
174
+ ```typescript fragment
168
175
  const UserModelBase = BasicDataModel(User, "users");
169
176
 
170
177
  class CustomUserModel extends UserModelBase {
171
178
  async findActive() {
172
179
  return this.table
173
180
  .filter((u) => u.key("status").eq("active"))
174
- .then((rows) => rows.map(CustomUserModel.fromDatabase) as User[]);
181
+ .then((rows) =>
182
+ rows
183
+ .map((row) => CustomUserModel.fromDatabase(row))
184
+ .filter((user) => user !== undefined),
185
+ );
175
186
  }
176
187
 
177
188
  async register(email: string, name: string) {
@@ -184,24 +195,24 @@ class CustomUserModel extends UserModelBase {
184
195
  }
185
196
  ```
186
197
 
187
- ## GetModel
198
+ ## `GetModel`
188
199
 
189
- The `GetModel` function retrieves or creates a cached model instance for a given schema:
200
+ The `GetModel` function returns the cached model instance for a model class and an instance ID, and creates it on the first call:
190
201
 
191
- ```typescript
202
+ ```typescript fragment
192
203
  import { GetModel } from "@antelopejs/interface-database-decorators";
193
204
 
194
205
  const userModel = GetModel(UserModel, "instance-id");
195
206
  ```
196
207
 
197
- Model instances are cached by class and instance ID, so repeated calls with the same arguments return the same instance. A model built by `BasicDataModel` uses the schema its table class was bound to by `RegisterSchema`, so it stays on the declaring module's database even when another module calls `GetModel`; an unbound table class throws. A custom model class without a `tableClass` is resolved from its `schemaName` with `Schema.get`.
208
+ The cache key is the class and the instance ID, so repeated calls with the same arguments return the same instance. Without an instance ID, the model uses the default instance. A model built by `BasicDataModel` uses the schema that `RegisterSchema` bound its table class to, so it stays on the declaring module's database even when another module calls `GetModel`. An unbound table class whose schema name has no schema throws `<Class> is not bound to a schema`. A custom model class without a `tableClass` gets its schema from its `schemaName` through `Schema.get`.
198
209
 
199
- ## Modifier Integration
210
+ ## Modifier integration
200
211
 
201
- Data models automatically apply all modifier transformations:
212
+ Data models apply every modifier transformation:
202
213
 
203
- - **On insert** - `fromPlainData` conversion is applied, the `insert` event fires, and the result is converted with `toDatabase` before writing.
204
- - **On update** - The same conversion pipeline applies with the `update` event.
205
- - **On read** - Results from `get`, `getBy`, and `getAll` are converted with `fromDatabase`.
214
+ - On insert, the model converts the data with `fromPlainData`, fires the `insert` event, and converts the result with `toDatabase` before the write.
215
+ - On update, the model runs the same conversions with the `update` event. It leaves out an empty `_internal`, so a patch keeps what the row stores there.
216
+ - On read, the model converts the results of `get`, `getBy`, and `getAll` with `fromDatabase`.
206
217
 
207
- This means encrypted fields are encrypted before storage and decrypted on retrieval, hashed fields are hashed on write, and localized fields follow their configured locale behavior -- all transparently.
218
+ As a result, encrypted fields are encrypted before storage and decrypted on read, hashed fields are hashed on write, and localized fields follow their locale settings.
@@ -1,16 +1,20 @@
1
- # Parameter Decoration
1
+ ---
2
+ title: Parameter decoration
3
+ description: Inject a data model into an API controller method or property with the Model decorator, with a fixed or a per-request instance ID.
4
+ mode: how-to
5
+ ---
2
6
 
3
- The Database Decorators package provides the `Model` decorator for injecting data model instances into API controller methods. This decorator integrates with the `@antelopejs/interface-api` package's parameter provider system.
7
+ The `Model` decorator injects a data model instance into an API controller method. It registers a parameter provider of `@antelopejs/interface-api` through `SetParameterProvider`.
4
8
 
5
- ## The Model Decorator
9
+ ## The `Model` decorator
6
10
 
7
- The `Model` decorator injects a cached model instance. It can be used as either a parameter decorator or a property decorator.
11
+ The `Model` decorator injects the cached model instance that `GetModel` returns. It works as a parameter decorator and as a property decorator.
8
12
 
9
- ### Static Instance ID
13
+ ### Pass a fixed instance
10
14
 
11
- Pass a fixed instance ID to bind the model to a specific schema instance:
15
+ To bind the model to one schema instance, pass a fixed instance ID:
12
16
 
13
- ```typescript
17
+ ```typescript fragment
14
18
  import { Controller, Get } from "@antelopejs/interface-api";
15
19
  import { Model } from "@antelopejs/interface-database-decorators";
16
20
  import { UserModel } from "./user.model";
@@ -25,13 +29,13 @@ class UsersController extends Controller("/users") {
25
29
  }
26
30
  ```
27
31
 
28
- ### Dynamic Instance ID
32
+ ### Pick the instance per request
29
33
 
30
- Pass a callback function to determine the instance ID at request time. The callback receives the request context:
34
+ To pick the instance ID for each request, pass a callback. It receives the request context and returns an instance ID, or `undefined` for the default instance.
31
35
 
32
- Authorize access to the requested tenant before reading or writing through the injected model. The callback selects an instance; a URL parameter alone is not proof that the caller belongs to that tenant.
36
+ Authorize access to the requested tenant before you read or write through the injected model. The callback only selects an instance, and a URL parameter alone does not prove that the caller belongs to that tenant.
33
37
 
34
- ```typescript
38
+ ```typescript fragment
35
39
  import { Controller, Get, Parameter } from "@antelopejs/interface-api";
36
40
  import { Model } from "@antelopejs/interface-database-decorators";
37
41
  import { UserModel } from "./user.model";
@@ -40,7 +44,7 @@ class MultitenantController extends Controller("/tenants") {
40
44
  @Get("/:tenantId/users")
41
45
  async getTenantUsers(
42
46
  @Parameter("tenantId") tenantId: string,
43
- @Model(UserModel, (ctx) => `tenant-${ctx.params.tenantId}`)
47
+ @Model(UserModel, (ctx) => `tenant-${ctx.routeParameters.tenantId}`)
44
48
  userModel: InstanceType<typeof UserModel>,
45
49
  ) {
46
50
  return await userModel.getAll();
@@ -48,11 +52,11 @@ class MultitenantController extends Controller("/tenants") {
48
52
  }
49
53
  ```
50
54
 
51
- ## Property Decoration
55
+ ## Property decoration
52
56
 
53
- The `Model` decorator also works as a property decorator. The model is initialized when the controller handles a request:
57
+ The `Model` decorator also works as a property decorator. The property gets its model when the controller handles a request. Outside a request handler, the property is unset, although the `!` declaration types it as always present:
54
58
 
55
- ```typescript
59
+ ```typescript fragment
56
60
  import { Controller, Get, Parameter } from "@antelopejs/interface-api";
57
61
  import { Model } from "@antelopejs/interface-database-decorators";
58
62
  import { UserModel } from "./models";
@@ -73,11 +77,11 @@ class UsersController extends Controller("/users") {
73
77
  }
74
78
  ```
75
79
 
76
- ## Model Caching
80
+ ## Model caching
77
81
 
78
- The `Model` decorator uses `GetModel` internally, which caches model instances by class and instance ID. Multiple decorators referencing the same model class and instance ID share a single instance:
82
+ The `Model` decorator calls `GetModel`, which caches model instances by class and instance ID. Decorators that name the same model class and instance ID share one instance:
79
83
 
80
- ```typescript
84
+ ```typescript fragment
81
85
  import { GetModel } from "@antelopejs/interface-database-decorators";
82
86
 
83
87
  // These return the same instance
@@ -88,13 +92,13 @@ const model2 = GetModel(UserModel, "main");
88
92
  const model3 = GetModel(UserModel, "other");
89
93
  ```
90
94
 
91
- ## Combine with API Decorators
95
+ ## Combine with API decorators
92
96
 
93
- Model decorators compose naturally with other API decorators for building complete CRUD controllers:
97
+ `@Model` combines with the other API decorators in a CRUD controller.
94
98
 
95
- This example demonstrates injection, not a complete public API policy. Before exposing writes, reject malformed or non-object JSON, allow only permitted fields, validate their values, and authorize the operation. `JSON.parse` and `@Model` do not provide these checks; string-token `@Field` declarations do not validate input either.
99
+ This example shows injection, not a complete public API policy. Before you expose writes, reject malformed or non-object JSON, allow only permitted fields, validate their values, and authorize the operation. `JSON.parse` and `@Model` do none of these checks, and string-token `@Field` declarations do not validate input either.
96
100
 
97
- ```typescript
101
+ ```typescript fragment
98
102
  import {
99
103
  Controller,
100
104
  Get,
@@ -150,9 +154,9 @@ class UsersController extends Controller("/users") {
150
154
  }
151
155
  ```
152
156
 
153
- ## Custom Parameter Providers
157
+ ## Custom parameter providers
154
158
 
155
- For more complex injection logic, create custom parameter providers using the core API:
159
+ For other injection logic, write your own decorator that registers a parameter provider:
156
160
 
157
161
  ```typescript
158
162
  import { SetParameterProvider } from "@antelopejs/interface-api";
@@ -2,6 +2,9 @@
2
2
  "extends": "../tsconfig.json",
3
3
  "compilerOptions": {
4
4
  "rootDir": "..",
5
+ "module": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "declaration": false,
5
8
  "noEmit": true,
6
9
  "composite": false,
7
10
  "incremental": false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/interface-database-decorators",
3
- "version": "0.1.11",
3
+ "version": "0.2.0",
4
4
  "description": "AntelopeJS decorators for database tables and typed data models, with encrypted, hashed and localized fields",
5
5
  "keywords": [
6
6
  "antelopejs",
@@ -131,9 +131,7 @@
131
131
  "prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi",
132
132
  "checks": "antelopejs-checks",
133
133
  "size": "antelopejs-check-size",
134
- "ratchet": "antelopejs-ratchet",
135
134
  "sync": "antelopejs-sync",
136
- "ratchet:update": "antelopejs-ratchet --update",
137
135
  "sync:check": "antelopejs-sync --check",
138
136
  "typecheck": "tsc --noEmit",
139
137
  "suppressions": "antelopejs-suppressions",
@@ -144,11 +142,11 @@
144
142
  "reflect-metadata": "^0.2.2"
145
143
  },
146
144
  "devDependencies": {
147
- "@antelopejs/core": "^1.16.0",
145
+ "@antelopejs/core": "^1.20.0",
148
146
  "@antelopejs/interface-api": "^0.0.15",
149
- "@antelopejs/interface-core": "^0.2.4",
150
- "@antelopejs/interface-database": "^0.1.11",
151
- "@antelopejs/tooling-configs": "^0.2.11",
147
+ "@antelopejs/interface-core": "^0.4.0",
148
+ "@antelopejs/interface-database": "^0.3.0",
149
+ "@antelopejs/tooling-configs": "^0.2.17",
152
150
  "@types/chai": "^4.3.20",
153
151
  "@types/mocha": "^10.0.10",
154
152
  "@types/node": "^22.19.15",
@@ -172,12 +170,15 @@
172
170
  "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
173
171
  "@antelopejs/interface-database": ">=0.1.11 <1.0.0"
174
172
  },
175
- "packageManager": "pnpm@10.6.5",
173
+ "packageManager": "pnpm@10.33.0",
176
174
  "antelopeJs": {
177
175
  "standalone": true,
178
176
  "test": "src/antelope.test.ts",
179
177
  "skills": [
180
178
  "./skills"
181
- ]
179
+ ],
180
+ "docs": {
181
+ "standard": true
182
+ }
182
183
  }
183
184
  }
@@ -110,7 +110,7 @@ class Chef extends Table.with(
110
110
  EncryptionModifier,
111
111
  LocalizationModifier,
112
112
  ) {
113
- @Hashed() declare passphrase: string; // one-way; test via instance.testHash("passphrase", value)
113
+ @Hashed() declare passphrase: string; // one-way scrypt; test via await instance.testHashAsync("passphrase", value)
114
114
  @Encrypted({ secretKey }) declare supplierCode: string; // transparent encrypt/decrypt (autolock + autounlock)
115
115
  @Localized() declare motto: string; // read AND write via instance.localize("en", ["motto"])
116
116
  @CreationTime() declare hiredAt: Date; // set on insert, stripped from updates
@@ -124,7 +124,9 @@ class Chef extends Table.with(
124
124
  is loaded once before decorators run (e.g. `import "@antelopejs/interface-database-decorators"`).
125
125
  - `Hashed`, `Encrypted`, and `Localized` require the matching modifier class in `Table.with(...)`; `CreationTime`/`UpdateTime` are event-only and need no mixin.
126
126
  - Modifier ordering matters: `attachModifier` throws if you stack a modifier after a one-way modifier (e.g. anything after `@Hashed` on the same field).
127
- - Hashed fields cannot be read back or queried directly — only equality-tested with `testHash`.
127
+ - Hashed fields cannot be read back or queried directly — only equality-tested with `testHashAsync`, or `testHash`, which blocks the event loop for one scrypt run.
128
+ - A row can hold an older hash that still verifies. After a successful test, when `instance.isHashOutdated(field)` is true, assign the tested value and save the row.
129
+ - `@Hashed({ algorithm })` stores a fast digest instead of scrypt: use it only for non-secret fingerprints, never for credentials.
128
130
  - Localized fields must be written through a localized instance (`instance.localize(locale).field = ...`);
129
131
  values assigned before `localize()` are held in floating state and silently dropped on insert/update.
130
132
  - A localized value `""` is stored as no value: reads and queries in that locale get the `fallbackLocale` value.
@@ -132,7 +134,8 @@ class Chef extends Table.with(
132
134
  - `Model.validate` (and `{ validate: true }` on insert/update) only checks fields whose `@Field` type is an
133
135
  io-ts-style codec with `.decode`; plain string field tokens are skipped. Update validates with
134
136
  `{ partial: true }` semantics.
135
- - `update(obj)` without an explicit id asserts that the object carries the primary key (`_id` by default).
137
+ - `update(obj)` without an explicit id takes an object typed with the primary key `_id`, and asserts at run time that it carries it.
138
+ - A row has three forms. `fromPlainData` reads a `PlainData<T>` (every field optional, a localized field possibly one text per locale), `toPlainData` gives a `PlainRow<T>`, and `toDatabase` gives a `StoredRow<T>`, its modified fields locked in `_internal`. `fromDatabase` takes the row the database answered, unchecked.
136
139
  - Indexes are scoped to the schema instance. Add `@Index({ crossInstance: true })` only for indexes also queried
137
140
  across all instances (`CROSS_INSTANCE`); it never changes results, only speed. On a group, one member setting it
138
141
  applies it to the whole group.