@antelopejs/interface-database-decorators 0.1.12 → 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.
- package/README.md +11 -9
- package/dist/antelope.test.js +1 -1
- package/dist/common.d.ts +48 -2
- package/dist/common.js +51 -5
- package/dist/common.js.map +1 -1
- package/dist/database.js +1 -2
- package/dist/database.js.map +1 -1
- package/dist/internal/bound-schema.d.ts +8 -0
- package/dist/internal/bound-schema.js +14 -0
- package/dist/internal/bound-schema.js.map +1 -0
- package/dist/model.d.ts +21 -8
- package/dist/model.js +10 -7
- package/dist/model.js.map +1 -1
- package/dist/modifiers/autodate.d.ts +2 -2
- package/dist/modifiers/autodate.js.map +1 -1
- package/dist/modifiers/common.d.ts +37 -20
- package/dist/modifiers/common.js +77 -60
- package/dist/modifiers/common.js.map +1 -1
- package/dist/modifiers/encryption.js +26 -14
- package/dist/modifiers/encryption.js.map +1 -1
- package/dist/modifiers/hash.js +4 -1
- package/dist/modifiers/hash.js.map +1 -1
- package/dist/modifiers/internal/kinds.d.ts +14 -1
- package/dist/modifiers/internal/kinds.js +31 -0
- package/dist/modifiers/internal/kinds.js.map +1 -1
- package/dist/modifiers/internal/records.d.ts +40 -7
- package/dist/modifiers/internal/records.js +69 -14
- package/dist/modifiers/internal/records.js.map +1 -1
- package/dist/modifiers/localization.js +9 -2
- package/dist/modifiers/localization.js.map +1 -1
- package/dist/relation.d.ts +5 -0
- package/dist/relation.js +11 -0
- package/dist/relation.js.map +1 -1
- package/dist/schema.js +5 -6
- package/dist/schema.js.map +1 -1
- package/dist/table.d.ts +3 -2
- package/dist/table.js +27 -12
- package/dist/table.js.map +1 -1
- package/dist/tests/cross_instance.test.js +5 -2
- package/dist/tests/cross_instance.test.js.map +1 -1
- package/dist/tests/database.test.js +42 -34
- package/dist/tests/database.test.js.map +1 -1
- package/dist/tests/helpers/other-instance.js.map +1 -1
- package/dist/tests/integration.test.js +15 -4
- package/dist/tests/integration.test.js.map +1 -1
- package/dist/tests/metadata_inheritance.test.d.ts +1 -0
- package/dist/tests/metadata_inheritance.test.js +212 -0
- package/dist/tests/metadata_inheritance.test.js.map +1 -0
- package/dist/tests/modifiers/encryption_date_marker.test.d.ts +1 -0
- package/dist/tests/modifiers/encryption_date_marker.test.js +78 -0
- package/dist/tests/modifiers/encryption_date_marker.test.js.map +1 -0
- package/dist/tests/modifiers/inherited_names.test.d.ts +1 -0
- package/dist/tests/modifiers/inherited_names.test.js +96 -0
- package/dist/tests/modifiers/inherited_names.test.js.map +1 -0
- package/dist/tests/own_key_lookups.test.d.ts +1 -0
- package/dist/tests/own_key_lookups.test.js +49 -0
- package/dist/tests/own_key_lookups.test.js.map +1 -0
- package/dist/tests/row_forms.test.d.ts +1 -0
- package/dist/tests/row_forms.test.js +59 -0
- package/dist/tests/row_forms.test.js.map +1 -0
- package/dist/tests/stored_metadata.test.d.ts +1 -0
- package/dist/tests/stored_metadata.test.js +91 -0
- package/dist/tests/stored_metadata.test.js.map +1 -0
- package/dist/tests/subclass_schema_binding.test.d.ts +1 -0
- package/dist/tests/subclass_schema_binding.test.js +115 -0
- package/dist/tests/subclass_schema_binding.test.js.map +1 -0
- package/dist/tests/table.test.js +16 -0
- package/dist/tests/table.test.js.map +1 -1
- package/docs/1.introduction.md +24 -20
- package/docs/2.table-definitions.md +63 -41
- package/docs/3.table-modifiers.md +113 -104
- package/docs/4.data-models.md +63 -52
- package/docs/5.parameter-decoration.md +28 -24
- package/docs/tsconfig.json +3 -0
- package/package.json +9 -8
- package/skills/database-decorators-interface/SKILL.md +2 -1
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Table modifiers
|
|
3
|
+
description: Reference for the encryption, hashing, localization, and auto-date modifiers, the scrypt hash format, and the classes and events for writing a custom modifier.
|
|
4
|
+
mode: reference
|
|
5
|
+
---
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
A table modifier transforms a field's value on its way into a row and on its way out. The built-in modifiers encrypt, hash, or localize a field, or set a timestamp. Add a modifier's mixin to a table class with `Table.with()`, then mark each field with the modifier's decorator.
|
|
4
8
|
|
|
5
|
-
## Built-in
|
|
9
|
+
## Built-in modifiers
|
|
6
10
|
|
|
7
|
-
### EncryptionModifier
|
|
11
|
+
### `EncryptionModifier`
|
|
8
12
|
|
|
9
|
-
The `EncryptionModifier` encrypts field
|
|
13
|
+
The `EncryptionModifier` encrypts a field's value when the value is assigned and decrypts it when the field is read.
|
|
10
14
|
|
|
11
|
-
The default `aes-256-gcm` algorithm
|
|
15
|
+
The default `aes-256-gcm` algorithm needs a key of exactly 32 bytes. `secretKey` goes to Node's cipher as a UTF-8 string, without hex or base64 decoding. Supply a high-entropy, 32-byte UTF-8 value from a secret manager, keep it stable while ciphertext exists, and fail startup if it is missing or invalid. A 64-character hex encoding is 64 bytes for this API, not a 32-byte key.
|
|
12
16
|
|
|
13
17
|
```ts docs-check
|
|
14
18
|
import {
|
|
@@ -41,13 +45,13 @@ class UserCredentials extends Table.with(EncryptionModifier) {
|
|
|
41
45
|
}
|
|
42
46
|
```
|
|
43
47
|
|
|
44
|
-
#### How
|
|
48
|
+
#### How it works
|
|
45
49
|
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
- Date
|
|
50
|
+
- An assignment encodes the value as JSON and encrypts it with the configured algorithm. The row stores `[ciphertext, iv, authTag]`, each in base64.
|
|
51
|
+
- Each write generates a random initialization vector (IV).
|
|
52
|
+
- An authenticated algorithm, such as `aes-256-gcm`, also stores its auth tag. Other algorithms store no tag.
|
|
53
|
+
- A read decrypts the value.
|
|
54
|
+
- A `Date` comes back as a `Date` instance. The encoder writes it with `toUTCString()`, which drops milliseconds, so an encrypted date keeps whole-second precision only.
|
|
51
55
|
|
|
52
56
|
#### Options
|
|
53
57
|
|
|
@@ -57,9 +61,9 @@ class UserCredentials extends Table.with(EncryptionModifier) {
|
|
|
57
61
|
| `algorithm` | `string` | `"aes-256-gcm"` | Encryption algorithm. |
|
|
58
62
|
| `ivSize` | `number` | `16` | Initialization vector size in bytes. |
|
|
59
63
|
|
|
60
|
-
### HashModifier
|
|
64
|
+
### `HashModifier`
|
|
61
65
|
|
|
62
|
-
The `HashModifier` stores a salted one-way hash of field
|
|
66
|
+
The `HashModifier` stores a salted one-way hash of a field's value. A hash cannot be reversed, so the original value cannot be read back. By default the field stores a scrypt hash, a memory-hard function made for passwords.
|
|
63
67
|
|
|
64
68
|
```typescript
|
|
65
69
|
import {
|
|
@@ -83,39 +87,43 @@ class User extends Table.with(HashModifier) {
|
|
|
83
87
|
}
|
|
84
88
|
```
|
|
85
89
|
|
|
86
|
-
#### How
|
|
90
|
+
#### How it works
|
|
87
91
|
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
92
|
+
- The field hashes the value encoded as JSON, so `"secret"` is hashed as the text `"\"secret\""`.
|
|
93
|
+
- Each write draws a random 16-byte salt and stores `$scrypt$ln=15,r=8,p=3$<salt>$<key>`. The salt and the 32-byte key are base64 without padding. `ln` is the base-2 logarithm of the scrypt cost N, so N is 32768. The string holds the parameters, the salt, and the key, so the cost can change later.
|
|
94
|
+
- A write is synchronous, because a property setter cannot wait, and so are the conversions behind a model's `insert` and `update`. Each assignment blocks the event loop for one scrypt run, which uses 32 MiB (128 × N × r bytes).
|
|
95
|
+
- Assigning the stored hash back to the field, or calling `lock` on a loaded row, keeps the hash as it is. Assigning `undefined` stores nothing.
|
|
96
|
+
- `testHash` and `testHashAsync` read `ln`, `r`, `p`, the salt, and the key from the stored string. They derive a key of the stored key's length from the candidate and compare the two with `crypto.timingSafeEqual`, in constant time.
|
|
97
|
+
- A stored scrypt string verifies only within these limits: `ln`, `r`, and `p` from 1 to 99, at most 128 MiB of memory, `p` at most 16, `ln` below 16 × `r`, and a salt and a key of 16 to 64 bytes each. A string outside them refuses every candidate.
|
|
98
|
+
- On a field without an `algorithm`, a test spends one scrypt run whatever the row holds, a hash in another format or nothing included. The time of a refusal does not tell whether the row holds a current hash.
|
|
93
99
|
|
|
94
100
|
#### Options
|
|
95
101
|
|
|
96
|
-
| Option | Type | Default | Description
|
|
97
|
-
| ----------- | -------- | ------- |
|
|
98
|
-
| `algorithm` | `string` | none | Digest to store instead of scrypt, any name `createHash` accepts. A digest is fast to guess
|
|
102
|
+
| Option | Type | Default | Description |
|
|
103
|
+
| ----------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
104
|
+
| `algorithm` | `string` | none | Digest to store instead of scrypt, any name `createHash` accepts. A digest is fast to guess, so use it only for non-secret fingerprints, never for credentials. See the next list. |
|
|
99
105
|
|
|
100
|
-
|
|
106
|
+
A field with an `algorithm` stores the base64 digest of the row's salt followed by the JSON-encoded value. The salt is a random string that the first such write creates. The row keeps it in `_internal.meta.HashModifier.salt` and reuses it for every later write to its digest fields. A test of a digest field that holds a digest spends no scrypt run. The field still verifies a scrypt string it holds, with one scrypt run, and `isHashOutdated` never flags it, so a scrypt hash is never flagged for replacement by a weaker digest.
|
|
107
|
+
|
|
108
|
+
#### Mixin methods
|
|
101
109
|
|
|
102
110
|
The `HashModifier` mixin adds these methods to table instances:
|
|
103
111
|
|
|
104
|
-
```typescript
|
|
105
|
-
testHash(field:
|
|
106
|
-
testHashAsync(field:
|
|
107
|
-
isHashOutdated(field:
|
|
112
|
+
```typescript fragment
|
|
113
|
+
testHash<This extends {}, F extends keyof This>(this: This, field: F, value: This[F]): unknown
|
|
114
|
+
testHashAsync<This extends {}, F extends keyof This>(this: This, field: F, value: This[F]): Promise<boolean>
|
|
115
|
+
isHashOutdated<This extends {}>(this: This, field: keyof This): boolean
|
|
108
116
|
```
|
|
109
117
|
|
|
110
|
-
- `testHashAsync`
|
|
111
|
-
- `testHash`
|
|
112
|
-
- `isHashOutdated` returns whether the stored hash differs from what the field writes today
|
|
118
|
+
- `testHashAsync` resolves whether `value` matches the stored hash. It runs scrypt on the thread pool, so use it on a server. It rejects with `Field <name> is not hashed` for a field without `@Hashed`.
|
|
119
|
+
- `testHash` gives the same result and blocks the event loop while scrypt runs. Its declared return type is `unknown`. On a hashed field, the value is a boolean.
|
|
120
|
+
- `isHashOutdated` returns whether the stored hash differs from what the field writes today, which is any stored string that does not start with `$scrypt$ln=15,r=8,p=3$`. It returns `false` when nothing is stored, and always on a field with an `algorithm`. It throws `Field <name> is not hashed` for a field without `@Hashed`.
|
|
113
121
|
|
|
114
|
-
#### Upgrade
|
|
122
|
+
#### Upgrade stored hashes
|
|
115
123
|
|
|
116
|
-
A row
|
|
124
|
+
A row whose hash has another format still verifies. On a field without an `algorithm`, a string that is not a scrypt hash is tested as a salted SHA-256 digest, made with the salt in `_internal.meta.HashModifier.salt`, and `isHashOutdated` returns `true` for it. A scrypt hash of another cost verifies with its own parameters and is flagged too. To rewrite a flagged hash, assign the value after a successful test, because only then is the plaintext known:
|
|
117
125
|
|
|
118
|
-
```typescript
|
|
126
|
+
```typescript fragment
|
|
119
127
|
if (await user.testHashAsync("password", password)) {
|
|
120
128
|
if (user.isHashOutdated("password")) {
|
|
121
129
|
user.password = password;
|
|
@@ -124,13 +132,13 @@ if (await user.testHashAsync("password", password)) {
|
|
|
124
132
|
}
|
|
125
133
|
```
|
|
126
134
|
|
|
127
|
-
|
|
135
|
+
`userModel.update(user._id, { password })` rewrites the hash too. An update that leaves the hashed field out, by ID or with the whole row, keeps the stored hash.
|
|
128
136
|
|
|
129
|
-
|
|
137
|
+
To move a credential off `@Hashed({ algorithm: "sha256" })`, remove the option. Its rows keep verifying, and `isHashOutdated` flags them. A row written with another digest verifies only while the field declares that digest.
|
|
130
138
|
|
|
131
|
-
### LocalizationModifier
|
|
139
|
+
### `LocalizationModifier`
|
|
132
140
|
|
|
133
|
-
The `LocalizationModifier` stores
|
|
141
|
+
The `LocalizationModifier` stores one value of a field per locale. To read or write a locale, call `localize()` on the table instance.
|
|
134
142
|
|
|
135
143
|
```typescript
|
|
136
144
|
import {
|
|
@@ -154,9 +162,11 @@ class Product extends Table.with(LocalizationModifier) {
|
|
|
154
162
|
}
|
|
155
163
|
```
|
|
156
164
|
|
|
165
|
+
`LocalizationModifier.fallbackLocaleOf(TableClass, field)` returns a field's `fallbackLocale`, or `undefined` when the field is not localized or declares none. Generated forms use it to require a value in that locale.
|
|
166
|
+
|
|
157
167
|
#### Usage
|
|
158
168
|
|
|
159
|
-
```typescript
|
|
169
|
+
```typescript fragment
|
|
160
170
|
const product = new Product();
|
|
161
171
|
product._id = "prod-123";
|
|
162
172
|
product.price = 99.99;
|
|
@@ -170,30 +180,29 @@ product.localize("es").name = "Auriculares Premium";
|
|
|
170
180
|
const spanish = product.localize("es");
|
|
171
181
|
console.log(spanish.name); // "Auriculares Premium"
|
|
172
182
|
|
|
173
|
-
//
|
|
183
|
+
// Fall back to the default locale when a translation is missing
|
|
174
184
|
const german = product.localize("de");
|
|
175
185
|
console.log(german.name); // "Premium Headphones" (fallback)
|
|
176
186
|
```
|
|
177
187
|
|
|
178
|
-
#### Wildcard
|
|
188
|
+
#### Wildcard key
|
|
179
189
|
|
|
180
|
-
The wildcard key `"*"`
|
|
190
|
+
The wildcard key `"*"` reads or writes all translations at once. `localize()` does not change the property's TypeScript type, so a field declared as `string` holds a record of texts by locale under `"*"`. Read that record as `unknown` and check it before use. Write it with `Object.assign`, which calls the field's setter:
|
|
181
191
|
|
|
182
|
-
```typescript
|
|
192
|
+
```typescript fragment
|
|
183
193
|
// Get all translations
|
|
184
|
-
const
|
|
185
|
-
name: Record<string, string>;
|
|
186
|
-
};
|
|
187
|
-
const allNames = translations.name;
|
|
194
|
+
const allNames: unknown = product.localize("*").name;
|
|
188
195
|
// { en: "Premium Headphones", fr: "Casque Premium", es: "Auriculares Premium" }
|
|
189
196
|
|
|
190
197
|
// Set all translations
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
198
|
+
Object.assign(product.localize("*"), {
|
|
199
|
+
name: {
|
|
200
|
+
en: "Premium Headphones",
|
|
201
|
+
fr: "Casque Premium",
|
|
202
|
+
es: "Auriculares Premium",
|
|
203
|
+
de: "Premium-Kopfhoerer",
|
|
204
|
+
},
|
|
205
|
+
});
|
|
197
206
|
```
|
|
198
207
|
|
|
199
208
|
#### Options
|
|
@@ -202,23 +211,23 @@ translations.name = {
|
|
|
202
211
|
| ---------------- | -------- | --------------------------------------------------------------------- |
|
|
203
212
|
| `fallbackLocale` | `string` | Locale to use when the requested locale has no value or an empty one. |
|
|
204
213
|
|
|
205
|
-
#### Mixin
|
|
214
|
+
#### Mixin method
|
|
206
215
|
|
|
207
216
|
The `LocalizationModifier` mixin adds a `localize` method:
|
|
208
217
|
|
|
209
|
-
```typescript
|
|
210
|
-
localize(locale: string, fields?: Array<keyof
|
|
218
|
+
```typescript fragment
|
|
219
|
+
localize<This extends { constructor: any }>(this: This, locale: string, fields?: Array<keyof This>): This
|
|
211
220
|
```
|
|
212
221
|
|
|
213
|
-
`localize()` changes the active locale and returns the same object, not a locale
|
|
222
|
+
`localize()` changes the active locale and returns the same object, not a copy for that locale.
|
|
214
223
|
|
|
215
224
|
- **References share state.** `spanish`, `german`, and `product` above refer to one object. A later `localize()` call changes what all three references read.
|
|
216
|
-
- **Fields can be restricted.** When you supply `fields`, only those fields
|
|
217
|
-
- **Copy values before switching.** Save the values you need before
|
|
225
|
+
- **Fields can be restricted.** When you supply `fields`, only those fields switch to the selected locale.
|
|
226
|
+
- **Copy values before switching.** Save the values you need before you change locales, and do not share one instance between concurrent operations on different locales.
|
|
218
227
|
|
|
219
|
-
### AutoDateModifier
|
|
228
|
+
### `AutoDateModifier`
|
|
220
229
|
|
|
221
|
-
The `AutoDateModifier`
|
|
230
|
+
The `AutoDateModifier` sets timestamp fields on insert and update. Mark the fields with the `CreationTime` and `UpdateTime` decorators.
|
|
222
231
|
|
|
223
232
|
```typescript
|
|
224
233
|
import {
|
|
@@ -238,15 +247,15 @@ class Article extends Table {
|
|
|
238
247
|
}
|
|
239
248
|
```
|
|
240
249
|
|
|
241
|
-
#### How
|
|
250
|
+
#### How it works
|
|
242
251
|
|
|
243
|
-
- On insert, both `CreationTime` and `UpdateTime` fields
|
|
244
|
-
- On update, `UpdateTime` fields
|
|
245
|
-
-
|
|
252
|
+
- On insert, both `CreationTime` and `UpdateTime` fields get the current date.
|
|
253
|
+
- On update, `UpdateTime` fields get the current date, and `CreationTime` fields are removed from the payload, so the stored creation date stays.
|
|
254
|
+
- The modifier only handles events. It does not transform stored values and has no mixin, so the table class needs no `Table.with()`.
|
|
246
255
|
|
|
247
|
-
## Combine
|
|
256
|
+
## Combine multiple modifiers
|
|
248
257
|
|
|
249
|
-
Pass
|
|
258
|
+
Pass several modifier classes to `Table.with()`:
|
|
250
259
|
|
|
251
260
|
```typescript
|
|
252
261
|
import {
|
|
@@ -287,11 +296,11 @@ class UserProfile extends Table.with(
|
|
|
287
296
|
}
|
|
288
297
|
```
|
|
289
298
|
|
|
290
|
-
## Create
|
|
299
|
+
## Create custom modifiers
|
|
291
300
|
|
|
292
|
-
### OneWayModifier
|
|
301
|
+
### `OneWayModifier`
|
|
293
302
|
|
|
294
|
-
A `OneWayModifier` transforms data in one direction
|
|
303
|
+
A `OneWayModifier` transforms data in one direction, when a value is written. Use it for a transformation that cannot be reversed, such as hashing.
|
|
295
304
|
|
|
296
305
|
```typescript
|
|
297
306
|
import {
|
|
@@ -332,18 +341,18 @@ const PrefixHashed = MakePropertyDecorator(
|
|
|
332
341
|
);
|
|
333
342
|
```
|
|
334
343
|
|
|
335
|
-
The generic parameters
|
|
344
|
+
The generic parameters of `OneWayModifier<LockedType, Args, Meta, Options>` are:
|
|
336
345
|
|
|
337
|
-
| Parameter | Description
|
|
338
|
-
| ------------ |
|
|
339
|
-
| `LockedType` | The type stored in the database after transformation
|
|
340
|
-
| `Args` | Additional arguments for `lock` and `test`
|
|
341
|
-
| `Meta` |
|
|
342
|
-
| `Options` |
|
|
346
|
+
| Parameter | Description |
|
|
347
|
+
| ------------ | -------------------------------------------------------------------------------------------------------- |
|
|
348
|
+
| `LockedType` | The type stored in the database after the transformation. |
|
|
349
|
+
| `Args` | Additional arguments for `lock` and `test`. Defaults to `[]`. |
|
|
350
|
+
| `Meta` | The modifier's state for a row, kept in `_internal.meta.<modifier class name>` and shared by its fields. |
|
|
351
|
+
| `Options` | The configuration that the decorator passes to `attachModifier`. |
|
|
343
352
|
|
|
344
|
-
### TwoWayModifier
|
|
353
|
+
### `TwoWayModifier`
|
|
345
354
|
|
|
346
|
-
A `TwoWayModifier` extends `OneWayModifier` with an `unlock` method for
|
|
355
|
+
A `TwoWayModifier` extends `OneWayModifier` with an `unlock` method, for a transformation that can be reversed, such as encryption:
|
|
347
356
|
|
|
348
357
|
```ts docs-check
|
|
349
358
|
import {
|
|
@@ -355,7 +364,7 @@ import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators";
|
|
|
355
364
|
import type { ValueProxy } from "@antelopejs/interface-database";
|
|
356
365
|
|
|
357
366
|
class Base64Modifier extends TwoWayModifier<
|
|
358
|
-
|
|
367
|
+
unknown,
|
|
359
368
|
[],
|
|
360
369
|
{},
|
|
361
370
|
{ prefix?: string }
|
|
@@ -363,23 +372,23 @@ class Base64Modifier extends TwoWayModifier<
|
|
|
363
372
|
public readonly autolock = true;
|
|
364
373
|
public readonly autounlock = true;
|
|
365
374
|
|
|
366
|
-
public override lock(_locked:
|
|
375
|
+
public override lock(_locked: unknown, value: unknown) {
|
|
367
376
|
if (value === undefined) return undefined;
|
|
368
377
|
const prefix = this.options.prefix || "b64-";
|
|
369
378
|
return `${prefix}${Buffer.from(JSON.stringify(value)).toString("base64")}`;
|
|
370
379
|
}
|
|
371
380
|
|
|
372
|
-
public override unlock(locked:
|
|
373
|
-
if (locked
|
|
381
|
+
public override unlock(locked: unknown): unknown {
|
|
382
|
+
if (typeof locked !== "string") return undefined;
|
|
374
383
|
const prefix = this.options.prefix || "b64-";
|
|
375
384
|
const encoded = locked.substring(prefix.length);
|
|
376
385
|
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
377
386
|
}
|
|
378
387
|
|
|
379
388
|
public override unlockrequest(
|
|
380
|
-
data: ValueProxy<
|
|
389
|
+
data: ValueProxy<unknown>,
|
|
381
390
|
): ValueProxy<unknown> {
|
|
382
|
-
return data
|
|
391
|
+
return data;
|
|
383
392
|
}
|
|
384
393
|
|
|
385
394
|
[MixinSymbol] = class {};
|
|
@@ -397,11 +406,11 @@ const Base64Encoded = MakePropertyDecorator(
|
|
|
397
406
|
);
|
|
398
407
|
```
|
|
399
408
|
|
|
400
|
-
Base64 is encoding, not encryption.
|
|
409
|
+
Base64 is an encoding, not encryption. The example declares its `LockedType` as `unknown`, so `unlockrequest()` can return the stored value's proxy as it is. Database expressions therefore see the encoded value, and only instance reads decode it. `unlock` reads a stored value that is not a string as `undefined`.
|
|
401
410
|
|
|
402
|
-
### ContainerModifier
|
|
411
|
+
### `ContainerModifier`
|
|
403
412
|
|
|
404
|
-
|
|
413
|
+
A modifier that stores several values under keys, as `LocalizationModifier` does, extends `ContainerModifier`. Its `lock`, `unlock`, and `unlockrequest` take the key as their argument. `lock` and `unlock` treat the wildcard key `"*"` as the whole record:
|
|
405
414
|
|
|
406
415
|
```typescript
|
|
407
416
|
import { ContainerModifier } from "@antelopejs/interface-database-decorators";
|
|
@@ -412,18 +421,18 @@ class MyContainerModifier extends ContainerModifier<{}, { fallback?: string }> {
|
|
|
412
421
|
}
|
|
413
422
|
```
|
|
414
423
|
|
|
415
|
-
## Modifier
|
|
424
|
+
## Modifier events
|
|
416
425
|
|
|
417
|
-
|
|
426
|
+
A modifier hooks into a row's lifecycle through methods named after events. The engine calls each one with the modifier as `this`, then the table instance, the field name, and the event's arguments:
|
|
418
427
|
|
|
419
|
-
| Event | Triggered
|
|
420
|
-
| --------------- |
|
|
421
|
-
| `fromPlainData` | After
|
|
422
|
-
| `toPlainData` | After
|
|
423
|
-
| `fromDatabase` | After
|
|
424
|
-
| `toDatabase` |
|
|
425
|
-
| `insert` | Before
|
|
426
|
-
| `update` | Before
|
|
428
|
+
| Event | Triggered when |
|
|
429
|
+
| --------------- | --------------------------------------------------------------------- |
|
|
430
|
+
| `fromPlainData` | After plain data is converted to an instance. |
|
|
431
|
+
| `toPlainData` | After an instance is converted to plain data. |
|
|
432
|
+
| `fromDatabase` | After a stored record is converted to an instance, as reads do. |
|
|
433
|
+
| `toDatabase` | When an instance is converted to the record to store, before a write. |
|
|
434
|
+
| `insert` | Before a model or a fixture inserts a new record. |
|
|
435
|
+
| `update` | Before a model updates an existing record. |
|
|
427
436
|
|
|
428
437
|
```typescript
|
|
429
438
|
import {
|
|
@@ -432,15 +441,15 @@ import {
|
|
|
432
441
|
} from "@antelopejs/interface-database-decorators";
|
|
433
442
|
|
|
434
443
|
class TimestampModifier extends Modifier {
|
|
435
|
-
public insert(
|
|
436
|
-
if (field === "createdAt" && !
|
|
437
|
-
|
|
444
|
+
public insert<F extends string>(object: Partial<Record<F, Date>>, field: F) {
|
|
445
|
+
if (field === "createdAt" && !object[field]) {
|
|
446
|
+
object[field] = new Date();
|
|
438
447
|
}
|
|
439
448
|
}
|
|
440
449
|
|
|
441
|
-
public update(
|
|
450
|
+
public update<F extends string>(object: Partial<Record<F, Date>>, field: F) {
|
|
442
451
|
if (field === "updatedAt") {
|
|
443
|
-
|
|
452
|
+
object[field] = new Date();
|
|
444
453
|
}
|
|
445
454
|
}
|
|
446
455
|
|
|
@@ -448,4 +457,4 @@ class TimestampModifier extends Modifier {
|
|
|
448
457
|
}
|
|
449
458
|
```
|
|
450
459
|
|
|
451
|
-
|
|
460
|
+
A modifier without `lock` and `unlock` only handles events. `attachModifier` then defines no accessor on the field, which keeps its plain value. Such a modifier fits side effects such as logging, validation, or filling in fields.
|