@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 CHANGED
@@ -1,30 +1,113 @@
1
- # Interface Database Decorators
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" alt="npm"></a>
5
- <a href="./LICENSE"><img alt="License" src="https://img.shields.io/badge/License-Apache_2.0-blue.svg?style=for-the-badge&labelColor=000000"></a>
6
- <a href="https://discord.gg/sjK28QHrA7"><img src="https://img.shields.io/badge/Discord-18181B?logo=discord&style=for-the-badge&color=000000" alt="Discord"></a>
7
- <a href="./docs/1.introduction.md"><img src="https://img.shields.io/badge/Docs-18181B?style=for-the-badge&color=000000" alt="Documentation"></a>
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
- A structured framework for implementing database operations using TypeScript decorators. Interface Database Decorators provides a flexible, object-oriented approach to defining database tables, models, and operations with full type safety, including built-in support for field-level encryption, hashing, and localization.
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
- ## Installation
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
- npm install @antelopejs/interface-database-decorators
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
- Detailed documentation is available in the `docs` directory:
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
- - [Introduction](./docs/1.introduction.md) - Overview and dependencies
23
- - [Table Definitions](./docs/2.table-definitions.md) - Define database tables with decorators, indexes, and fixtures
24
- - [Table Modifiers](./docs/3.table-modifiers.md) - Encryption, hashing, localization, and custom modifier creation
25
- - [Data Models](./docs/4.data-models.md) - Create and use data models with CRUD operations
26
- - [Parameter Decoration](./docs/5.parameter-decoration.md) - Inject models into API controllers
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
- This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
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: process.env.SECRET_KEY || "default-key" })
42
+ @Encrypted({ secretKey })
38
43
  @Field("string")
39
44
  declare secureContent: string;
40
45
  }
41
46
  ```
42
47
 
43
- For detailed information about modifiers, see [Table Modifiers](./3.table-modifiers.md).
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
- ```typescript
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: process.env.ENCRYPTION_KEY || "default-key",
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: process.env.ENCRYPTION_KEY || "default-key" })
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 preserved through JSON serialization with a custom encoder.
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. Use this for passwords and other sensitive data that only needs equality verification.
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 allNames = product.localize("*").name;
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
- product.localize("*").name = {
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: process.env.SECRET_KEY || "default-key" })
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
- ```typescript
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(data: any) {
336
- return data;
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:
@@ -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
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "..",
5
+ "noEmit": true,
6
+ "composite": false,
7
+ "incremental": false
8
+ },
9
+ "include": ["../src/**/*.ts", "./**/*.ts"]
10
+ }
package/package.json CHANGED
@@ -1,14 +1,19 @@
1
1
  {
2
2
  "name": "@antelopejs/interface-database-decorators",
3
- "version": "0.1.8",
4
- "description": "AntelopeJS interface for database schema decorators and model definitions",
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.13.4",
133
- "@antelopejs/interface-api": "^0.0.13",
134
- "@antelopejs/interface-core": "^0.1.1",
135
- "@antelopejs/interface-database": "^0.1.8",
136
- "@antelopejs/tooling-configs": "^0.0.6",
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.13 <1.0.0",
156
- "@antelopejs/interface-core": ">=0.1.1 <1.0.0",
157
- "@antelopejs/interface-database": ">=0.1.8 <1.0.0"
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": {