@decaf-ts/core 0.8.26 → 0.8.27

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 (42) hide show
  1. package/README.md +184 -179
  2. package/dist/core.cjs +1 -1
  3. package/dist/core.cjs.map +1 -1
  4. package/dist/core.js +1 -1
  5. package/dist/core.js.map +1 -1
  6. package/lib/esm/index.d.ts +1 -1
  7. package/lib/esm/index.js +1 -1
  8. package/lib/esm/query/MethodQueryBuilder.d.ts +31 -3
  9. package/lib/esm/query/MethodQueryBuilder.js +153 -32
  10. package/lib/esm/query/MethodQueryBuilder.js.map +1 -1
  11. package/lib/esm/query/Statement.js +135 -7
  12. package/lib/esm/query/Statement.js.map +1 -1
  13. package/lib/esm/query/constants.d.ts +8 -1
  14. package/lib/esm/query/constants.js +7 -0
  15. package/lib/esm/query/constants.js.map +1 -1
  16. package/lib/esm/query/decorators.js +60 -6
  17. package/lib/esm/query/decorators.js.map +1 -1
  18. package/lib/esm/query/types.d.ts +21 -1
  19. package/lib/esm/query/types.js +8 -0
  20. package/lib/esm/query/types.js.map +1 -1
  21. package/lib/esm/repository/Repository.d.ts +56 -0
  22. package/lib/esm/repository/Repository.js +129 -0
  23. package/lib/esm/repository/Repository.js.map +1 -1
  24. package/lib/index.cjs +1 -1
  25. package/lib/index.d.ts +1 -1
  26. package/lib/query/MethodQueryBuilder.cjs +153 -32
  27. package/lib/query/MethodQueryBuilder.d.ts +31 -3
  28. package/lib/query/MethodQueryBuilder.js.map +1 -1
  29. package/lib/query/Statement.cjs +135 -7
  30. package/lib/query/Statement.js.map +1 -1
  31. package/lib/query/constants.cjs +7 -0
  32. package/lib/query/constants.d.ts +8 -1
  33. package/lib/query/constants.js.map +1 -1
  34. package/lib/query/decorators.cjs +60 -6
  35. package/lib/query/decorators.js.map +1 -1
  36. package/lib/query/types.cjs +8 -0
  37. package/lib/query/types.d.ts +21 -1
  38. package/lib/query/types.js.map +1 -1
  39. package/lib/repository/Repository.cjs +129 -0
  40. package/lib/repository/Repository.d.ts +56 -0
  41. package/lib/repository/Repository.js.map +1 -1
  42. package/package.json +1 -1
package/README.md CHANGED
@@ -6,6 +6,16 @@ Decaf Core provides the foundational building blocks for the Decaf TypeScript ec
6
6
 
7
7
  > Release docs refreshed on 2025-11-26. See [workdocs/reports/RELEASE_NOTES.md](./workdocs/reports/RELEASE_NOTES.md) for ticket summaries.
8
8
 
9
+ ### Core Concepts
10
+
11
+ * **`Repository`**: A class that implements the repository pattern, providing a consistent API for CRUD operations and querying.
12
+ * **`Adapter`**: An abstract class that defines the interface for connecting to different database backends.
13
+ * **`Statement`**: A query builder for creating complex database queries in a fluent, type-safe manner.
14
+ * **`TaskEngine`**: A system for managing background jobs and asynchronous operations.
15
+ * **`ModelService` and `PersistenceService`**: Base classes for creating services that encapsulate business logic and data access.
16
+ * **Migrations**: A system for managing database schema changes over time.
17
+ * **RAM Adapter**: An in-memory adapter for testing and development.
18
+
9
19
  ![Licence](https://img.shields.io/github/license/decaf-ts/core.svg?style=plastic)
10
20
  ![GitHub language count](https://img.shields.io/github/languages/count/decaf-ts/core?style=plastic)
11
21
  ![GitHub top language](https://img.shields.io/github/languages/top/decaf-ts/core?style=plastic)
@@ -29,7 +39,7 @@ Decaf Core provides the foundational building blocks for the Decaf TypeScript ec
29
39
 
30
40
  Documentation [here](https://decaf-ts.github.io/injectable-decorators/), Test results [here](https://decaf-ts.github.io/injectable-decorators/workdocs/reports/html/test-report.html) and Coverage [here](https://decaf-ts.github.io/injectable-decorators/workdocs/reports/coverage/lcov-report/index.html)
31
41
 
32
- Minimal size: 32 KB kb gzipped
42
+ Minimal size: 33.6 KB kb gzipped
33
43
 
34
44
 
35
45
  # Core Package — Detailed Description
@@ -133,235 +143,230 @@ Design intent
133
143
  - Enable DI and decorators for ergonomic repository wiring and testing
134
144
 
135
145
 
136
- # How To Use — Core Package
146
+ # How to Use
137
147
 
138
- Below are practical, focused examples for the public APIs exposed by the Core package. Each example includes a short description and valid TypeScript code. Examples are inspired by and aligned with the unit tests under core/tests.
148
+ This guide provides detailed, real-life examples of how to use the main features of the `@decaf-ts/core` library.
139
149
 
140
- Prerequisites used across examples:
141
- - Ensure your model builder is set for tests/dev scenarios: Model.setBuilder(Model.fromModel)
142
- - Use the RAM adapter for quick in-memory demos
143
- ```typescript
144
- import { Model, model } from "@decaf-ts/decorator-validation";
145
- import type { ModelArg } from "@decaf-ts/decorator-validation";
146
- import {
147
- Adapter,
148
- OrderDirection,
149
- Paginator,
150
- Repository,
151
- repository,
152
- uses,
153
- pk,
154
- column,
155
- table,
156
- } from "@decaf-ts/core";
157
- import { RamAdapter, RamRepository } from "@decaf-ts/core/ram";
158
-
159
- @table("tst_user")
160
- @model()
161
- class User extends Model {
162
- @pk() id!: string;
163
- @column("tst_name") name!: string;
164
- @column("tst_nif") nif!: string;
165
- constructor(arg?: ModelArg<User>) { super(arg); }
166
- }
150
+ ## Repository and Adapter Interaction
151
+
152
+ The `Repository` and `Adapter` are the core of the persistence layer. The `Repository` provides a high-level API for your application to interact with, while the `Adapter` handles the specific implementation details of your chosen database.
153
+
154
+ ### The `prepare` -> `action` -> `revert` Loop
155
+
156
+ This loop is the foundation of the persistence process. It ensures data is correctly transformed, validated, and persisted.
157
+
158
+ ```mermaid
159
+ sequenceDiagram
160
+ participant C as Client Code
161
+ participant R as Repository
162
+ participant V as Validators/Decorators
163
+ participant A as Adapter
164
+ participant DB as Database
165
+
166
+ C->>+R: create(model)
167
+ R->>R: 1. createPrefix(model)
168
+ R->>+V: 2. Enforce DB Decorators (ON)
169
+ V-->>-R:
170
+ R->>+A: 3. prepare(model)
171
+ A-->>-R: { record, id, transient }
172
+ R->>+A: 4. create(table, id, record)
173
+ A->>+DB: 5. Database Insert
174
+ DB-->>-A: Result
175
+ A-->>-R: record
176
+ R->>+A: 6. revert(record)
177
+ A-->>-R: model instance
178
+ R->>R: 7. createSuffix(model)
179
+ R->>+V: 8. Enforce DB Decorators (AFTER)
180
+ V-->>-R:
181
+ R-->>-C: created model
167
182
  ```
168
183
 
184
+ 1. **`createPrefix`**: The `Repository`'s `createPrefix` method is called. This is where you can add logic to be executed before the main `create` operation.
185
+ 2. **Decorators (ON)**: Any decorators configured to run `ON` the `CREATE` operation are executed. This is a good place for validation or data transformation.
186
+ 3. **`prepare`**: The `Adapter`'s `prepare` method is called to convert the model into a format suitable for the database. This includes separating transient properties.
187
+ 4. **`create`**: The `Adapter`'s `create` method is called to persist the data to the database.
188
+ 5. **Database Insert**: The `Adapter` communicates with the database to perform the insert operation.
189
+ 6. **`revert`**: The `Adapter`'s `revert` method is called to convert the database record back into a model instance.
190
+ 7. **`createSuffix`**: The `Repository`'s `createSuffix` method is called. This is where you can add logic to be executed after the main `create` operation.
191
+ 8. **Decorators (AFTER)**: Any decorators configured to run `AFTER` the `CREATE` operation are executed.
169
192
 
170
- - Repository + RAM adapter: basic CRUD
171
- Description: Create a RamAdapter and a Repository for a model and perform CRUD operations; mirrors core/tests/unit/RamAdapter.test.ts and adapter.test.ts.
172
- ```typescript
173
- import { NotFoundError } from "@decaf-ts/db-decorators";
193
+ ## Core Decorators
174
194
 
175
- async function crudExample() {
176
- const adapter = new RamAdapter();
177
- const repo: RamRepository<User> = new Repository(adapter, User);
195
+ The library provides a set of powerful decorators for defining models and their behavior.
178
196
 
179
- // CREATE
180
- const created = await repo.create(
181
- new User({ id: Date.now().toString(), name: "Alice", nif: "123456789" })
182
- );
197
+ * `@table(name)`: Specifies the database table name for a model.
198
+ * `@pk()`: Marks a property as the primary key.
199
+ * `@column(name)`: Maps a property to a database column with a different name.
200
+ * `@createdAt()`: Automatically sets the property to the current timestamp when a model is created.
201
+ * `@updatedAt()`: Automatically sets the property to the current timestamp when a model is created or updated.
202
+ * `@index()`: Creates a database index on a property.
183
203
 
184
- // READ
185
- const read = await repo.read(created.id);
186
- console.log(read.equals(created)); // true (same data, different instance)
204
+ ```typescript
205
+ import { table, pk, column, createdAt, updatedAt, index } from '@decaf-ts/core';
206
+ import { model, Model } from '@decaf-ts/decorator-validation';
207
+
208
+ @table('users')
209
+ @model()
210
+ export class User extends Model {
211
+ @pk()
212
+ id: string;
187
213
 
188
- // UPDATE
189
- const updated = await repo.update(Object.assign(read, {name: "Alice 2" }));
214
+ @column('user_name')
215
+ @index()
216
+ name: string;
190
217
 
191
- // DELETE
192
- const deleted = await repo.delete(created.id);
193
- console.log(deleted.equals(updated)); // true
218
+ @createdAt()
219
+ createdAt: Date;
220
+
221
+ @updatedAt()
222
+ updatedAt: Date;
194
223
  }
195
224
  ```
196
225
 
226
+ ## Complex Relations
227
+
228
+ You can model complex relationships between your classes using `@oneToOne`, `@oneToMany`, and `@manyToOne`.
197
229
 
198
- - Adapter current and registered models; @repository class decorator
199
- Description: Show how to set/get current adapter and register a repository via the @repository decorator; mirrors adapter.test.ts.
200
230
  ```typescript
231
+ import { table, pk, oneToOne, oneToMany, manyToOne } from '@decaf-ts/core';
232
+ import { model, Model } from '@decaf-ts/decorator-validation';
233
+ import { User } from './User';
201
234
 
235
+ @table('profiles')
202
236
  @model()
203
- class Managed extends Model { constructor(arg?: ModelArg<Managed>) { super(arg); } }
237
+ export class Profile extends Model {
238
+ @pk()
239
+ id: string;
204
240
 
205
- @repository(Managed)
206
- @uses("ram")
207
- class ManagedRepository extends Repository<Managed> {
208
- // Concrete adapter-backed methods would be provided by adapter implementation
209
- // For quick test or demo, use a RamAdapter
241
+ bio: string;
210
242
  }
211
243
 
212
- async function adapterRegistryExample() {
213
- const adapter = new RamAdapter();
244
+ @table('posts')
245
+ @model()
246
+ export class Post extends Model {
247
+ @pk()
248
+ id: string;
214
249
 
215
- Adapter.setCurrent("ram"); // set current flavour
216
- console.log(Adapter.current === Adapter.get("ram")); // true
250
+ title: string;
217
251
 
218
- // Models managed by current or specific adapter flavour
219
- const managed = Adapter.models("ram");
220
- console.log(Array.isArray(managed));
252
+ @manyToOne(() => User)
253
+ author: User;
221
254
  }
222
- ```
223
-
224
- - Query building with select/order and execution
225
- Description: Build a statement with orderBy and run it, as done in core/tests/unit/Pagination.test.ts.
226
255
 
227
- ```typescript
228
- async function queryExample() {
229
- const adapter = new RamAdapter();
230
- const repo: RamRepository<User> = new Repository(adapter, User);
231
-
232
- // Seed data
233
- await repo.createAll(
234
- Array.from({ length: 5 }).map((_, i) =>
235
- new User({ id: (i + 1).toString(), name: `u${i + 1}`, nif: "123456789" })
236
- )
237
- );
238
-
239
- const results = await repo
240
- .select()
241
- .orderBy(["id", OrderDirection.ASC])
242
- .execute();
243
-
244
- console.log(results.map((u) => u.id)); // ["1","2","3","4","5"]
245
- }
246
- ```
256
+ @table('users')
257
+ @model()
258
+ export class User extends Model {
259
+ @pk()
260
+ id: string;
247
261
 
248
- - Pagination with Paginator
249
- Description: Paginate query results using Statement.paginate(size), then page through results; mirrors Pagination.test.ts.
262
+ @oneToOne(() => Profile)
263
+ profile: Profile;
250
264
 
251
- ```typescript
252
- async function paginationExample() {
253
- const adapter = new RamAdapter();
254
- const repo: RamRepository<User> = new Repository(adapter, User);
255
-
256
- // Seed data
257
- const size = 25;
258
- await repo.createAll(
259
- Array.from({ length: size }).map((_, i) =>
260
- new User({ id: (i + 1).toString(), name: `u${i + 1}`, nif: "123456789" })
261
- )
262
- );
263
-
264
- const paginator: Paginator<User> = await repo
265
- .select()
266
- .orderBy(["id", OrderDirection.DSC])
267
- .paginate(10);
268
-
269
- const page1 = await paginator.page(); // first page by default
270
- const page2 = await paginator.next();
271
- const page3 = await paginator.next();
272
-
273
- console.log(page1.length, page2.length, page3.length); // 10, 10, 5
265
+ @oneToMany(() => Post)
266
+ posts: Post[];
274
267
  }
275
268
  ```
276
269
 
270
+ ## Extending the Adapter
277
271
 
278
- - Conditions: building filters
279
- Description: Compose conditions with the builder and apply them in a where clause.
280
- ```typescript
281
- import { Condition } from "@decaf-ts/core";
272
+ You can create your own persistence layer by extending the `Adapter` class.
282
273
 
283
- async function conditionExample() {
284
- const adapter = new RamAdapter();
285
- const repo: RamRepository<User> = new Repository(adapter, User);
274
+ ```typescript
275
+ import { Adapter, Model, Constructor, PrimaryKeyType } from '@decaf-ts/core';
286
276
 
287
- await repo.createAll([
288
- new User({ id: "1", name: "Alice", nif: "111111111" }),
289
- new User({ id: "2", name: "Bob", nif: "222222222" }),
290
- ]);
277
+ class MyCustomAdapter extends Adapter<any, any, any, any> {
278
+ constructor() {
279
+ super({}, 'my-custom-adapter');
280
+ }
291
281
 
292
- const cond = Condition.attr<User>("name")
293
- .eq("Alice")
294
- .build();
282
+ async create<M extends Model>(
283
+ clazz: Constructor<M>,
284
+ id: PrimaryKeyType,
285
+ model: Record<string, any>
286
+ ): Promise<Record<string, any>> {
287
+ console.log(`Creating in ${Model.tableName(clazz)} with id ${id}`);
288
+ // Your database insert logic here
289
+ return model;
290
+ }
295
291
 
296
- const results = await repo.select().where(cond).execute();
297
- console.log(results.length); // 1
292
+ // Implement other abstract methods: read, update, delete, raw
298
293
  }
299
294
  ```
300
295
 
296
+ ## Services
301
297
 
302
- - Adapter mapping: prepare and revert
303
- Description: Convert a model to a storage record and back using Adapter.prepare and Adapter.revert; mirrors adapter.test.ts.
304
- ```typescript
305
- async function mappingExample() {
306
- const adapter = new RamAdapter();
307
- const repo: RamRepository<User> = new Repository(adapter, User);
298
+ The `ModelService` provides a convenient way to interact with your repositories.
308
299
 
309
- const toCreate = new User({ id: "abc", name: "Test", nif: "123456789" });
300
+ ```typescript
301
+ import { ModelService, Repository } from '@decaf-ts/core';
302
+ import { User } from './models';
310
303
 
311
- // prepare: model -> record
312
- const pk = "id"; // infer with findPrimaryKey(toCreate).id if available
313
- const { record, id } = adapter.prepare(toCreate, pk);
314
- console.log(id === toCreate.id); // true
304
+ class UserService extends ModelService<User, Repository<User, any>> {
305
+ constructor() {
306
+ super(User);
307
+ }
315
308
 
316
- // revert: record -> model instance
317
- const model = adapter.revert(record, User, pk, id) as User;
318
- console.log(model instanceof User); // true
309
+ async findActiveUsers(): Promise<User[]> {
310
+ return this.repository.select().where({ status: 'active' }).execute();
311
+ }
319
312
  }
313
+
314
+ const userService = new UserService();
315
+ const activeUsers = await userService.findActiveUsers();
320
316
  ```
321
317
 
318
+ ## Task Engine
319
+
320
+ The `TaskEngine` is a powerful tool for managing background jobs.
321
+
322
+ ### Creating a Task Handler
323
+
324
+ A `TaskHandler` defines the logic for a specific task.
322
325
 
323
- - Auto-resolving repositories with InjectablesRegistry
324
- Description: Retrieve a repository by model name or constructor using the DI registry; see repository/injectables.ts flow.
325
326
  ```typescript
326
- import { Injectables } from "@decaf-ts/injectable-decorators";
327
- import { InjectablesRegistry } from "@decaf-ts/core";
328
-
329
- async function injectablesExample() {
330
- // Register current adapter so repositories can be created
331
- new RamAdapter();
332
- Adapter.setCurrent("ram");
333
-
334
- // Resolve by constructor
335
- const userRepo = Injectables.get<Repository<User>>(User);
336
- if (userRepo) {
337
- const u = await userRepo.create(
338
- new User({ id: "1", name: "A", nif: "123456789" })
339
- );
340
- console.log(!!u);
327
+ import { TaskHandler, TaskContext } from '@decaf-ts/core';
328
+
329
+ class MyTaskHandler implements TaskHandler<any, any> {
330
+ async run(input: any, context: TaskContext): Promise<any> {
331
+ console.log('Running my task with input:', input);
332
+ await context.progress({ message: 'Step 1 complete' });
333
+ // ... task logic
334
+ return { result: 'success' };
341
335
  }
342
336
  }
343
337
  ```
344
338
 
339
+ ### Using the Task Engine
340
+
341
+ ```typescript
342
+ import { TaskEngine, TaskModel, TaskHandlerRegistry } from '@decaf-ts/core';
343
+ import { MyTaskHandler } from './MyTaskHandler';
344
+
345
+ // 1. Register the handler
346
+ const registry = new TaskHandlerRegistry();
347
+ registry.register('my-task', new MyTaskHandler());
345
348
 
346
- ## Coding Principles
347
-
348
- - group similar functionality in folders (analog to namespaces but without any namespace declaration)
349
- - one class per file;
350
- - one interface per file (unless interface is just used as a type);
351
- - group types as other interfaces in a types.ts file per folder;
352
- - group constants or enums in a constants.ts file per folder;
353
- - group decorators in a decorators.ts file per folder;
354
- - always import from the specific file, never from a folder or index file (exceptions for dependencies on other packages);
355
- - prefer the usage of established design patters where applicable:
356
- - Singleton (can be an anti-pattern. use with care);
357
- - factory;
358
- - observer;
359
- - strategy;
360
- - builder;
361
- - etc;
362
-
363
- ## Release Documentation Hooks
364
- Stay aligned with the automated release pipeline by reviewing [Release Notes](./workdocs/reports/RELEASE_NOTES.md) and [Dependencies](./workdocs/reports/DEPENDENCIES.md) after trying these recipes (updated on 2025-11-26).
349
+ // 2. Create the task engine
350
+ const taskEngine = new TaskEngine({ adapter, registry });
351
+
352
+ // 3. Push a task
353
+ const task = new TaskModel({
354
+ classification: 'my-task',
355
+ input: { some: 'data' },
356
+ });
357
+ const { tracker } = await taskEngine.push(task, true);
358
+
359
+ // 4. Track the task's progress and result
360
+ tracker.on('progress', (payload) => {
361
+ console.log('Task progress:', payload);
362
+ });
363
+
364
+ const result = await tracker.resolve();
365
+ console.log('Task result:', result);
366
+
367
+ // 5. Schedule a task
368
+ taskEngine.schedule(task).for(new Date(Date.now() + 5000)); // 5 seconds from now
369
+ ```
365
370
 
366
371
 
367
372
  ### Related