@antelopejs/interface-database-decorators 0.1.5 → 0.1.6
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.
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Interface Database Decorators
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Interface Database Decorators package provides a structured, decorator-based framework for defining database tables, applying field-level modifiers (encryption, hashing, localization), building data models with CRUD operations, and injecting models into API controllers. It builds on top of the `@antelopejs/interface-database` package and integrates with the `@antelopejs/interface-api` package for controller parameter injection.
|
|
6
|
+
|
|
7
|
+
## Dependencies
|
|
8
|
+
|
|
9
|
+
This package depends on the following:
|
|
10
|
+
|
|
11
|
+
- [**@antelopejs/interface-database**](https://github.com/AntelopeJS/interface-database) - The underlying database query interface (AQL)
|
|
12
|
+
- [**@antelopejs/interface-api**](https://github.com/AntelopeJS/interface-api) - API controller integration for parameter decoration
|
|
13
|
+
- [**@antelopejs/interface-core**](https://github.com/AntelopeJS/interface-core) - Core decorator utilities
|
|
14
|
+
- [**reflect-metadata**](https://www.npmjs.com/package/reflect-metadata) - Metadata reflection for TypeScript decorators
|
|
15
|
+
- [**randomstring**](https://www.npmjs.com/package/randomstring) - Random string generation for hash salts
|
|
16
|
+
|
|
17
|
+
## Documentation
|
|
18
|
+
|
|
19
|
+
- [Table Definitions](./2.table-definitions.md) - Define database tables with the `Table` class, `Index` decorator, and `Fixture` decorator
|
|
20
|
+
- [Table Modifiers](./3.table-modifiers.md) - Apply encryption, hashing, and localization modifiers to table fields, or create custom modifiers
|
|
21
|
+
- [Data Models](./4.data-models.md) - Create data models with `BasicDataModel` for CRUD operations and data conversion
|
|
22
|
+
- [Parameter Decoration](./5.parameter-decoration.md) - Inject model instances into API controller methods with the `Model` decorator
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Table Definitions
|
|
2
|
+
|
|
3
|
+
The Database Decorators package provides a class-based approach to defining database tables. You extend the `Table` base class, declare each field with TypeScript's `declare` keyword and the `@Field` decorator, and use further decorators to configure indexes, relations, and initial data.
|
|
4
|
+
|
|
5
|
+
## The Table Class
|
|
6
|
+
|
|
7
|
+
The `Table` class serves as the base class for all table definitions. Every table automatically includes an `_id` field as the primary key.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Table, Field } from "@antelopejs/interface-database-decorators";
|
|
11
|
+
|
|
12
|
+
class User extends Table {
|
|
13
|
+
@Field("string")
|
|
14
|
+
declare name: string;
|
|
15
|
+
|
|
16
|
+
@Field("string")
|
|
17
|
+
declare email: string;
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Extend with Modifiers
|
|
22
|
+
|
|
23
|
+
The `Table.with()` static method incorporates modifier mixins into the table class, adding capabilities like encryption, hashing, or localization:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { Table, Field, EncryptionModifier, Encrypted } from "@antelopejs/interface-database-decorators";
|
|
27
|
+
|
|
28
|
+
class SensitiveData extends Table.with(EncryptionModifier) {
|
|
29
|
+
@Field("string")
|
|
30
|
+
declare publicContent: string;
|
|
31
|
+
|
|
32
|
+
@Encrypted({ secretKey: process.env.SECRET_KEY || "default-key" })
|
|
33
|
+
@Field("string")
|
|
34
|
+
declare secureContent: string;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For detailed information about modifiers, see [Table Modifiers](./3.table-modifiers.md).
|
|
39
|
+
|
|
40
|
+
## The Index Decorator
|
|
41
|
+
|
|
42
|
+
The `Index` decorator marks a field as a database index. Indexed fields enable efficient lookups through `getAll()` and `between()` on the underlying AQL table.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { Table, Field, Index } from "@antelopejs/interface-database-decorators";
|
|
46
|
+
|
|
47
|
+
class User extends Table {
|
|
48
|
+
@Index()
|
|
49
|
+
@Field("string")
|
|
50
|
+
declare email: string;
|
|
51
|
+
|
|
52
|
+
@Field("string")
|
|
53
|
+
declare name: string;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Options
|
|
58
|
+
|
|
59
|
+
| Option | Type | Description |
|
|
60
|
+
| ------- | -------- | ----------------------------------------------- |
|
|
61
|
+
| `group` | `string` | Assign the field to a named compound index group. |
|
|
62
|
+
|
|
63
|
+
### Compound Indexes
|
|
64
|
+
|
|
65
|
+
Assign multiple fields to the same index group to create a compound index:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { Table, Field, Index, Relation } from "@antelopejs/interface-database-decorators";
|
|
69
|
+
|
|
70
|
+
class User extends Table {
|
|
71
|
+
@Field("string")
|
|
72
|
+
declare name: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class UserActivity extends Table {
|
|
76
|
+
@Index({ group: "user_action" })
|
|
77
|
+
@Field("string")
|
|
78
|
+
@Relation({ to: () => User })
|
|
79
|
+
declare userId: string;
|
|
80
|
+
|
|
81
|
+
@Index({ group: "user_action" })
|
|
82
|
+
@Field("string")
|
|
83
|
+
declare action: string;
|
|
84
|
+
|
|
85
|
+
@Field("date")
|
|
86
|
+
declare timestamp: Date;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## The Field Decorator
|
|
91
|
+
|
|
92
|
+
`@Field(type)` declares the field type for a property. The type is forwarded verbatim into the `TableDefinition.fields` map handed to the adapter. It accepts the full `FieldType` union -- primitive tokens (`"string"`, `"number"`, ...), arrays, nested records, or an io-ts codec.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import * as t from "io-ts";
|
|
96
|
+
import { Table, Field, Index, RegisterTable } from "@antelopejs/interface-database-decorators";
|
|
97
|
+
|
|
98
|
+
@RegisterTable("orders", "shop")
|
|
99
|
+
class Order extends Table {
|
|
100
|
+
@Field("string")
|
|
101
|
+
declare id: string;
|
|
102
|
+
|
|
103
|
+
@Field(t.union([t.literal("open"), t.literal("closed")]))
|
|
104
|
+
declare status: "open" | "closed";
|
|
105
|
+
|
|
106
|
+
@Index()
|
|
107
|
+
@Field("number")
|
|
108
|
+
declare total: number;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Properties without `@Field` are omitted from `fields`. `@Field` stacks freely with `@Index`.
|
|
113
|
+
|
|
114
|
+
## The Relation Decorator
|
|
115
|
+
|
|
116
|
+
The `Relation` decorator declares a link from a field to another table. It is **declarative metadata only**: database implementations do not enforce it (no foreign key constraint, no referential validation). Introspection tooling consumes it to expose the links between tables — for example to render schema diagrams or navigate related records.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { Table, Field, Index, Relation } from "@antelopejs/interface-database-decorators";
|
|
120
|
+
|
|
121
|
+
class Comment extends Table {
|
|
122
|
+
@Index()
|
|
123
|
+
@Field("string")
|
|
124
|
+
@Relation({ to: () => Post })
|
|
125
|
+
declare postId: string;
|
|
126
|
+
|
|
127
|
+
@Field("string")
|
|
128
|
+
@Relation({ to: () => Tag, many: true })
|
|
129
|
+
declare tagIds: string[];
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Options
|
|
134
|
+
|
|
135
|
+
| Option | Type | Description |
|
|
136
|
+
| --------- | ------------------- | ---------------------------------------------------------------------- |
|
|
137
|
+
| `to` | `() => typeof Table` | Thunk returning the target table class (lazy to allow forward references). |
|
|
138
|
+
| `toField` | `string` | Target field name. Defaults to the target table's primary key. |
|
|
139
|
+
| `many` | `boolean` | The decorated field holds multiple target keys (many targets per source record). |
|
|
140
|
+
|
|
141
|
+
`@Relation` stacks freely with `@Field` and `@Index`.
|
|
142
|
+
|
|
143
|
+
## The RegisterTable Decorator
|
|
144
|
+
|
|
145
|
+
The `RegisterTable` class decorator associates a table class with a specific table name and schema. This registration is used by `RegisterSchema` to build the schema definition automatically.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { Table, Field, Index, RegisterTable } from "@antelopejs/interface-database-decorators";
|
|
149
|
+
|
|
150
|
+
@RegisterTable("users", "myapp")
|
|
151
|
+
class User extends Table {
|
|
152
|
+
@Index()
|
|
153
|
+
@Field("string")
|
|
154
|
+
declare email: string;
|
|
155
|
+
|
|
156
|
+
@Field("string")
|
|
157
|
+
declare name: string;
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The first argument is the table name in the database, and the second is the schema name.
|
|
162
|
+
|
|
163
|
+
## The Fixture Decorator
|
|
164
|
+
|
|
165
|
+
The `Fixture` decorator defines default data to insert when a table is first created. It receives a generator function that produces initial records.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { Table, Field, Fixture } from "@antelopejs/interface-database-decorators";
|
|
169
|
+
|
|
170
|
+
@Fixture(() => [
|
|
171
|
+
{ _id: "admin", name: "Administrator" },
|
|
172
|
+
{ _id: "user", name: "Standard User" },
|
|
173
|
+
{ _id: "guest", name: "Guest" },
|
|
174
|
+
])
|
|
175
|
+
class UserRole extends Table {
|
|
176
|
+
@Field("string")
|
|
177
|
+
declare name: string;
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The generator function:
|
|
182
|
+
|
|
183
|
+
- Receives the table class as its parameter.
|
|
184
|
+
- Can return a single object, an array of objects, or a `Promise` resolving to either.
|
|
185
|
+
- Runs only when the table is empty, preventing duplicate inserts on subsequent starts.
|
|
186
|
+
|
|
187
|
+
### Async Generators
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
@Fixture(async () => {
|
|
191
|
+
const defaults = await loadDefaultConfig();
|
|
192
|
+
return defaults.map((cfg) => ({
|
|
193
|
+
_id: cfg.key,
|
|
194
|
+
value: cfg.value,
|
|
195
|
+
}));
|
|
196
|
+
})
|
|
197
|
+
class SystemConfig extends Table {
|
|
198
|
+
@Field("string")
|
|
199
|
+
declare value: string;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Inheritance
|
|
204
|
+
|
|
205
|
+
Tables support standard class inheritance. Define a base table with common fields and extend it for specific use cases:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { Table, Field, Index, Relation } from "@antelopejs/interface-database-decorators";
|
|
209
|
+
|
|
210
|
+
class BaseEntity extends Table {
|
|
211
|
+
@Field("date")
|
|
212
|
+
declare createdAt: Date;
|
|
213
|
+
|
|
214
|
+
@Field("date")
|
|
215
|
+
declare updatedAt: Date;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
class User extends BaseEntity {
|
|
219
|
+
@Index()
|
|
220
|
+
@Field("string")
|
|
221
|
+
declare email: string;
|
|
222
|
+
|
|
223
|
+
@Field("string")
|
|
224
|
+
declare firstName: string;
|
|
225
|
+
|
|
226
|
+
@Field("string")
|
|
227
|
+
declare lastName: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
class Post extends BaseEntity {
|
|
231
|
+
@Index()
|
|
232
|
+
@Field("string")
|
|
233
|
+
@Relation({ to: () => User })
|
|
234
|
+
declare authorId: string;
|
|
235
|
+
|
|
236
|
+
@Field("string")
|
|
237
|
+
declare title: string;
|
|
238
|
+
|
|
239
|
+
@Field("string")
|
|
240
|
+
declare content: string;
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Register a Schema
|
|
245
|
+
|
|
246
|
+
The `RegisterSchema` function builds a `SchemaDefinition` from all tables registered for the given schema id (via `@RegisterTable`), constructs the `Schema` (which the adapter provisions), then inserts any fixture data for tables that are empty.
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { RegisterSchema } from "@antelopejs/interface-database-decorators";
|
|
250
|
+
|
|
251
|
+
await RegisterSchema("myapp");
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Parameters
|
|
255
|
+
|
|
256
|
+
| Parameter | Type | Description |
|
|
257
|
+
| ---------- | -------- | ----------------------------------------------------------------- |
|
|
258
|
+
| `schemaId` | `string` | Schema id matching the `@RegisterTable(_, schemaId)` declarations. |
|
|
259
|
+
|
|
260
|
+
`RegisterSchema` returns `Promise<void>`.
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# Table Modifiers
|
|
2
|
+
|
|
3
|
+
Table modifiers transform field values as they are stored in and retrieved from the database. They enable transparent encryption, hashing, localization, and custom data transformations. Add modifiers to a table class using the `Table.with()` method.
|
|
4
|
+
|
|
5
|
+
## Built-in Modifiers
|
|
6
|
+
|
|
7
|
+
### EncryptionModifier
|
|
8
|
+
|
|
9
|
+
The `EncryptionModifier` encrypts field values before storing them and decrypts them on retrieval. Both operations happen transparently.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { Table, Field, EncryptionModifier, Encrypted } from "@antelopejs/interface-database-decorators";
|
|
13
|
+
|
|
14
|
+
class UserCredentials extends Table.with(EncryptionModifier) {
|
|
15
|
+
@Encrypted({
|
|
16
|
+
secretKey: process.env.ENCRYPTION_KEY || "default-key",
|
|
17
|
+
algorithm: "aes-256-gcm",
|
|
18
|
+
ivSize: 16,
|
|
19
|
+
})
|
|
20
|
+
@Field("string")
|
|
21
|
+
declare creditCardNumber: string;
|
|
22
|
+
|
|
23
|
+
@Encrypted({ secretKey: process.env.ENCRYPTION_KEY || "default-key" })
|
|
24
|
+
@Field("any")
|
|
25
|
+
declare personalData: {
|
|
26
|
+
ssn: string;
|
|
27
|
+
birthDate: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
#### How It Works
|
|
33
|
+
|
|
34
|
+
- Data is encrypted using the specified algorithm before database insertion.
|
|
35
|
+
- A random initialization vector (IV) is generated for each write.
|
|
36
|
+
- For authenticated encryption algorithms (e.g., `aes-256-gcm`), an auth tag is stored alongside the ciphertext.
|
|
37
|
+
- On retrieval, the data is automatically decrypted.
|
|
38
|
+
- Date objects are preserved through JSON serialization with a custom encoder.
|
|
39
|
+
|
|
40
|
+
#### Options
|
|
41
|
+
|
|
42
|
+
| Option | Type | Default | Description |
|
|
43
|
+
| ----------- | -------- | ----------------- | -------------------------------- |
|
|
44
|
+
| `secretKey` | `string` | (required) | The encryption key. |
|
|
45
|
+
| `algorithm` | `string` | `"aes-256-gcm"` | Encryption algorithm. |
|
|
46
|
+
| `ivSize` | `number` | `16` | Initialization vector size in bytes. |
|
|
47
|
+
|
|
48
|
+
### HashModifier
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { Table, Field, HashModifier, Hashed } from "@antelopejs/interface-database-decorators";
|
|
54
|
+
|
|
55
|
+
class User extends Table.with(HashModifier) {
|
|
56
|
+
@Field("string")
|
|
57
|
+
declare email: string;
|
|
58
|
+
|
|
59
|
+
@Hashed({ algorithm: "sha256" })
|
|
60
|
+
@Field("string")
|
|
61
|
+
declare password: string;
|
|
62
|
+
|
|
63
|
+
verifyPassword(plainPassword: string): boolean {
|
|
64
|
+
return this.testHash("password", plainPassword);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### How It Works
|
|
70
|
+
|
|
71
|
+
- A random salt is generated and stored in the field's metadata on first write.
|
|
72
|
+
- The value is hashed with the salt using the specified algorithm.
|
|
73
|
+
- The `testHash` method (added by the mixin) hashes a candidate value with the stored salt and compares the result.
|
|
74
|
+
|
|
75
|
+
#### Options
|
|
76
|
+
|
|
77
|
+
| Option | Type | Default | Description |
|
|
78
|
+
| ----------- | -------- | ---------- | ------------------ |
|
|
79
|
+
| `algorithm` | `string` | `"sha256"` | Hashing algorithm. |
|
|
80
|
+
|
|
81
|
+
#### Mixin Method
|
|
82
|
+
|
|
83
|
+
The `HashModifier` mixin adds a `testHash` method to table instances:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
testHash(field: string, value: unknown): boolean
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Returns `true` if `value`, when hashed, matches the stored hash for the given field.
|
|
90
|
+
|
|
91
|
+
### LocalizationModifier
|
|
92
|
+
|
|
93
|
+
The `LocalizationModifier` stores multiple language versions of a field value. Access a specific locale by calling `localize()` on the table instance.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { Table, Field, LocalizationModifier, Localized } from "@antelopejs/interface-database-decorators";
|
|
97
|
+
|
|
98
|
+
class Product extends Table.with(LocalizationModifier) {
|
|
99
|
+
@Field("number")
|
|
100
|
+
declare price: number;
|
|
101
|
+
|
|
102
|
+
@Localized({ fallbackLocale: "en" })
|
|
103
|
+
@Field("string")
|
|
104
|
+
declare name: string;
|
|
105
|
+
|
|
106
|
+
@Localized({ fallbackLocale: "en" })
|
|
107
|
+
@Field("string")
|
|
108
|
+
declare description: string;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### Usage
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const product = new Product();
|
|
116
|
+
product._id = "prod-123";
|
|
117
|
+
product.price = 99.99;
|
|
118
|
+
|
|
119
|
+
// Set localized content
|
|
120
|
+
product.localize("en").name = "Premium Headphones";
|
|
121
|
+
product.localize("fr").name = "Casque Premium";
|
|
122
|
+
product.localize("es").name = "Auriculares Premium";
|
|
123
|
+
|
|
124
|
+
// Read localized content
|
|
125
|
+
const spanish = product.localize("es");
|
|
126
|
+
console.log(spanish.name); // "Auriculares Premium"
|
|
127
|
+
|
|
128
|
+
// Fallback to default locale when translation is missing
|
|
129
|
+
const german = product.localize("de");
|
|
130
|
+
console.log(german.name); // "Premium Headphones" (fallback)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Wildcard Key
|
|
134
|
+
|
|
135
|
+
The wildcard key `"*"` retrieves or sets all translations at once:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// Get all translations
|
|
139
|
+
const allNames = product.localize("*").name;
|
|
140
|
+
// { en: "Premium Headphones", fr: "Casque Premium", es: "Auriculares Premium" }
|
|
141
|
+
|
|
142
|
+
// Set all translations
|
|
143
|
+
product.localize("*").name = {
|
|
144
|
+
en: "Premium Headphones",
|
|
145
|
+
fr: "Casque Premium",
|
|
146
|
+
es: "Auriculares Premium",
|
|
147
|
+
de: "Premium-Kopfhoerer",
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### Options
|
|
152
|
+
|
|
153
|
+
| Option | Type | Description |
|
|
154
|
+
| ---------------- | -------- | ----------------------------------------------------- |
|
|
155
|
+
| `fallbackLocale` | `string` | Locale to use when the requested locale is unavailable. |
|
|
156
|
+
|
|
157
|
+
#### Mixin Method
|
|
158
|
+
|
|
159
|
+
The `LocalizationModifier` mixin adds a `localize` method:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
localize(locale: string, fields?: Array<keyof this>): this
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Sets the active locale for accessing localized fields. If `fields` is provided, only those fields are unlocked for the specified locale.
|
|
166
|
+
|
|
167
|
+
### AutoDateModifier
|
|
168
|
+
|
|
169
|
+
The `AutoDateModifier` automatically populates timestamp fields during insert and update operations. Use the `CreationTime` and `UpdateTime` decorators to mark fields.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { Table, CreationTime, UpdateTime } from "@antelopejs/interface-database-decorators";
|
|
173
|
+
|
|
174
|
+
class Article extends Table {
|
|
175
|
+
declare title: string;
|
|
176
|
+
|
|
177
|
+
@CreationTime()
|
|
178
|
+
declare creationDate: Date;
|
|
179
|
+
|
|
180
|
+
@UpdateTime()
|
|
181
|
+
declare updateDate: Date;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### How It Works
|
|
186
|
+
|
|
187
|
+
- On insert, both `CreationTime` and `UpdateTime` fields are set to the current date.
|
|
188
|
+
- On update, `UpdateTime` fields are refreshed while `CreationTime` fields are removed from the payload so the original creation date is preserved.
|
|
189
|
+
- This is an event-only modifier: it does not transform stored values and does not require adding a mixin through `Table.with()`.
|
|
190
|
+
|
|
191
|
+
## Combine Multiple Modifiers
|
|
192
|
+
|
|
193
|
+
Pass multiple modifier classes to `Table.with()`:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import {
|
|
197
|
+
Table,
|
|
198
|
+
Field,
|
|
199
|
+
EncryptionModifier,
|
|
200
|
+
HashModifier,
|
|
201
|
+
LocalizationModifier,
|
|
202
|
+
Encrypted,
|
|
203
|
+
Hashed,
|
|
204
|
+
Localized,
|
|
205
|
+
} from "@antelopejs/interface-database-decorators";
|
|
206
|
+
|
|
207
|
+
class UserProfile extends Table.with(EncryptionModifier, HashModifier, LocalizationModifier) {
|
|
208
|
+
@Encrypted({ secretKey: process.env.SECRET_KEY || "default-key" })
|
|
209
|
+
@Field("any")
|
|
210
|
+
declare privateInfo: {
|
|
211
|
+
address: string;
|
|
212
|
+
phoneNumber: string;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
@Hashed()
|
|
216
|
+
@Field("string")
|
|
217
|
+
declare password: string;
|
|
218
|
+
|
|
219
|
+
@Localized({ fallbackLocale: "en" })
|
|
220
|
+
@Field("string")
|
|
221
|
+
declare bio: string;
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Create Custom Modifiers
|
|
226
|
+
|
|
227
|
+
### OneWayModifier
|
|
228
|
+
|
|
229
|
+
A `OneWayModifier` transforms data in one direction (when writing to the database). Use it for irreversible transformations like hashing.
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { OneWayModifier, MixinSymbol, attachModifier } from "@antelopejs/interface-database-decorators";
|
|
233
|
+
import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators";
|
|
234
|
+
import { createHash } from "crypto";
|
|
235
|
+
|
|
236
|
+
class PrefixHashModifier extends OneWayModifier<string | undefined, [], {}, { algorithm?: string }> {
|
|
237
|
+
public readonly autolock = true;
|
|
238
|
+
|
|
239
|
+
public override lock(_locked: string | undefined, value: unknown) {
|
|
240
|
+
if (value === undefined) return undefined;
|
|
241
|
+
return createHash(this.options.algorithm || "sha256")
|
|
242
|
+
.update(JSON.stringify(value))
|
|
243
|
+
.digest("hex");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
[MixinSymbol] = class {};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const PrefixHashed = MakePropertyDecorator(
|
|
250
|
+
(target, propertyKey, options?: { algorithm?: string }) => {
|
|
251
|
+
attachModifier(target.constructor, PrefixHashModifier, propertyKey, options || {});
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The generic parameters for `OneWayModifier<LockedType, Args, Meta, Options>` are:
|
|
257
|
+
|
|
258
|
+
| Parameter | Description |
|
|
259
|
+
| ------------ | --------------------------------------------------- |
|
|
260
|
+
| `LockedType` | The type stored in the database after transformation |
|
|
261
|
+
| `Args` | Additional arguments for `lock` and `test` |
|
|
262
|
+
| `Meta` | Per-field metadata stored alongside the value |
|
|
263
|
+
| `Options` | Configuration passed via the decorator |
|
|
264
|
+
|
|
265
|
+
### TwoWayModifier
|
|
266
|
+
|
|
267
|
+
A `TwoWayModifier` extends `OneWayModifier` with an `unlock` method for reversible transformations like encryption:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { TwoWayModifier, MixinSymbol, attachModifier } from "@antelopejs/interface-database-decorators";
|
|
271
|
+
import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators";
|
|
272
|
+
|
|
273
|
+
class Base64Modifier extends TwoWayModifier<string, [], {}, { prefix?: string }> {
|
|
274
|
+
public readonly autolock = true;
|
|
275
|
+
public readonly autounlock = true;
|
|
276
|
+
|
|
277
|
+
public override lock(_locked: string | undefined, value: unknown) {
|
|
278
|
+
if (value === undefined) return undefined;
|
|
279
|
+
const prefix = this.options.prefix || "b64-";
|
|
280
|
+
return `${prefix}${Buffer.from(JSON.stringify(value)).toString("base64")}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public override unlock(locked: string) {
|
|
284
|
+
const prefix = this.options.prefix || "b64-";
|
|
285
|
+
const encoded = locked.substring(prefix.length);
|
|
286
|
+
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public override unlockrequest(data: any) {
|
|
290
|
+
return data;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
[MixinSymbol] = class {};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const Base64Encoded = MakePropertyDecorator(
|
|
297
|
+
(target, propertyKey, options?: { prefix?: string }) => {
|
|
298
|
+
attachModifier(target.constructor, Base64Modifier, propertyKey, options || {});
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### ContainerModifier
|
|
304
|
+
|
|
305
|
+
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:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
import { ContainerModifier } from "@antelopejs/interface-database-decorators";
|
|
309
|
+
|
|
310
|
+
class MyContainerModifier extends ContainerModifier<{}, { fallback?: string }> {
|
|
311
|
+
// Override unlock() to add fallback behavior
|
|
312
|
+
// Override unlockrequest() for query-time data access
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Modifier Events
|
|
317
|
+
|
|
318
|
+
Modifiers can hook into the data lifecycle by implementing event handler methods. These methods are called during data conversion and database operations:
|
|
319
|
+
|
|
320
|
+
| Event | Triggered When |
|
|
321
|
+
| --------------- | ----------------------------------------- |
|
|
322
|
+
| `fromPlainData` | After converting plain data to an instance |
|
|
323
|
+
| `toPlainData` | After converting an instance to plain data |
|
|
324
|
+
| `fromDatabase` | After reading from the database |
|
|
325
|
+
| `toDatabase` | Before writing to the database |
|
|
326
|
+
| `insert` | Before inserting a new record |
|
|
327
|
+
| `update` | Before updating an existing record |
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { Modifier, MixinSymbol } from "@antelopejs/interface-database-decorators";
|
|
331
|
+
|
|
332
|
+
class TimestampModifier extends Modifier {
|
|
333
|
+
public insert(instance: any, field: string) {
|
|
334
|
+
if (field === "createdAt" && !instance[field]) {
|
|
335
|
+
instance[field] = new Date();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
public update(instance: any, field: string) {
|
|
340
|
+
if (field === "updatedAt") {
|
|
341
|
+
instance[field] = new Date();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
[MixinSymbol] = class {};
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Event-only modifiers (without `lock`/`unlock`) are valid and useful for side effects like logging, validation, or auto-populating fields.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Data Models
|
|
2
|
+
|
|
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.
|
|
4
|
+
|
|
5
|
+
## BasicDataModel
|
|
6
|
+
|
|
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.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { BasicDataModel } from "@antelopejs/interface-database-decorators";
|
|
11
|
+
import { User } from "./user.table";
|
|
12
|
+
|
|
13
|
+
const UserModel = BasicDataModel(User, "users");
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Parameters
|
|
17
|
+
|
|
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
|
+
|
|
23
|
+
When the table class has been decorated with `@RegisterTable`, the `tableName` parameter can be omitted.
|
|
24
|
+
|
|
25
|
+
## Instantiate a Model
|
|
26
|
+
|
|
27
|
+
A model instance requires a `SchemaInstance` from the database interface:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Schema } from "@antelopejs/interface-database";
|
|
31
|
+
|
|
32
|
+
const schema = Schema.get("myapp")!;
|
|
33
|
+
const db = schema.instance();
|
|
34
|
+
|
|
35
|
+
const userModel = new UserModel(db);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## CRUD Operations
|
|
39
|
+
|
|
40
|
+
### Read
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// Get by primary key
|
|
44
|
+
const user = await userModel.get("user-123");
|
|
45
|
+
|
|
46
|
+
// Get by secondary index
|
|
47
|
+
const admins = await userModel.getBy("role", "admin");
|
|
48
|
+
|
|
49
|
+
// Get all documents
|
|
50
|
+
const allUsers = await userModel.getAll();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Create
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// Insert a single document
|
|
57
|
+
const ids = await userModel.insert({
|
|
58
|
+
email: "alice@example.com",
|
|
59
|
+
name: "Alice",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Insert multiple documents
|
|
63
|
+
const batchIds = await userModel.insert([
|
|
64
|
+
{ email: "bob@example.com", name: "Bob" },
|
|
65
|
+
{ email: "carol@example.com", name: "Carol" },
|
|
66
|
+
]);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Update
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Update by ID
|
|
73
|
+
await userModel.update("user-123", { name: "Updated Name" });
|
|
74
|
+
|
|
75
|
+
// Update by including the primary key in the object
|
|
76
|
+
await userModel.update({ _id: "user-123", name: "Updated Name" });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> **Important:** When calling `update` without a separate ID argument, the object must include the primary key field.
|
|
80
|
+
|
|
81
|
+
### Delete
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
await userModel.delete("user-123");
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Data Conversion
|
|
88
|
+
|
|
89
|
+
Data models include static methods for converting between different data representations:
|
|
90
|
+
|
|
91
|
+
### `fromPlainData(obj)`
|
|
92
|
+
|
|
93
|
+
Convert a plain JavaScript object into a Table class instance. Modifier transformations (like encryption and hashing) are applied during this conversion.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
const user = UserModel.fromPlainData({
|
|
97
|
+
_id: "user-123",
|
|
98
|
+
email: "user@example.com",
|
|
99
|
+
name: "John",
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `fromDatabase(obj)`
|
|
104
|
+
|
|
105
|
+
Convert a raw database record into a Table class instance. This attaches the class prototype and triggers the `fromDatabase` modifier event.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const user = UserModel.fromDatabase(rawDbRecord);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `toDatabase(obj)`
|
|
112
|
+
|
|
113
|
+
Convert a Table class instance into a plain object suitable for database storage. Modifier events like `toDatabase` are triggered.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const dbReady = UserModel.toDatabase(userInstance);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Validation
|
|
120
|
+
|
|
121
|
+
`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.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const result = UserModel.validate({ name: "Alice", age: 30 });
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
console.error(result.errors); // [{ field: "age", message: "not number" }, ...]
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
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.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
await userModel.insert({ name: "Alice", age: 30 }, { validate: true });
|
|
134
|
+
await userModel.update("user-123", { name: "Bob" }, { validate: true });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`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`.
|
|
138
|
+
|
|
139
|
+
## Extend BasicDataModel
|
|
140
|
+
|
|
141
|
+
Extend the generated model class to add custom query methods:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const UserModelBase = BasicDataModel(User, "users");
|
|
145
|
+
|
|
146
|
+
class CustomUserModel extends UserModelBase {
|
|
147
|
+
async findActive() {
|
|
148
|
+
return this.table
|
|
149
|
+
.filter((u) => u.key("status").eq("active"))
|
|
150
|
+
.then((rows) => rows.map(CustomUserModel.fromDatabase) as User[]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async register(email: string, name: string) {
|
|
154
|
+
const existing = await this.getBy("email", email);
|
|
155
|
+
if (existing.length > 0) {
|
|
156
|
+
throw new Error("Email already in use");
|
|
157
|
+
}
|
|
158
|
+
return this.insert({ email, name, status: "active" });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## GetModel
|
|
164
|
+
|
|
165
|
+
The `GetModel` function retrieves or creates a cached model instance for a given schema:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { GetModel } from "@antelopejs/interface-database-decorators";
|
|
169
|
+
|
|
170
|
+
const userModel = GetModel(UserModel, "instance-id");
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Model instances are cached by class and instance ID, so repeated calls with the same arguments return the same instance. The schema is resolved automatically from the `schemaName` property on the model class.
|
|
174
|
+
|
|
175
|
+
## Modifier Integration
|
|
176
|
+
|
|
177
|
+
Data models automatically apply all modifier transformations:
|
|
178
|
+
|
|
179
|
+
- **On insert** - `fromPlainData` conversion is applied, the `insert` event fires, and the result is converted with `toDatabase` before writing.
|
|
180
|
+
- **On update** - The same conversion pipeline applies with the `update` event.
|
|
181
|
+
- **On read** - Results from `get`, `getBy`, and `getAll` are converted with `fromDatabase`.
|
|
182
|
+
|
|
183
|
+
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.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Parameter Decoration
|
|
2
|
+
|
|
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.
|
|
4
|
+
|
|
5
|
+
## The Model Decorator
|
|
6
|
+
|
|
7
|
+
The `Model` decorator injects a cached model instance. It can be used as either a parameter decorator or a property decorator.
|
|
8
|
+
|
|
9
|
+
### Static Instance ID
|
|
10
|
+
|
|
11
|
+
Pass a fixed instance ID to bind the model to a specific schema instance:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Controller, Get } from "@antelopejs/interface-api";
|
|
15
|
+
import { Model } from "@antelopejs/interface-database-decorators";
|
|
16
|
+
import { UserModel } from "./user.model";
|
|
17
|
+
|
|
18
|
+
class UsersController extends Controller("/users") {
|
|
19
|
+
@Get()
|
|
20
|
+
async listUsers(@Model(UserModel, "main") userModel: InstanceType<typeof UserModel>) {
|
|
21
|
+
return await userModel.getAll();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Dynamic Instance ID
|
|
27
|
+
|
|
28
|
+
Pass a callback function to determine the instance ID at request time. The callback receives the request context:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { Controller, Get, Parameter } from "@antelopejs/interface-api";
|
|
32
|
+
import { Model } from "@antelopejs/interface-database-decorators";
|
|
33
|
+
import { UserModel } from "./user.model";
|
|
34
|
+
|
|
35
|
+
class MultitenantController extends Controller("/tenants") {
|
|
36
|
+
@Get("/:tenantId/users")
|
|
37
|
+
async getTenantUsers(
|
|
38
|
+
@Parameter("tenantId") tenantId: string,
|
|
39
|
+
@Model(UserModel, (ctx) => `tenant-${ctx.params.tenantId}`) userModel: InstanceType<typeof UserModel>,
|
|
40
|
+
) {
|
|
41
|
+
return await userModel.getAll();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Property Decoration
|
|
47
|
+
|
|
48
|
+
The `Model` decorator also works as a property decorator. The model is initialized when the controller handles a request:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { Controller, Get, Parameter } from "@antelopejs/interface-api";
|
|
52
|
+
import { Model } from "@antelopejs/interface-database-decorators";
|
|
53
|
+
import { UserModel } from "./models";
|
|
54
|
+
|
|
55
|
+
class UsersController extends Controller("/users") {
|
|
56
|
+
@Model(UserModel, "main")
|
|
57
|
+
private userModel!: InstanceType<typeof UserModel>;
|
|
58
|
+
|
|
59
|
+
@Get()
|
|
60
|
+
async listUsers() {
|
|
61
|
+
return await this.userModel.getAll();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Get("/:id")
|
|
65
|
+
async getUser(@Parameter("id") id: string) {
|
|
66
|
+
return await this.userModel.get(id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Model Caching
|
|
72
|
+
|
|
73
|
+
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:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { GetModel } from "@antelopejs/interface-database-decorators";
|
|
77
|
+
|
|
78
|
+
// These return the same instance
|
|
79
|
+
const model1 = GetModel(UserModel, "main");
|
|
80
|
+
const model2 = GetModel(UserModel, "main");
|
|
81
|
+
|
|
82
|
+
// This returns a different instance
|
|
83
|
+
const model3 = GetModel(UserModel, "other");
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Combine with API Decorators
|
|
87
|
+
|
|
88
|
+
Model decorators compose naturally with other API decorators for building complete CRUD controllers:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { Controller, Get, Post, Put, Delete, RawBody, Parameter } from "@antelopejs/interface-api";
|
|
92
|
+
import { Model } from "@antelopejs/interface-database-decorators";
|
|
93
|
+
import { UserModel } from "./user.model";
|
|
94
|
+
|
|
95
|
+
class UsersController extends Controller("/users") {
|
|
96
|
+
@Get()
|
|
97
|
+
async list(@Model(UserModel, "main") model: InstanceType<typeof UserModel>) {
|
|
98
|
+
return await model.getAll();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Get("/:id")
|
|
102
|
+
async get(
|
|
103
|
+
@Parameter("id") id: string,
|
|
104
|
+
@Model(UserModel, "main") model: InstanceType<typeof UserModel>,
|
|
105
|
+
) {
|
|
106
|
+
return await model.get(id);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Post()
|
|
110
|
+
async create(
|
|
111
|
+
@RawBody() body: Buffer,
|
|
112
|
+
@Model(UserModel, "main") model: InstanceType<typeof UserModel>,
|
|
113
|
+
) {
|
|
114
|
+
const data = JSON.parse(body.toString());
|
|
115
|
+
return await model.insert(data);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Put("/:id")
|
|
119
|
+
async update(
|
|
120
|
+
@Parameter("id") id: string,
|
|
121
|
+
@RawBody() body: Buffer,
|
|
122
|
+
@Model(UserModel, "main") model: InstanceType<typeof UserModel>,
|
|
123
|
+
) {
|
|
124
|
+
const data = JSON.parse(body.toString());
|
|
125
|
+
return await model.update(id, data);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Delete("/:id")
|
|
129
|
+
async remove(
|
|
130
|
+
@Parameter("id") id: string,
|
|
131
|
+
@Model(UserModel, "main") model: InstanceType<typeof UserModel>,
|
|
132
|
+
) {
|
|
133
|
+
return await model.delete(id);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Custom Parameter Providers
|
|
139
|
+
|
|
140
|
+
For more complex injection logic, create custom parameter providers using the core API:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { SetParameterProvider } from "@antelopejs/interface-api";
|
|
144
|
+
import { GetModel } from "@antelopejs/interface-database-decorators";
|
|
145
|
+
import { MakeParameterAndPropertyDecorator } from "@antelopejs/interface-core/decorators";
|
|
146
|
+
|
|
147
|
+
const CustomModel = MakeParameterAndPropertyDecorator(
|
|
148
|
+
(target, key, index, modelClass, resolver) => {
|
|
149
|
+
SetParameterProvider(target, key, index, (ctx) => {
|
|
150
|
+
const instanceId = resolver(ctx);
|
|
151
|
+
return GetModel(modelClass, instanceId);
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/interface-database-decorators",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "AntelopeJS interface for database schema decorators and model definitions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"main": "dist/index.js",
|
|
19
19
|
"types": "dist/index.d.ts",
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"docs",
|
|
23
|
+
"skills"
|
|
22
24
|
],
|
|
23
25
|
"exports": {
|
|
24
26
|
".": {
|
|
@@ -148,6 +150,9 @@
|
|
|
148
150
|
},
|
|
149
151
|
"antelopeJs": {
|
|
150
152
|
"standalone": true,
|
|
151
|
-
"test": "src/antelope.test.ts"
|
|
153
|
+
"test": "src/antelope.test.ts",
|
|
154
|
+
"skills": [
|
|
155
|
+
"./skills"
|
|
156
|
+
]
|
|
152
157
|
}
|
|
153
158
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: database-decorators-interface
|
|
3
|
+
description: Provides decorator-based database table definitions for AntelopeJS - a Table base class with RegisterTable/Index/Field/Fixture/Relation decorators, field modifiers (Encrypted, Hashed, Localized, CreationTime, UpdateTime), BasicDataModel CRUD models with validation, RegisterSchema provisioning, and the Model decorator for injecting models into API controllers. Use when code imports @antelopejs/interface-database-decorators (root or /table, /schema, /database, /model, /relation, /common, /modifiers/* subpaths), or when asked to define a database table or data model class, add encrypted/hashed/localized/timestamp fields, seed fixture data, register a schema, or inject a data model into an interface-api controller.
|
|
4
|
+
category: antelopejs-interface
|
|
5
|
+
tags: [database, decorators, orm, models, antelopejs]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Database Decorators Interface
|
|
9
|
+
|
|
10
|
+
Consumer-side decorator layer on top of `@antelopejs/interface-database` (AQL). This package itself has no proxy
|
|
11
|
+
points and no provider side — you never `ImplementInterface` it. All actual database access flows through
|
|
12
|
+
`Schema`/`Table` from `@antelopejs/interface-database`, which must be implemented by a database module (e.g. MongoDB).
|
|
13
|
+
Peer interfaces required: `@antelopejs/interface-core`, `@antelopejs/interface-api` (only for the `Model` controller decorator), `@antelopejs/interface-database`.
|
|
14
|
+
|
|
15
|
+
## Imports
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { Table, Field, Index, Fixture } from "@antelopejs/interface-database-decorators/table";
|
|
19
|
+
import { RegisterTable, getTablesForSchema } from "@antelopejs/interface-database-decorators/schema";
|
|
20
|
+
import { RegisterSchema } from "@antelopejs/interface-database-decorators/database";
|
|
21
|
+
import { BasicDataModel, GetModel, Model } from "@antelopejs/interface-database-decorators/model";
|
|
22
|
+
import { Relation } from "@antelopejs/interface-database-decorators/relation";
|
|
23
|
+
import { DatumStaticMetadata, getMetadata } from "@antelopejs/interface-database-decorators/common";
|
|
24
|
+
import { CreationTime, UpdateTime } from "@antelopejs/interface-database-decorators/modifiers/autodate";
|
|
25
|
+
import { Encrypted, EncryptionModifier } from "@antelopejs/interface-database-decorators/modifiers/encryption";
|
|
26
|
+
import { Hashed, HashModifier } from "@antelopejs/interface-database-decorators/modifiers/hash";
|
|
27
|
+
import { Localized, LocalizationModifier } from "@antelopejs/interface-database-decorators/modifiers/localization";
|
|
28
|
+
import { Modifier, OneWayModifier, TwoWayModifier, attachModifier, toPlainData } from "@antelopejs/interface-database-decorators/modifiers/common";
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The package root re-exports everything above and also imports `reflect-metadata` (see gotchas).
|
|
32
|
+
|
|
33
|
+
## Minimal usage
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
@RegisterTable("recipes", "cookbook") // (tableName, schemaName)
|
|
37
|
+
class Recipe extends Table {
|
|
38
|
+
// _id: string is inherited as the primary key
|
|
39
|
+
@Index() @Field("string")
|
|
40
|
+
declare slug: string;
|
|
41
|
+
@Field("string")
|
|
42
|
+
declare tagline: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const RecipeModel = BasicDataModel(Recipe); // table name taken from @RegisterTable
|
|
46
|
+
await RegisterSchema("cookbook"); // once at startup: provisions all tables registered for "cookbook", runs fixtures
|
|
47
|
+
|
|
48
|
+
const recipes = GetModel(RecipeModel); // cached per (model class, instanceId)
|
|
49
|
+
await recipes.insert({ slug: "grilled-halloumi", tagline: "Smoky skewers" }, { validate: true });
|
|
50
|
+
const bySlug = await recipes.getBy("slug", "grilled-halloumi");
|
|
51
|
+
const one = await recipes.get(bySlug[0]._id);
|
|
52
|
+
await recipes.update(one._id, { tagline: "Smoky halloumi skewers" });
|
|
53
|
+
await recipes.delete(one._id);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Controller injection (works as parameter or property decorator; second arg is a static `InstanceId` or a
|
|
57
|
+
`(ctx: RequestContext) => InstanceId | undefined` callback — returning `undefined` falls back to the default schema instance):
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
class RecipesController extends Controller("/recipes") {
|
|
61
|
+
@Get()
|
|
62
|
+
async list(@Model(RecipeModel) recipes: InstanceType<typeof RecipeModel>) {
|
|
63
|
+
return recipes.getAll();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Field modifiers
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
class Chef extends Table.with(HashModifier, EncryptionModifier, LocalizationModifier) {
|
|
72
|
+
@Hashed() declare passphrase: string; // one-way; test via instance.testHash("passphrase", value)
|
|
73
|
+
@Encrypted({ secretKey }) declare supplierCode: string; // transparent encrypt/decrypt (autolock + autounlock)
|
|
74
|
+
@Localized() declare motto: string; // read AND write via instance.localize("en", ["motto"])
|
|
75
|
+
@CreationTime() declare hiredAt: Date; // set on insert, stripped from updates
|
|
76
|
+
@UpdateTime() declare lastActiveAt: Date; // refreshed on insert and update
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Gotchas
|
|
81
|
+
|
|
82
|
+
- Only the package root imports `reflect-metadata`; if you import only subpaths, ensure `reflect-metadata`
|
|
83
|
+
is loaded once before decorators run (e.g. `import "@antelopejs/interface-database-decorators"`).
|
|
84
|
+
- `Hashed`, `Encrypted`, and `Localized` require the matching modifier class in `Table.with(...)`; `CreationTime`/`UpdateTime` are event-only and need no mixin.
|
|
85
|
+
- Modifier ordering matters: `attachModifier` throws if you stack a modifier after a one-way modifier (e.g. anything after `@Hashed` on the same field).
|
|
86
|
+
- Hashed fields cannot be read back or queried directly — only equality-tested with `testHash`.
|
|
87
|
+
- Localized fields must be written through a localized instance (`instance.localize(locale).field = ...`);
|
|
88
|
+
values assigned before `localize()` are held in floating state and silently dropped on insert/update.
|
|
89
|
+
- `@Relation({ to: () => Target })` is declarative metadata only: no foreign-key enforcement, consumed by introspection tooling via `RelationStaticMetadata`.
|
|
90
|
+
- `Model.validate` (and `{ validate: true }` on insert/update) only checks fields whose `@Field` type is an
|
|
91
|
+
io-ts-style codec with `.decode`; plain string field tokens are skipped. Update validates with
|
|
92
|
+
`{ partial: true }` semantics.
|
|
93
|
+
- `update(obj)` without an explicit id asserts that the object carries the primary key (`_id` by default).
|
|
94
|
+
- `@Fixture` data is only inserted by `RegisterSchema` when the table is empty (count === 0).
|
|
95
|
+
- `GetModel` caches by model class + instanceId; the same pair always returns the same instance.
|
|
96
|
+
- Serialization of modified instances to plain JSON goes through `toPlainData` (auto-attached as `toJSON`).
|
|
97
|
+
|
|
98
|
+
Deeper reference: this package's `docs/` chapters — Introduction, Table Definitions, Table Modifiers,
|
|
99
|
+
Data Models, Parameter Decoration — and the shipped `.d.ts` files. Do not duplicate them here.
|