@antelopejs/interface-database-decorators 0.1.4 → 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,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.4",
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
  ".": {
@@ -49,6 +51,10 @@
49
51
  "types": "./dist/table.d.ts",
50
52
  "default": "./dist/table.js"
51
53
  },
54
+ "./modifiers/autodate": {
55
+ "types": "./dist/modifiers/autodate.d.ts",
56
+ "default": "./dist/modifiers/autodate.js"
57
+ },
52
58
  "./modifiers/common": {
53
59
  "types": "./dist/modifiers/common.d.ts",
54
60
  "default": "./dist/modifiers/common.js"
@@ -125,6 +131,9 @@
125
131
  "table": [
126
132
  "dist/table.d.ts"
127
133
  ],
134
+ "modifiers/autodate": [
135
+ "dist/modifiers/autodate.d.ts"
136
+ ],
128
137
  "modifiers/common": [
129
138
  "dist/modifiers/common.d.ts"
130
139
  ],
@@ -141,6 +150,9 @@
141
150
  },
142
151
  "antelopeJs": {
143
152
  "standalone": true,
144
- "test": "src/antelope.test.ts"
153
+ "test": "src/antelope.test.ts",
154
+ "skills": [
155
+ "./skills"
156
+ ]
145
157
  }
146
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.