@antelopejs/interface-database-decorators 0.1.5 → 0.1.7

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.
Files changed (54) hide show
  1. package/README.md +0 -2
  2. package/dist/antelope.test.js +4 -3
  3. package/dist/antelope.test.js.map +1 -1
  4. package/dist/common.d.ts +2 -1
  5. package/dist/common.js +5 -1
  6. package/dist/common.js.map +1 -1
  7. package/dist/database.js +12 -8
  8. package/dist/database.js.map +1 -1
  9. package/dist/model.d.ts +2 -2
  10. package/dist/model.js +10 -10
  11. package/dist/model.js.map +1 -1
  12. package/dist/modifiers/autodate.js.map +1 -1
  13. package/dist/modifiers/common.js.map +1 -1
  14. package/dist/modifiers/encryption.js +1 -1
  15. package/dist/modifiers/encryption.js.map +1 -1
  16. package/dist/modifiers/hash.js +1 -1
  17. package/dist/modifiers/hash.js.map +1 -1
  18. package/dist/modifiers/localization.js.map +1 -1
  19. package/dist/relation.js.map +1 -1
  20. package/dist/schema.js.map +1 -1
  21. package/dist/table.d.ts +21 -5
  22. package/dist/table.js +8 -8
  23. package/dist/table.js.map +1 -1
  24. package/dist/tests/common.test.js +1 -1
  25. package/dist/tests/common.test.js.map +1 -1
  26. package/dist/tests/database.test.js +80 -2
  27. package/dist/tests/database.test.js.map +1 -1
  28. package/dist/tests/integration.test.js +4 -4
  29. package/dist/tests/integration.test.js.map +1 -1
  30. package/dist/tests/model.test.js +4 -4
  31. package/dist/tests/model.test.js.map +1 -1
  32. package/dist/tests/modifiers/autodate.test.js +3 -3
  33. package/dist/tests/modifiers/autodate.test.js.map +1 -1
  34. package/dist/tests/modifiers/common.test.js +2 -2
  35. package/dist/tests/modifiers/common.test.js.map +1 -1
  36. package/dist/tests/modifiers/encryption.test.js +2 -2
  37. package/dist/tests/modifiers/encryption.test.js.map +1 -1
  38. package/dist/tests/modifiers/hash.test.js +2 -2
  39. package/dist/tests/modifiers/hash.test.js.map +1 -1
  40. package/dist/tests/modifiers/localization.test.js +2 -2
  41. package/dist/tests/modifiers/localization.test.js.map +1 -1
  42. package/dist/tests/relation.test.js +2 -2
  43. package/dist/tests/relation.test.js.map +1 -1
  44. package/dist/tests/schema.test.js +2 -2
  45. package/dist/tests/schema.test.js.map +1 -1
  46. package/dist/tests/table.test.js +11 -11
  47. package/dist/tests/table.test.js.map +1 -1
  48. package/docs/1.introduction.md +22 -0
  49. package/docs/2.table-definitions.md +311 -0
  50. package/docs/3.table-modifiers.md +403 -0
  51. package/docs/4.data-models.md +183 -0
  52. package/docs/5.parameter-decoration.md +166 -0
  53. package/package.json +73 -59
  54. package/skills/database-decorators-interface/SKILL.md +143 -0
@@ -0,0 +1,403 @@
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 {
13
+ Table,
14
+ Field,
15
+ EncryptionModifier,
16
+ Encrypted,
17
+ } from "@antelopejs/interface-database-decorators";
18
+
19
+ class UserCredentials extends Table.with(EncryptionModifier) {
20
+ @Encrypted({
21
+ secretKey: process.env.ENCRYPTION_KEY || "default-key",
22
+ algorithm: "aes-256-gcm",
23
+ ivSize: 16,
24
+ })
25
+ @Field("string")
26
+ declare creditCardNumber: string;
27
+
28
+ @Encrypted({ secretKey: process.env.ENCRYPTION_KEY || "default-key" })
29
+ @Field("any")
30
+ declare personalData: {
31
+ ssn: string;
32
+ birthDate: string;
33
+ };
34
+ }
35
+ ```
36
+
37
+ #### How It Works
38
+
39
+ - Data is encrypted using the specified algorithm before database insertion.
40
+ - A random initialization vector (IV) is generated for each write.
41
+ - For authenticated encryption algorithms (e.g., `aes-256-gcm`), an auth tag is stored alongside the ciphertext.
42
+ - On retrieval, the data is automatically decrypted.
43
+ - Date objects are preserved through JSON serialization with a custom encoder.
44
+
45
+ #### Options
46
+
47
+ | Option | Type | Default | Description |
48
+ | ----------- | -------- | --------------- | ------------------------------------ |
49
+ | `secretKey` | `string` | (required) | The encryption key. |
50
+ | `algorithm` | `string` | `"aes-256-gcm"` | Encryption algorithm. |
51
+ | `ivSize` | `number` | `16` | Initialization vector size in bytes. |
52
+
53
+ ### HashModifier
54
+
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.
56
+
57
+ ```typescript
58
+ import {
59
+ Table,
60
+ Field,
61
+ HashModifier,
62
+ Hashed,
63
+ } from "@antelopejs/interface-database-decorators";
64
+
65
+ class User extends Table.with(HashModifier) {
66
+ @Field("string")
67
+ declare email: string;
68
+
69
+ @Hashed({ algorithm: "sha256" })
70
+ @Field("string")
71
+ declare password: string;
72
+
73
+ verifyPassword(plainPassword: string): boolean {
74
+ return this.testHash("password", plainPassword);
75
+ }
76
+ }
77
+ ```
78
+
79
+ #### How It Works
80
+
81
+ - A random salt is generated and stored in the field's metadata on first write.
82
+ - The value is hashed with the salt using the specified algorithm.
83
+ - The `testHash` method (added by the mixin) hashes a candidate value with the stored salt and compares the result.
84
+
85
+ #### Options
86
+
87
+ | Option | Type | Default | Description |
88
+ | ----------- | -------- | ---------- | ------------------ |
89
+ | `algorithm` | `string` | `"sha256"` | Hashing algorithm. |
90
+
91
+ #### Mixin Method
92
+
93
+ The `HashModifier` mixin adds a `testHash` method to table instances:
94
+
95
+ ```typescript
96
+ testHash(field: string, value: unknown): boolean
97
+ ```
98
+
99
+ Returns `true` if `value`, when hashed, matches the stored hash for the given field.
100
+
101
+ ### LocalizationModifier
102
+
103
+ The `LocalizationModifier` stores multiple language versions of a field value. Access a specific locale by calling `localize()` on the table instance.
104
+
105
+ ```typescript
106
+ import {
107
+ Table,
108
+ Field,
109
+ LocalizationModifier,
110
+ Localized,
111
+ } from "@antelopejs/interface-database-decorators";
112
+
113
+ class Product extends Table.with(LocalizationModifier) {
114
+ @Field("number")
115
+ declare price: number;
116
+
117
+ @Localized({ fallbackLocale: "en" })
118
+ @Field("string")
119
+ declare name: string;
120
+
121
+ @Localized({ fallbackLocale: "en" })
122
+ @Field("string")
123
+ declare description: string;
124
+ }
125
+ ```
126
+
127
+ #### Usage
128
+
129
+ ```typescript
130
+ const product = new Product();
131
+ product._id = "prod-123";
132
+ product.price = 99.99;
133
+
134
+ // Set localized content
135
+ product.localize("en").name = "Premium Headphones";
136
+ product.localize("fr").name = "Casque Premium";
137
+ product.localize("es").name = "Auriculares Premium";
138
+
139
+ // Read localized content
140
+ const spanish = product.localize("es");
141
+ console.log(spanish.name); // "Auriculares Premium"
142
+
143
+ // Fallback to default locale when translation is missing
144
+ const german = product.localize("de");
145
+ console.log(german.name); // "Premium Headphones" (fallback)
146
+ ```
147
+
148
+ #### Wildcard Key
149
+
150
+ The wildcard key `"*"` retrieves or sets all translations at once:
151
+
152
+ ```typescript
153
+ // Get all translations
154
+ const allNames = product.localize("*").name;
155
+ // { en: "Premium Headphones", fr: "Casque Premium", es: "Auriculares Premium" }
156
+
157
+ // Set all translations
158
+ product.localize("*").name = {
159
+ en: "Premium Headphones",
160
+ fr: "Casque Premium",
161
+ es: "Auriculares Premium",
162
+ de: "Premium-Kopfhoerer",
163
+ };
164
+ ```
165
+
166
+ #### Options
167
+
168
+ | Option | Type | Description |
169
+ | ---------------- | -------- | ------------------------------------------------------- |
170
+ | `fallbackLocale` | `string` | Locale to use when the requested locale is unavailable. |
171
+
172
+ #### Mixin Method
173
+
174
+ The `LocalizationModifier` mixin adds a `localize` method:
175
+
176
+ ```typescript
177
+ localize(locale: string, fields?: Array<keyof this>): this
178
+ ```
179
+
180
+ Sets the active locale for accessing localized fields. If `fields` is provided, only those fields are unlocked for the specified locale.
181
+
182
+ ### AutoDateModifier
183
+
184
+ The `AutoDateModifier` automatically populates timestamp fields during insert and update operations. Use the `CreationTime` and `UpdateTime` decorators to mark fields.
185
+
186
+ ```typescript
187
+ import {
188
+ Table,
189
+ CreationTime,
190
+ UpdateTime,
191
+ } from "@antelopejs/interface-database-decorators";
192
+
193
+ class Article extends Table {
194
+ declare title: string;
195
+
196
+ @CreationTime()
197
+ declare creationDate: Date;
198
+
199
+ @UpdateTime()
200
+ declare updateDate: Date;
201
+ }
202
+ ```
203
+
204
+ #### How It Works
205
+
206
+ - On insert, both `CreationTime` and `UpdateTime` fields are set to the current date.
207
+ - On update, `UpdateTime` fields are refreshed while `CreationTime` fields are removed from the payload so the original creation date is preserved.
208
+ - This is an event-only modifier: it does not transform stored values and does not require adding a mixin through `Table.with()`.
209
+
210
+ ## Combine Multiple Modifiers
211
+
212
+ Pass multiple modifier classes to `Table.with()`:
213
+
214
+ ```typescript
215
+ import {
216
+ Table,
217
+ Field,
218
+ EncryptionModifier,
219
+ HashModifier,
220
+ LocalizationModifier,
221
+ Encrypted,
222
+ Hashed,
223
+ Localized,
224
+ } from "@antelopejs/interface-database-decorators";
225
+
226
+ class UserProfile extends Table.with(
227
+ EncryptionModifier,
228
+ HashModifier,
229
+ LocalizationModifier,
230
+ ) {
231
+ @Encrypted({ secretKey: process.env.SECRET_KEY || "default-key" })
232
+ @Field("any")
233
+ declare privateInfo: {
234
+ address: string;
235
+ phoneNumber: string;
236
+ };
237
+
238
+ @Hashed()
239
+ @Field("string")
240
+ declare password: string;
241
+
242
+ @Localized({ fallbackLocale: "en" })
243
+ @Field("string")
244
+ declare bio: string;
245
+ }
246
+ ```
247
+
248
+ ## Create Custom Modifiers
249
+
250
+ ### OneWayModifier
251
+
252
+ A `OneWayModifier` transforms data in one direction (when writing to the database). Use it for irreversible transformations like hashing.
253
+
254
+ ```typescript
255
+ import {
256
+ OneWayModifier,
257
+ MixinSymbol,
258
+ attachModifier,
259
+ } from "@antelopejs/interface-database-decorators";
260
+ import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators";
261
+ import { createHash } from "crypto";
262
+
263
+ class PrefixHashModifier extends OneWayModifier<
264
+ string | undefined,
265
+ [],
266
+ {},
267
+ { algorithm?: string }
268
+ > {
269
+ public readonly autolock = true;
270
+
271
+ public override lock(_locked: string | undefined, value: unknown) {
272
+ if (value === undefined) return undefined;
273
+ return createHash(this.options.algorithm || "sha256")
274
+ .update(JSON.stringify(value))
275
+ .digest("hex");
276
+ }
277
+
278
+ [MixinSymbol] = class {};
279
+ }
280
+
281
+ const PrefixHashed = MakePropertyDecorator(
282
+ (target, propertyKey, options?: { algorithm?: string }) => {
283
+ attachModifier(
284
+ target.constructor,
285
+ PrefixHashModifier,
286
+ propertyKey,
287
+ options || {},
288
+ );
289
+ },
290
+ );
291
+ ```
292
+
293
+ The generic parameters for `OneWayModifier<LockedType, Args, Meta, Options>` are:
294
+
295
+ | Parameter | Description |
296
+ | ------------ | ---------------------------------------------------- |
297
+ | `LockedType` | The type stored in the database after transformation |
298
+ | `Args` | Additional arguments for `lock` and `test` |
299
+ | `Meta` | Per-field metadata stored alongside the value |
300
+ | `Options` | Configuration passed via the decorator |
301
+
302
+ ### TwoWayModifier
303
+
304
+ A `TwoWayModifier` extends `OneWayModifier` with an `unlock` method for reversible transformations like encryption:
305
+
306
+ ```typescript
307
+ import {
308
+ TwoWayModifier,
309
+ MixinSymbol,
310
+ attachModifier,
311
+ } from "@antelopejs/interface-database-decorators";
312
+ import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators";
313
+
314
+ class Base64Modifier extends TwoWayModifier<
315
+ string,
316
+ [],
317
+ {},
318
+ { prefix?: string }
319
+ > {
320
+ public readonly autolock = true;
321
+ public readonly autounlock = true;
322
+
323
+ public override lock(_locked: string | undefined, value: unknown) {
324
+ if (value === undefined) return undefined;
325
+ const prefix = this.options.prefix || "b64-";
326
+ return `${prefix}${Buffer.from(JSON.stringify(value)).toString("base64")}`;
327
+ }
328
+
329
+ public override unlock(locked: string) {
330
+ const prefix = this.options.prefix || "b64-";
331
+ const encoded = locked.substring(prefix.length);
332
+ return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
333
+ }
334
+
335
+ public override unlockrequest(data: any) {
336
+ return data;
337
+ }
338
+
339
+ [MixinSymbol] = class {};
340
+ }
341
+
342
+ const Base64Encoded = MakePropertyDecorator(
343
+ (target, propertyKey, options?: { prefix?: string }) => {
344
+ attachModifier(
345
+ target.constructor,
346
+ Base64Modifier,
347
+ propertyKey,
348
+ options || {},
349
+ );
350
+ },
351
+ );
352
+ ```
353
+
354
+ ### ContainerModifier
355
+
356
+ 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:
357
+
358
+ ```typescript
359
+ import { ContainerModifier } from "@antelopejs/interface-database-decorators";
360
+
361
+ class MyContainerModifier extends ContainerModifier<{}, { fallback?: string }> {
362
+ // Override unlock() to add fallback behavior
363
+ // Override unlockrequest() for query-time data access
364
+ }
365
+ ```
366
+
367
+ ## Modifier Events
368
+
369
+ Modifiers can hook into the data lifecycle by implementing event handler methods. These methods are called during data conversion and database operations:
370
+
371
+ | Event | Triggered When |
372
+ | --------------- | ------------------------------------------ |
373
+ | `fromPlainData` | After converting plain data to an instance |
374
+ | `toPlainData` | After converting an instance to plain data |
375
+ | `fromDatabase` | After reading from the database |
376
+ | `toDatabase` | Before writing to the database |
377
+ | `insert` | Before inserting a new record |
378
+ | `update` | Before updating an existing record |
379
+
380
+ ```typescript
381
+ import {
382
+ Modifier,
383
+ MixinSymbol,
384
+ } from "@antelopejs/interface-database-decorators";
385
+
386
+ class TimestampModifier extends Modifier {
387
+ public insert(instance: any, field: string) {
388
+ if (field === "createdAt" && !instance[field]) {
389
+ instance[field] = new Date();
390
+ }
391
+ }
392
+
393
+ public update(instance: any, field: string) {
394
+ if (field === "updatedAt") {
395
+ instance[field] = new Date();
396
+ }
397
+ }
398
+
399
+ [MixinSymbol] = class {};
400
+ }
401
+ ```
402
+
403
+ 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.