@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.
- package/README.md +17 -13
- 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 +18 -3
- 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 +34 -15
- 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 +81 -68
- 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.d.ts +46 -4
- package/dist/modifiers/hash.js +175 -14
- package/dist/modifiers/hash.js.map +1 -1
- package/dist/modifiers/internal/kinds.d.ts +15 -1
- package/dist/modifiers/internal/kinds.js +35 -0
- package/dist/modifiers/internal/kinds.js.map +1 -1
- package/dist/modifiers/internal/records.d.ts +40 -5
- package/dist/modifiers/internal/records.js +70 -1
- package/dist/modifiers/internal/records.js.map +1 -1
- package/dist/modifiers/localization.js +12 -7
- 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 +29 -13
- 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 -2
- package/dist/tests/database.test.js.map +1 -1
- package/dist/tests/hash_upgrade.test.d.ts +1 -0
- package/dist/tests/hash_upgrade.test.js +132 -0
- package/dist/tests/hash_upgrade.test.js.map +1 -0
- package/dist/tests/helpers/hash-rows.d.ts +40 -0
- package/dist/tests/helpers/hash-rows.js +41 -0
- package/dist/tests/helpers/hash-rows.js.map +1 -0
- 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/hash.test.js +295 -0
- package/dist/tests/modifiers/hash.test.js.map +1 -1
- 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/partial_update.test.d.ts +1 -0
- package/dist/tests/partial_update.test.js +55 -0
- package/dist/tests/partial_update.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 +128 -98
- 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 +10 -9
- package/skills/database-decorators-interface/SKILL.md +6 -3
|
@@ -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 {
|
|
@@ -73,43 +77,68 @@ class User extends Table.with(HashModifier) {
|
|
|
73
77
|
@Field("string")
|
|
74
78
|
declare email: string;
|
|
75
79
|
|
|
76
|
-
@Hashed(
|
|
80
|
+
@Hashed()
|
|
77
81
|
@Field("string")
|
|
78
82
|
declare password: string;
|
|
79
83
|
|
|
80
|
-
verifyPassword(plainPassword: string): boolean {
|
|
81
|
-
return this.
|
|
84
|
+
verifyPassword(plainPassword: string): Promise<boolean> {
|
|
85
|
+
return this.testHashAsync("password", plainPassword);
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
```
|
|
85
89
|
|
|
86
|
-
#### How
|
|
90
|
+
#### How it works
|
|
87
91
|
|
|
88
|
-
-
|
|
89
|
-
- The
|
|
90
|
-
-
|
|
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.
|
|
91
99
|
|
|
92
100
|
#### Options
|
|
93
101
|
|
|
94
|
-
| Option | Type | Default
|
|
95
|
-
| ----------- | -------- |
|
|
96
|
-
| `algorithm` | `string` | `
|
|
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. |
|
|
97
105
|
|
|
98
|
-
|
|
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.
|
|
99
107
|
|
|
100
|
-
#### Mixin
|
|
108
|
+
#### Mixin methods
|
|
101
109
|
|
|
102
|
-
The `HashModifier` mixin adds
|
|
110
|
+
The `HashModifier` mixin adds these methods to table instances:
|
|
103
111
|
|
|
104
|
-
```typescript
|
|
105
|
-
testHash(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
|
|
116
|
+
```
|
|
117
|
+
|
|
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`.
|
|
121
|
+
|
|
122
|
+
#### Upgrade stored hashes
|
|
123
|
+
|
|
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:
|
|
125
|
+
|
|
126
|
+
```typescript fragment
|
|
127
|
+
if (await user.testHashAsync("password", password)) {
|
|
128
|
+
if (user.isHashOutdated("password")) {
|
|
129
|
+
user.password = password;
|
|
130
|
+
await userModel.update(user);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
106
133
|
```
|
|
107
134
|
|
|
108
|
-
|
|
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.
|
|
109
136
|
|
|
110
|
-
|
|
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.
|
|
111
138
|
|
|
112
|
-
|
|
139
|
+
### `LocalizationModifier`
|
|
140
|
+
|
|
141
|
+
The `LocalizationModifier` stores one value of a field per locale. To read or write a locale, call `localize()` on the table instance.
|
|
113
142
|
|
|
114
143
|
```typescript
|
|
115
144
|
import {
|
|
@@ -133,9 +162,11 @@ class Product extends Table.with(LocalizationModifier) {
|
|
|
133
162
|
}
|
|
134
163
|
```
|
|
135
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
|
+
|
|
136
167
|
#### Usage
|
|
137
168
|
|
|
138
|
-
```typescript
|
|
169
|
+
```typescript fragment
|
|
139
170
|
const product = new Product();
|
|
140
171
|
product._id = "prod-123";
|
|
141
172
|
product.price = 99.99;
|
|
@@ -149,30 +180,29 @@ product.localize("es").name = "Auriculares Premium";
|
|
|
149
180
|
const spanish = product.localize("es");
|
|
150
181
|
console.log(spanish.name); // "Auriculares Premium"
|
|
151
182
|
|
|
152
|
-
//
|
|
183
|
+
// Fall back to the default locale when a translation is missing
|
|
153
184
|
const german = product.localize("de");
|
|
154
185
|
console.log(german.name); // "Premium Headphones" (fallback)
|
|
155
186
|
```
|
|
156
187
|
|
|
157
|
-
#### Wildcard
|
|
188
|
+
#### Wildcard key
|
|
158
189
|
|
|
159
|
-
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:
|
|
160
191
|
|
|
161
|
-
```typescript
|
|
192
|
+
```typescript fragment
|
|
162
193
|
// Get all translations
|
|
163
|
-
const
|
|
164
|
-
name: Record<string, string>;
|
|
165
|
-
};
|
|
166
|
-
const allNames = translations.name;
|
|
194
|
+
const allNames: unknown = product.localize("*").name;
|
|
167
195
|
// { en: "Premium Headphones", fr: "Casque Premium", es: "Auriculares Premium" }
|
|
168
196
|
|
|
169
197
|
// Set all translations
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
});
|
|
176
206
|
```
|
|
177
207
|
|
|
178
208
|
#### Options
|
|
@@ -181,23 +211,23 @@ translations.name = {
|
|
|
181
211
|
| ---------------- | -------- | --------------------------------------------------------------------- |
|
|
182
212
|
| `fallbackLocale` | `string` | Locale to use when the requested locale has no value or an empty one. |
|
|
183
213
|
|
|
184
|
-
#### Mixin
|
|
214
|
+
#### Mixin method
|
|
185
215
|
|
|
186
216
|
The `LocalizationModifier` mixin adds a `localize` method:
|
|
187
217
|
|
|
188
|
-
```typescript
|
|
189
|
-
localize(locale: string, fields?: Array<keyof
|
|
218
|
+
```typescript fragment
|
|
219
|
+
localize<This extends { constructor: any }>(this: This, locale: string, fields?: Array<keyof This>): This
|
|
190
220
|
```
|
|
191
221
|
|
|
192
|
-
`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.
|
|
193
223
|
|
|
194
224
|
- **References share state.** `spanish`, `german`, and `product` above refer to one object. A later `localize()` call changes what all three references read.
|
|
195
|
-
- **Fields can be restricted.** When you supply `fields`, only those fields
|
|
196
|
-
- **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.
|
|
197
227
|
|
|
198
|
-
### AutoDateModifier
|
|
228
|
+
### `AutoDateModifier`
|
|
199
229
|
|
|
200
|
-
The `AutoDateModifier`
|
|
230
|
+
The `AutoDateModifier` sets timestamp fields on insert and update. Mark the fields with the `CreationTime` and `UpdateTime` decorators.
|
|
201
231
|
|
|
202
232
|
```typescript
|
|
203
233
|
import {
|
|
@@ -217,15 +247,15 @@ class Article extends Table {
|
|
|
217
247
|
}
|
|
218
248
|
```
|
|
219
249
|
|
|
220
|
-
#### How
|
|
250
|
+
#### How it works
|
|
221
251
|
|
|
222
|
-
- On insert, both `CreationTime` and `UpdateTime` fields
|
|
223
|
-
- On update, `UpdateTime` fields
|
|
224
|
-
-
|
|
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()`.
|
|
225
255
|
|
|
226
|
-
## Combine
|
|
256
|
+
## Combine multiple modifiers
|
|
227
257
|
|
|
228
|
-
Pass
|
|
258
|
+
Pass several modifier classes to `Table.with()`:
|
|
229
259
|
|
|
230
260
|
```typescript
|
|
231
261
|
import {
|
|
@@ -266,11 +296,11 @@ class UserProfile extends Table.with(
|
|
|
266
296
|
}
|
|
267
297
|
```
|
|
268
298
|
|
|
269
|
-
## Create
|
|
299
|
+
## Create custom modifiers
|
|
270
300
|
|
|
271
|
-
### OneWayModifier
|
|
301
|
+
### `OneWayModifier`
|
|
272
302
|
|
|
273
|
-
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.
|
|
274
304
|
|
|
275
305
|
```typescript
|
|
276
306
|
import {
|
|
@@ -311,18 +341,18 @@ const PrefixHashed = MakePropertyDecorator(
|
|
|
311
341
|
);
|
|
312
342
|
```
|
|
313
343
|
|
|
314
|
-
The generic parameters
|
|
344
|
+
The generic parameters of `OneWayModifier<LockedType, Args, Meta, Options>` are:
|
|
315
345
|
|
|
316
|
-
| Parameter | Description
|
|
317
|
-
| ------------ |
|
|
318
|
-
| `LockedType` | The type stored in the database after transformation
|
|
319
|
-
| `Args` | Additional arguments for `lock` and `test`
|
|
320
|
-
| `Meta` |
|
|
321
|
-
| `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`. |
|
|
322
352
|
|
|
323
|
-
### TwoWayModifier
|
|
353
|
+
### `TwoWayModifier`
|
|
324
354
|
|
|
325
|
-
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:
|
|
326
356
|
|
|
327
357
|
```ts docs-check
|
|
328
358
|
import {
|
|
@@ -334,7 +364,7 @@ import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators";
|
|
|
334
364
|
import type { ValueProxy } from "@antelopejs/interface-database";
|
|
335
365
|
|
|
336
366
|
class Base64Modifier extends TwoWayModifier<
|
|
337
|
-
|
|
367
|
+
unknown,
|
|
338
368
|
[],
|
|
339
369
|
{},
|
|
340
370
|
{ prefix?: string }
|
|
@@ -342,23 +372,23 @@ class Base64Modifier extends TwoWayModifier<
|
|
|
342
372
|
public readonly autolock = true;
|
|
343
373
|
public readonly autounlock = true;
|
|
344
374
|
|
|
345
|
-
public override lock(_locked:
|
|
375
|
+
public override lock(_locked: unknown, value: unknown) {
|
|
346
376
|
if (value === undefined) return undefined;
|
|
347
377
|
const prefix = this.options.prefix || "b64-";
|
|
348
378
|
return `${prefix}${Buffer.from(JSON.stringify(value)).toString("base64")}`;
|
|
349
379
|
}
|
|
350
380
|
|
|
351
|
-
public override unlock(locked:
|
|
352
|
-
if (locked
|
|
381
|
+
public override unlock(locked: unknown): unknown {
|
|
382
|
+
if (typeof locked !== "string") return undefined;
|
|
353
383
|
const prefix = this.options.prefix || "b64-";
|
|
354
384
|
const encoded = locked.substring(prefix.length);
|
|
355
385
|
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
356
386
|
}
|
|
357
387
|
|
|
358
388
|
public override unlockrequest(
|
|
359
|
-
data: ValueProxy<
|
|
389
|
+
data: ValueProxy<unknown>,
|
|
360
390
|
): ValueProxy<unknown> {
|
|
361
|
-
return data
|
|
391
|
+
return data;
|
|
362
392
|
}
|
|
363
393
|
|
|
364
394
|
[MixinSymbol] = class {};
|
|
@@ -376,11 +406,11 @@ const Base64Encoded = MakePropertyDecorator(
|
|
|
376
406
|
);
|
|
377
407
|
```
|
|
378
408
|
|
|
379
|
-
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`.
|
|
380
410
|
|
|
381
|
-
### ContainerModifier
|
|
411
|
+
### `ContainerModifier`
|
|
382
412
|
|
|
383
|
-
|
|
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:
|
|
384
414
|
|
|
385
415
|
```typescript
|
|
386
416
|
import { ContainerModifier } from "@antelopejs/interface-database-decorators";
|
|
@@ -391,18 +421,18 @@ class MyContainerModifier extends ContainerModifier<{}, { fallback?: string }> {
|
|
|
391
421
|
}
|
|
392
422
|
```
|
|
393
423
|
|
|
394
|
-
## Modifier
|
|
424
|
+
## Modifier events
|
|
395
425
|
|
|
396
|
-
|
|
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:
|
|
397
427
|
|
|
398
|
-
| Event | Triggered
|
|
399
|
-
| --------------- |
|
|
400
|
-
| `fromPlainData` | After
|
|
401
|
-
| `toPlainData` | After
|
|
402
|
-
| `fromDatabase` | After
|
|
403
|
-
| `toDatabase` |
|
|
404
|
-
| `insert` | Before
|
|
405
|
-
| `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. |
|
|
406
436
|
|
|
407
437
|
```typescript
|
|
408
438
|
import {
|
|
@@ -411,15 +441,15 @@ import {
|
|
|
411
441
|
} from "@antelopejs/interface-database-decorators";
|
|
412
442
|
|
|
413
443
|
class TimestampModifier extends Modifier {
|
|
414
|
-
public insert(
|
|
415
|
-
if (field === "createdAt" && !
|
|
416
|
-
|
|
444
|
+
public insert<F extends string>(object: Partial<Record<F, Date>>, field: F) {
|
|
445
|
+
if (field === "createdAt" && !object[field]) {
|
|
446
|
+
object[field] = new Date();
|
|
417
447
|
}
|
|
418
448
|
}
|
|
419
449
|
|
|
420
|
-
public update(
|
|
450
|
+
public update<F extends string>(object: Partial<Record<F, Date>>, field: F) {
|
|
421
451
|
if (field === "updatedAt") {
|
|
422
|
-
|
|
452
|
+
object[field] = new Date();
|
|
423
453
|
}
|
|
424
454
|
}
|
|
425
455
|
|
|
@@ -427,4 +457,4 @@ class TimestampModifier extends Modifier {
|
|
|
427
457
|
}
|
|
428
458
|
```
|
|
429
459
|
|
|
430
|
-
|
|
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.
|