@antelopejs/interface-database-decorators 0.1.8 → 0.1.9
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 +98 -15
- package/docs/2.table-definitions.md +8 -3
- package/docs/3.table-modifiers.md +36 -15
- package/docs/4.data-models.md +12 -0
- package/docs/tsconfig.json +10 -0
- package/package.json +15 -10
package/README.md
CHANGED
|
@@ -1,30 +1,113 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @antelopejs/interface-database-decorators
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
|
-
<a href="https://www.npmjs.com/package/@antelopejs/interface-database-decorators"><img src="https://img.shields.io/npm/v/@antelopejs/interface-database-decorators?style=for-the-badge&labelColor=000000&color=000000"
|
|
5
|
-
<a href="./LICENSE"><img alt="License" src="https://img.shields.io/
|
|
6
|
-
<a href="https://discord.gg/sjK28QHrA7"><img src="https://img.shields.io/badge/Discord-18181B?logo=discord&style=for-the-badge&color=000000"
|
|
7
|
-
<a href="
|
|
4
|
+
<a href="https://www.npmjs.com/package/@antelopejs/interface-database-decorators"><img alt="npm" src="https://img.shields.io/npm/v/@antelopejs/interface-database-decorators?style=for-the-badge&labelColor=000000&color=000000"></a>
|
|
5
|
+
<a href="./LICENSE"><img alt="License" src="https://img.shields.io/npm/l/@antelopejs/interface-database-decorators?style=for-the-badge&labelColor=000000&color=000000"></a>
|
|
6
|
+
<a href="https://discord.gg/sjK28QHrA7"><img alt="Discord" src="https://img.shields.io/badge/Discord-18181B?logo=discord&style=for-the-badge&color=000000"></a>
|
|
7
|
+
<a href="https://interfaces.antelopejs.com/docs/database-decorators"><img alt="Docs" src="https://img.shields.io/badge/Docs-18181B?style=for-the-badge&color=000000"></a>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Database tables and data models as TypeScript classes, on top of [`@antelopejs/interface-database`](https://github.com/AntelopeJS/interface-database): declare fields and indexes with decorators, then read and write rows through a typed model.
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## What it offers
|
|
13
|
+
|
|
14
|
+
- `Table`, `@RegisterTable`, `@Field`, `@Index` and `@Fixture` declare tables, indexes and seed rows; `RegisterSchema` provisions them.
|
|
15
|
+
- `BasicDataModel` and `GetModel` give each table typed CRUD methods with optional validation.
|
|
16
|
+
- `@Encrypted`, `@Hashed`, `@Localized`, `@CreationTime` and `@UpdateTime` transform field values on write and read.
|
|
17
|
+
- `@Model` injects a model into an [`@antelopejs/interface-api`](https://github.com/AntelopeJS/interface-api) controller.
|
|
18
|
+
|
|
19
|
+
## Implementations
|
|
20
|
+
|
|
21
|
+
The package runs in your module and has no implementing module of its own. The project needs a module that implements `@antelopejs/interface-database`:
|
|
22
|
+
|
|
23
|
+
| Module | Notes |
|
|
24
|
+
| ------------------------------------------------------------------ | --------- |
|
|
25
|
+
| [`@antelopejs/mongodb`](https://github.com/AntelopeJS/mongodb) | MongoDB |
|
|
26
|
+
| [`@antelopejs/rethinkdb`](https://github.com/AntelopeJS/rethinkdb) | RethinkDB |
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
Add the package to the module that declares tables:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm add @antelopejs/interface-database-decorators
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then add a database module to the project:
|
|
13
37
|
|
|
14
38
|
```bash
|
|
15
|
-
|
|
39
|
+
ajs project modules add @antelopejs/mongodb
|
|
16
40
|
```
|
|
17
41
|
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import {
|
|
46
|
+
BasicDataModel,
|
|
47
|
+
Field,
|
|
48
|
+
GetModel,
|
|
49
|
+
Index,
|
|
50
|
+
RegisterSchema,
|
|
51
|
+
RegisterTable,
|
|
52
|
+
Table,
|
|
53
|
+
} from "@antelopejs/interface-database-decorators";
|
|
54
|
+
|
|
55
|
+
@RegisterTable("recipes", "cookbook")
|
|
56
|
+
class Recipe extends Table {
|
|
57
|
+
@Index()
|
|
58
|
+
@Field("string")
|
|
59
|
+
declare slug: string;
|
|
60
|
+
|
|
61
|
+
@Field("string")
|
|
62
|
+
declare tagline: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class RecipeModel extends BasicDataModel(Recipe) {}
|
|
66
|
+
|
|
67
|
+
export async function construct() {
|
|
68
|
+
await RegisterSchema("cookbook");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function start() {
|
|
72
|
+
const recipes = GetModel(RecipeModel);
|
|
73
|
+
await recipes.insert({ slug: "grilled-halloumi", tagline: "Smoky skewers" });
|
|
74
|
+
const [recipe] = await recipes.getBy("slug", "grilled-halloumi");
|
|
75
|
+
await recipes.update(recipe._id, { tagline: "Smoky halloumi skewers" });
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`RegisterSchema` creates every table registered for the schema and inserts their fixtures. Call it once, in the module that declares the tables.
|
|
80
|
+
|
|
18
81
|
## Documentation
|
|
19
82
|
|
|
20
|
-
|
|
83
|
+
- [Introduction](./docs/1.introduction.md)
|
|
84
|
+
- [Table definitions](./docs/2.table-definitions.md)
|
|
85
|
+
- [Table modifiers](./docs/3.table-modifiers.md)
|
|
86
|
+
- [Data models](./docs/4.data-models.md)
|
|
87
|
+
- [Parameter decoration](./docs/5.parameter-decoration.md)
|
|
88
|
+
|
|
89
|
+
## Compatibility
|
|
90
|
+
|
|
91
|
+
Requires AntelopeJS core `1.x` and the following peer dependencies:
|
|
21
92
|
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
93
|
+
<!-- docs-compat: ./package.json#peerDependencies -->
|
|
94
|
+
|
|
95
|
+
| Package | Range |
|
|
96
|
+
| -------------------------------- | ----------------- |
|
|
97
|
+
| `@antelopejs/interface-api` | `>=0.0.15 <1.0.0` |
|
|
98
|
+
| `@antelopejs/interface-core` | `>=0.2.4 <1.0.0` |
|
|
99
|
+
| `@antelopejs/interface-database` | `>=0.1.10 <1.0.0` |
|
|
100
|
+
|
|
101
|
+
## Contributing
|
|
102
|
+
|
|
103
|
+
Read [CONTRIBUTING.md](./CONTRIBUTING.md) before you open a pull request. The local checks are:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pnpm install
|
|
107
|
+
pnpm lint
|
|
108
|
+
pnpm test
|
|
109
|
+
```
|
|
27
110
|
|
|
28
111
|
## License
|
|
29
112
|
|
|
30
|
-
|
|
113
|
+
[Apache-2.0](./LICENSE)
|
|
@@ -30,17 +30,22 @@ import {
|
|
|
30
30
|
Encrypted,
|
|
31
31
|
} from "@antelopejs/interface-database-decorators";
|
|
32
32
|
|
|
33
|
+
const secretKey = process.env.ENCRYPTION_KEY;
|
|
34
|
+
if (!secretKey || Buffer.byteLength(secretKey, "utf8") !== 32) {
|
|
35
|
+
throw new Error("ENCRYPTION_KEY must contain exactly 32 UTF-8 bytes");
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
class SensitiveData extends Table.with(EncryptionModifier) {
|
|
34
39
|
@Field("string")
|
|
35
40
|
declare publicContent: string;
|
|
36
41
|
|
|
37
|
-
@Encrypted({ secretKey
|
|
42
|
+
@Encrypted({ secretKey })
|
|
38
43
|
@Field("string")
|
|
39
44
|
declare secureContent: string;
|
|
40
45
|
}
|
|
41
46
|
```
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
Provide a high-entropy key through your secret manager; do not use a default key. See [Table Modifiers](./3.table-modifiers.md#encryptionmodifier) for key format and date precision constraints.
|
|
44
49
|
|
|
45
50
|
## The Index Decorator
|
|
46
51
|
|
|
@@ -161,7 +166,7 @@ class Comment extends Table {
|
|
|
161
166
|
@Relation({ to: () => Post })
|
|
162
167
|
declare postId: string;
|
|
163
168
|
|
|
164
|
-
@Field("string")
|
|
169
|
+
@Field(["string"])
|
|
165
170
|
@Relation({ to: () => Tag, many: true })
|
|
166
171
|
declare tagIds: string[];
|
|
167
172
|
}
|
|
@@ -8,7 +8,9 @@ Table modifiers transform field values as they are stored in and retrieved from
|
|
|
8
8
|
|
|
9
9
|
The `EncryptionModifier` encrypts field values before storing them and decrypts them on retrieval. Both operations happen transparently.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The default `aes-256-gcm` algorithm requires a key of exactly 32 bytes. `secretKey` is passed 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 for existing ciphertext, and fail startup if it is missing or invalid; a 64-character hex encoding is not a 32-byte key for this API.
|
|
12
|
+
|
|
13
|
+
```ts docs-check
|
|
12
14
|
import {
|
|
13
15
|
Table,
|
|
14
16
|
Field,
|
|
@@ -16,16 +18,21 @@ import {
|
|
|
16
18
|
Encrypted,
|
|
17
19
|
} from "@antelopejs/interface-database-decorators";
|
|
18
20
|
|
|
21
|
+
const secretKey = process.env.ENCRYPTION_KEY;
|
|
22
|
+
if (!secretKey || Buffer.byteLength(secretKey, "utf8") !== 32) {
|
|
23
|
+
throw new Error("ENCRYPTION_KEY must contain exactly 32 UTF-8 bytes");
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
class UserCredentials extends Table.with(EncryptionModifier) {
|
|
20
27
|
@Encrypted({
|
|
21
|
-
secretKey
|
|
28
|
+
secretKey,
|
|
22
29
|
algorithm: "aes-256-gcm",
|
|
23
30
|
ivSize: 16,
|
|
24
31
|
})
|
|
25
32
|
@Field("string")
|
|
26
33
|
declare creditCardNumber: string;
|
|
27
34
|
|
|
28
|
-
@Encrypted({ secretKey
|
|
35
|
+
@Encrypted({ secretKey })
|
|
29
36
|
@Field("any")
|
|
30
37
|
declare personalData: {
|
|
31
38
|
ssn: string;
|
|
@@ -40,7 +47,7 @@ class UserCredentials extends Table.with(EncryptionModifier) {
|
|
|
40
47
|
- A random initialization vector (IV) is generated for each write.
|
|
41
48
|
- For authenticated encryption algorithms (e.g., `aes-256-gcm`), an auth tag is stored alongside the ciphertext.
|
|
42
49
|
- On retrieval, the data is automatically decrypted.
|
|
43
|
-
- Date objects are
|
|
50
|
+
- Date objects are restored as `Date` instances, but the encoder uses `toUTCString()`, which drops milliseconds. Encrypted dates round-trip only to whole-second precision.
|
|
44
51
|
|
|
45
52
|
#### Options
|
|
46
53
|
|
|
@@ -52,7 +59,7 @@ class UserCredentials extends Table.with(EncryptionModifier) {
|
|
|
52
59
|
|
|
53
60
|
### HashModifier
|
|
54
61
|
|
|
55
|
-
The `HashModifier` stores a one-way hash of field values. Unlike encryption, hashing is irreversible -- you cannot retrieve the original value.
|
|
62
|
+
The `HashModifier` stores a salted one-way hash of field values. Unlike encryption, hashing is irreversible -- you cannot retrieve the original value. Its default SHA-256 hash is fast, not a password-hardening function; use a dedicated password hashing library for production credentials.
|
|
56
63
|
|
|
57
64
|
```typescript
|
|
58
65
|
import {
|
|
@@ -147,15 +154,18 @@ console.log(german.name); // "Premium Headphones" (fallback)
|
|
|
147
154
|
|
|
148
155
|
#### Wildcard Key
|
|
149
156
|
|
|
150
|
-
The wildcard key `"*"` retrieves or sets all translations at once:
|
|
157
|
+
The wildcard key `"*"` retrieves or sets all translations at once at runtime. `localize()` does not change the property's TypeScript type, so a field declared as `string` needs an explicit view for bulk access:
|
|
151
158
|
|
|
152
159
|
```typescript
|
|
153
160
|
// Get all translations
|
|
154
|
-
const
|
|
161
|
+
const translations = product.localize("*") as unknown as {
|
|
162
|
+
name: Record<string, string>;
|
|
163
|
+
};
|
|
164
|
+
const allNames = translations.name;
|
|
155
165
|
// { en: "Premium Headphones", fr: "Casque Premium", es: "Auriculares Premium" }
|
|
156
166
|
|
|
157
167
|
// Set all translations
|
|
158
|
-
|
|
168
|
+
translations.name = {
|
|
159
169
|
en: "Premium Headphones",
|
|
160
170
|
fr: "Casque Premium",
|
|
161
171
|
es: "Auriculares Premium",
|
|
@@ -177,7 +187,7 @@ The `LocalizationModifier` mixin adds a `localize` method:
|
|
|
177
187
|
localize(locale: string, fields?: Array<keyof this>): this
|
|
178
188
|
```
|
|
179
189
|
|
|
180
|
-
Sets the active locale for accessing localized fields. If `fields` is provided, only those fields are unlocked for the specified locale.
|
|
190
|
+
Sets the active locale for accessing localized fields and returns the same, mutated object. It does not make a locale-specific copy: `spanish`, `german`, and `product` above all refer to the same object, and subsequent calls change what all those references read. If `fields` is provided, only those fields are unlocked for the specified locale. Copy the values you need before changing locales, and do not share one instance between concurrent locale-specific operations.
|
|
181
191
|
|
|
182
192
|
### AutoDateModifier
|
|
183
193
|
|
|
@@ -223,12 +233,17 @@ import {
|
|
|
223
233
|
Localized,
|
|
224
234
|
} from "@antelopejs/interface-database-decorators";
|
|
225
235
|
|
|
236
|
+
const secretKey = process.env.ENCRYPTION_KEY;
|
|
237
|
+
if (!secretKey || Buffer.byteLength(secretKey, "utf8") !== 32) {
|
|
238
|
+
throw new Error("ENCRYPTION_KEY must contain exactly 32 UTF-8 bytes");
|
|
239
|
+
}
|
|
240
|
+
|
|
226
241
|
class UserProfile extends Table.with(
|
|
227
242
|
EncryptionModifier,
|
|
228
243
|
HashModifier,
|
|
229
244
|
LocalizationModifier,
|
|
230
245
|
) {
|
|
231
|
-
@Encrypted({ secretKey
|
|
246
|
+
@Encrypted({ secretKey })
|
|
232
247
|
@Field("any")
|
|
233
248
|
declare privateInfo: {
|
|
234
249
|
address: string;
|
|
@@ -303,16 +318,17 @@ The generic parameters for `OneWayModifier<LockedType, Args, Meta, Options>` are
|
|
|
303
318
|
|
|
304
319
|
A `TwoWayModifier` extends `OneWayModifier` with an `unlock` method for reversible transformations like encryption:
|
|
305
320
|
|
|
306
|
-
```
|
|
321
|
+
```ts docs-check
|
|
307
322
|
import {
|
|
308
323
|
TwoWayModifier,
|
|
309
324
|
MixinSymbol,
|
|
310
325
|
attachModifier,
|
|
311
326
|
} from "@antelopejs/interface-database-decorators";
|
|
312
327
|
import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators";
|
|
328
|
+
import type { ValueProxy } from "@antelopejs/interface-database";
|
|
313
329
|
|
|
314
330
|
class Base64Modifier extends TwoWayModifier<
|
|
315
|
-
string,
|
|
331
|
+
string | undefined,
|
|
316
332
|
[],
|
|
317
333
|
{},
|
|
318
334
|
{ prefix?: string }
|
|
@@ -326,14 +342,17 @@ class Base64Modifier extends TwoWayModifier<
|
|
|
326
342
|
return `${prefix}${Buffer.from(JSON.stringify(value)).toString("base64")}`;
|
|
327
343
|
}
|
|
328
344
|
|
|
329
|
-
public override unlock(locked: string) {
|
|
345
|
+
public override unlock(locked: string | undefined): unknown {
|
|
346
|
+
if (locked === undefined) return undefined;
|
|
330
347
|
const prefix = this.options.prefix || "b64-";
|
|
331
348
|
const encoded = locked.substring(prefix.length);
|
|
332
349
|
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
333
350
|
}
|
|
334
351
|
|
|
335
|
-
public override unlockrequest(
|
|
336
|
-
|
|
352
|
+
public override unlockrequest(
|
|
353
|
+
data: ValueProxy<string | undefined>,
|
|
354
|
+
): ValueProxy<unknown> {
|
|
355
|
+
return data.cast<unknown>();
|
|
337
356
|
}
|
|
338
357
|
|
|
339
358
|
[MixinSymbol] = class {};
|
|
@@ -351,6 +370,8 @@ const Base64Encoded = MakePropertyDecorator(
|
|
|
351
370
|
);
|
|
352
371
|
```
|
|
353
372
|
|
|
373
|
+
Base64 is encoding, not encryption. This example's `unlockrequest()` leaves the encoded value unchanged in database expressions; only instance reads decode it.
|
|
374
|
+
|
|
354
375
|
### ContainerModifier
|
|
355
376
|
|
|
356
377
|
For modifiers that store multiple values in a key-value structure (like `LocalizationModifier`), extend `ContainerModifier`. It provides built-in support for the wildcard key `"*"` for bulk operations:
|
package/docs/4.data-models.md
CHANGED
|
@@ -37,6 +37,18 @@ const userModel = new UserModel(db);
|
|
|
37
37
|
|
|
38
38
|
## CRUD Operations
|
|
39
39
|
|
|
40
|
+
For a model created with `BasicDataModel(User)`, the return values are:
|
|
41
|
+
|
|
42
|
+
| Method | Declared return | Meaning |
|
|
43
|
+
| ------------------------------------ | -------------------------------- | ------------------------------------------------------------------- |
|
|
44
|
+
| `get(id)` | `PromiseLike<User \| undefined>` | The converted row, or `undefined` when the provider returns no row. |
|
|
45
|
+
| `getBy(index, ...keys)` / `getAll()` | `PromiseLike<User[]>` | Converted rows, not an AQL query to chain. |
|
|
46
|
+
| `insert(data, options?)` | `Promise<string[]>` | Inserted document IDs. |
|
|
47
|
+
| `update(...)` | `Promise<number>` | Number of modified documents. |
|
|
48
|
+
| `delete(id)` | `Promise<number>` | Number of deleted documents. |
|
|
49
|
+
|
|
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.
|
|
51
|
+
|
|
40
52
|
### Read
|
|
41
53
|
|
|
42
54
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/interface-database-decorators",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "AntelopeJS
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "AntelopeJS decorators for database tables and typed data models, with encrypted, hashed and localized fields",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
7
7
|
"database",
|
|
8
8
|
"decorators",
|
|
9
9
|
"interface",
|
|
10
|
+
"model",
|
|
10
11
|
"orm"
|
|
11
12
|
],
|
|
13
|
+
"homepage": "https://interfaces.antelopejs.com/docs/database-decorators",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/AntelopeJS/interface-database-decorators/issues"
|
|
16
|
+
},
|
|
12
17
|
"license": "Apache-2.0",
|
|
13
18
|
"repository": {
|
|
14
19
|
"type": "git",
|
|
@@ -129,11 +134,11 @@
|
|
|
129
134
|
"reflect-metadata": "^0.2.2"
|
|
130
135
|
},
|
|
131
136
|
"devDependencies": {
|
|
132
|
-
"@antelopejs/core": "^1.
|
|
133
|
-
"@antelopejs/interface-api": "^0.0.
|
|
134
|
-
"@antelopejs/interface-core": "^0.
|
|
135
|
-
"@antelopejs/interface-database": "^0.1.
|
|
136
|
-
"@antelopejs/tooling-configs": "^0.0.
|
|
137
|
+
"@antelopejs/core": "^1.16.0",
|
|
138
|
+
"@antelopejs/interface-api": "^0.0.15",
|
|
139
|
+
"@antelopejs/interface-core": "^0.2.4",
|
|
140
|
+
"@antelopejs/interface-database": "^0.1.10",
|
|
141
|
+
"@antelopejs/tooling-configs": "^0.0.9",
|
|
137
142
|
"@types/chai": "^4.3.20",
|
|
138
143
|
"@types/mocha": "^10.0.10",
|
|
139
144
|
"@types/node": "^22.19.15",
|
|
@@ -152,9 +157,9 @@
|
|
|
152
157
|
"typescript": "^5.9.3"
|
|
153
158
|
},
|
|
154
159
|
"peerDependencies": {
|
|
155
|
-
"@antelopejs/interface-api": ">=0.0.
|
|
156
|
-
"@antelopejs/interface-core": ">=0.
|
|
157
|
-
"@antelopejs/interface-database": ">=0.1.
|
|
160
|
+
"@antelopejs/interface-api": ">=0.0.15 <1.0.0",
|
|
161
|
+
"@antelopejs/interface-core": ">=0.2.4 <1.0.0",
|
|
162
|
+
"@antelopejs/interface-database": ">=0.1.10 <1.0.0"
|
|
158
163
|
},
|
|
159
164
|
"packageManager": "pnpm@10.6.5",
|
|
160
165
|
"antelopeJs": {
|