tshex-cli 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -140,1100 +140,29 @@ users/
140
140
  └── languages/
141
141
  ```
142
142
 
143
- ## Library structure
143
+ ## Documentation index
144
144
 
145
- The library is divided into a root, a shared directory, and one or more contexts. Each level has a role in the implementation.
145
+ From this point on, the guide is split into dedicated documents under `docs/`.
146
146
 
147
- ### Root
147
+ ### General
148
148
 
149
- The root contains `index.d.ts` and `main.ts`.
149
+ - [Library structure](https://github.com/virtualitems/tshex-cli/blob/main/docs/library-structure.md)
150
+ - [Library types](https://github.com/virtualitems/tshex-cli/blob/main/docs/library-types.md)
151
+ - [Context ports](https://github.com/virtualitems/tshex-cli/blob/main/docs/context-ports.md)
152
+ - [Generated file reference](https://github.com/virtualitems/tshex-cli/blob/main/docs/generated-file-reference.md)
150
153
 
151
- `index.d.ts` declares the types available to the library. `main.ts` starts as a placeholder for the main implementation and the public components that belong directly to that entry point.
154
+ ### Shared application
152
155
 
153
- ### Shared code
156
+ - [shared/application/data](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/data.md)
157
+ - [shared/application/events.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/events.md)
158
+ - [shared/application/http.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http.md)
159
+ - [shared/application/loggers.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/loggers.md)
160
+ - [shared/application/services.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/services.md)
161
+ - [shared/application/validations.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/validations.md)
154
162
 
155
- The `shared` directory contains code used by multiple contexts. It holds abstractions, interfaces, contracts, base types, and specific implementations with a common meaning across the library.
163
+ ### Shared domain
156
164
 
157
- ```text
158
- shared/
159
- ├── domain/
160
- └── application/
161
- ```
162
-
163
- `shared/domain` contains foundations for modeling business concepts. `shared/application` contains contracts for coordinating use cases, data sources, events, validations, HTTP responses, and logs.
164
-
165
- ### Contexts
166
-
167
- A context represents an application capability and groups its vocabulary, rules, and operations.
168
-
169
- ```text
170
- users/
171
- billing/
172
- sales/
173
- inventory/
174
- ```
175
-
176
- Separating by context organizes an application around its capabilities. Multiple contexts can be part of the same project, process, and data source. Each context preserves its own rules while sharing the general abstractions in `shared`.
177
-
178
- Each generated context contains three directories:
179
-
180
- ```text
181
- domain/
182
- application/
183
- adapters/
184
- ```
185
-
186
- #### `domain`
187
-
188
- Contains the context's capabilities. A capability groups business knowledge that can be used in different processes: validating an email address, identifying an entity, calculating a price, changing an order's status, or grouping the parts of a sale.
189
-
190
- Value objects, entities, and aggregates materialize these capabilities through data, rules, and behavior.
191
-
192
- #### `application`
193
-
194
- Contains the application of domain capabilities in processes that fulfill system purposes.
195
-
196
- An application service combines capabilities to complete an operation. For example, the process of registering a user can validate the email address, construct the entity, save its data, publish an event, and log the result.
197
-
198
- #### Context root
199
-
200
- The generated context root contains `example-ports.ts` as a starting point for the context's ports. You can keep that file, replace it, or add `index.ts` and other `.ts` files at the context root as the communication surface grows.
201
-
202
- A port describes a form of communication between the context and another system. It defines the received data, the returned data, and the operation available at that boundary.
203
-
204
- #### `adapters`
205
-
206
- Contains the integrations that connect the context with other systems. An adapter imports a port, implements the communication defined by that port, and connects an external input or output to an application process.
207
-
208
- An adapter can integrate an HTTP controller, a message consumer, an SDK, a driver, a remote client, an event bus, or a logging provider.
209
-
210
- ### Layered architecture
211
-
212
- The structure is organized into three levels: capabilities, processes, and communication.
213
-
214
- ```text
215
- External system
216
-
217
- Port + adapter
218
-
219
- Application
220
-
221
- Domain
222
- ```
223
-
224
- The domain occupies the inner layer and contains the context's capabilities.
225
-
226
- The application occupies the middle layer and uses those capabilities to build processes that fulfill system purposes.
227
-
228
- Ports and adapters occupy the outer layer and handle communication between systems.
229
-
230
- A port declares the communication available at the context boundary: what data enters, what data leaves, and which operation is exposed. Ports are declared in `.ts` modules located at the context root. Many projects centralize them in `index.ts`, but the generated template starts with `example-ports.ts`.
231
-
232
- An adapter implements that communication. It imports the corresponding port, translates external input or output into the application process format, and delegates the work to the application service.
233
-
234
- The import direction follows this path:
235
-
236
- ```text
237
- adapter → port
238
- adapter → application
239
- application → domain
240
- ```
241
-
242
- One possible arrangement is to keep `users/example-ports.ts` at the context root and replace the starter export with your own contracts as the context grows. In this guide, the examples stay close to the generated files: value objects extend `ValueObject`, entities extend `Entity`, services extend `Service`, repositories extend `Repository`, and ports live at the context root.
243
-
244
- > **Tip:** start the implementation inside the context and move a component to `shared` when its meaning and use belong to multiple contexts.
245
-
246
- ## Library types
247
-
248
- ### `index.d.ts`
249
-
250
- The file declares the generic type:
251
-
252
- ```ts
253
- type Generic<T = unknown> = Record<string, T>
254
- ```
255
-
256
- `Generic<T>` represents an object with `string` keys and values of a common type.
257
-
258
- ```ts
259
- const filters: Generic<string> = {
260
- status: 'active'
261
- }
262
- ```
263
-
264
- When the type is omitted, the values use `unknown`:
265
-
266
- ```ts
267
- const metadata: Generic = {
268
- retries: 2
269
- }
270
- ```
271
-
272
- Data source contracts use this form to represent plain objects whose specific structure will be defined by each implementation.
273
-
274
- ## Shared domain
275
-
276
- The domain starts with small concepts and progresses toward structures that combine multiple identities. We will begin with value objects, continue with entities, and finish with aggregates.
277
-
278
- ### Value objects
279
-
280
- A value object represents a concept with its own rules, semantics, or behavior. Its identity is determined by its value.
281
-
282
- The `shared/domain/value-objects.ts` file generates the `ValueObject<T>` base class and the `Email` and `NullableBoolean` implementations.
283
-
284
- #### Create an email address
285
-
286
- `Email` turns text into a domain concept with validation and its own operations:
287
-
288
- ```ts
289
- import { Email } from './core/shared/domain/value-objects.js'
290
-
291
- const email = Email.from('ana@example.com')
292
-
293
- email.value
294
- email.username
295
- email.domain
296
- ```
297
-
298
- Creation is performed with `Email.from()`. This method runs `Email.isValid()` before constructing the instance.
299
-
300
- ```ts
301
- Email.isValid('ana@example.com')
302
- ```
303
-
304
- Every creation follows the same validity rule:
305
-
306
- ```text
307
- received value
308
-
309
- isValid(value)
310
-
311
- from(value)
312
-
313
- valid instance
314
- ```
315
-
316
- When validation fails, `from()` throws `ValueError`.
317
-
318
- > **Tip:** use a native type when it fully expresses the data. Create a value object when the concept provides its own rules or operations. A text identifier can be represented with `string`; an email address benefits from `Email` because it includes validation and behavior.
319
-
320
- #### Represent a nullable Boolean state
321
-
322
- `NullableBoolean` models the values `true`, `false`, and `null`:
323
-
324
- ```ts
325
- import { NullableBoolean } from './core/shared/domain/value-objects.js'
326
-
327
- const active = NullableBoolean.from(null)
328
-
329
- active.value
330
- active.isIndeterminate()
331
- ```
332
-
333
- The `isIndeterminate()` method expresses an operation specific to the concept and allows the `null` state to be checked with explicit intent.
334
-
335
- #### Implement a value object
336
-
337
- We will create a user name. The example has one rule and one small operation so the focus stays on the value object itself.
338
-
339
- ```ts
340
- import { ValueError } from '../../shared/domain/errors.js'
341
- import { ValueObject } from '../../shared/domain/value-objects.js'
342
-
343
- export class UserName extends ValueObject<string> {
344
- public override readonly value: string
345
-
346
- protected constructor(value: string) {
347
- super()
348
- this.value = value
349
- }
350
-
351
- public override equals(
352
- other: UserName | null | undefined
353
- ): boolean {
354
- return other instanceof UserName &&
355
- this.value === other.value
356
- }
357
-
358
- public normalized(): string {
359
- return this.value.trim().toLowerCase()
360
- }
361
-
362
- public static override isValid(value: unknown): boolean {
363
- if (super.isValid(value) === false) {
364
- return false
365
- }
366
-
367
- return typeof value === 'string' &&
368
- value.trim().length > 0
369
- }
370
-
371
- public static from(value: string): UserName {
372
- if (this.isValid(value) === false) {
373
- throw new ValueError(value, this.name)
374
- }
375
-
376
- return new this(value)
377
- }
378
- }
379
- ```
380
-
381
- Now we can create and use the concept:
382
-
383
- ```ts
384
- const name = UserName.from('Ana')
385
- const normalized = name.normalized()
386
- ```
387
-
388
- `isValid()` centralizes the rule. `from()` creates the valid instance. `normalized()` adds behavior specific to the concept.
389
-
390
- ### Entities
391
-
392
- An entity represents a concept with its own identity. Two instances represent the same element when they share that identity.
393
-
394
- The `shared/domain/entities.ts` file generates the `Entity` base class. Each entity implements `equals()`. The inherited `toJSON()` method can be overridden when you want to return a plain object.
395
-
396
- This example creates an entity for the `users` context.
397
-
398
- ```ts
399
- import { Entity } from '../../shared/domain/entities.js'
400
- import {
401
- Email,
402
- NullableBoolean
403
- } from '../../shared/domain/value-objects.js'
404
-
405
- type UserName = {
406
- value: string
407
- }
408
-
409
- export class User extends Entity {
410
- public constructor(
411
- public readonly id: string,
412
- public readonly name: UserName,
413
- public readonly email: Email,
414
- public readonly active: NullableBoolean
415
- ) {
416
- super()
417
- }
418
-
419
- public override equals(other: Entity): boolean {
420
- return other instanceof User && other.id === this.id
421
- }
422
-
423
- public override toJSON(): Record<string, unknown> {
424
- return {
425
- id: this.id,
426
- name: this.name.value,
427
- email: this.email.value,
428
- active: this.active.value
429
- }
430
- }
431
- }
432
- ```
433
-
434
- The `id` property defines the identity. The `equals()` method compares entities using that property.
435
-
436
- `toJSON()` produces a plain representation of the entity:
437
-
438
- ```ts
439
- const user = new User(
440
- 'user-1',
441
- UserName.from('Ana'),
442
- Email.from('ana@example.com'),
443
- NullableBoolean.from(true)
444
- )
445
-
446
- const json = user.toJSON()
447
- ```
448
-
449
- The result uses the internal values of `UserName`, `Email`, and `NullableBoolean`.
450
-
451
- ### Aggregates
452
-
453
- An aggregate groups multiple entities into a logical unit. The aggregate's operations depend on all the identities that compose it.
454
-
455
- A user list can group several users:
456
-
457
- ```text
458
- UserList
459
- └── items
460
- ```
461
-
462
- Each element preserves its own identity within the unit.
463
-
464
- ```ts
465
- import { Aggregate } from '../../shared/domain/aggregates.js'
466
-
467
- type User = {
468
- id: string
469
- }
470
-
471
- export class UserList extends Aggregate {
472
- public constructor(public readonly items: User[]) {
473
- super()
474
- }
475
-
476
- public count(): number {
477
- return this.items.length
478
- }
479
- }
480
- ```
481
-
482
- `UserList` groups multiple `User` entities into a single logical unit. The `count()` method operates on that group.
483
-
484
- ### Domain errors
485
-
486
- The `shared/domain/errors.ts` file generates `ValueError`. This error represents a received value that does not satisfy the expected rule.
487
-
488
- ```ts
489
- import { ValueError } from '../../shared/domain/errors.js'
490
-
491
- if (userId.trim().length === 0) {
492
- throw new ValueError(userId, 'UserId')
493
- }
494
- ```
495
-
496
- Because it is in `shared`, `ValueError` can be used from any context and by any domain concept that validates values.
497
-
498
- ## Shared application
499
-
500
- The application layer coordinates use cases. Its contracts connect the domain with validations, data sources, HTTP responses, logs, and events.
501
-
502
- ### Validations
503
-
504
- The `shared/application/validations.ts` file declares the `Validatable` contract:
505
-
506
- ```ts
507
- isValid(): boolean
508
- ```
509
-
510
- `isValid()` checks whether the data is valid according to the rule defined by the implementation.
511
-
512
- We will create a validation for the use case that registers users.
513
-
514
- ```ts
515
- import type { Validatable } from '../../shared/application/validations.js'
516
- import { Email } from '../../shared/domain/value-objects.js'
517
-
518
- export type CreateUserValidationData = {
519
- name: string
520
- email: string
521
- }
522
-
523
- export class CreateUserValidation implements Validatable {
524
- public constructor(
525
- private readonly data: CreateUserValidationData
526
- ) {}
527
-
528
- public isValid(): boolean {
529
- return this.data.name.trim().length > 0 &&
530
- Email.isValid(this.data.email)
531
- }
532
- }
533
- ```
534
-
535
- In this example, the application service runs this validation before constructing the `User` entity.
536
-
537
- ### Services
538
-
539
- Services represent application use cases. Each service applies domain capabilities in a process that fulfills a purpose, such as creating a user, confirming an order, or recording a payment.
540
-
541
- The `shared/application/services.ts` file generates the `Service` base class. A concrete implementation defines its inputs, dependencies, and the method that executes the process.
542
-
543
- We will begin with a service that creates a user.
544
-
545
- ```ts
546
- import type { Validatable } from '../../shared/application/validations.js'
547
- import { Service } from '../../shared/application/services.js'
548
- import {
549
- Email,
550
- NullableBoolean
551
- } from '../../shared/domain/value-objects.js'
552
-
553
- export type CreateUserData = {
554
- id: string
555
- name: string
556
- email: string
557
- }
558
-
559
- export class UserName {
560
- public constructor(public readonly value: string) {}
561
-
562
- public static from(value: string): UserName {
563
- return new UserName(value.trim())
564
- }
565
- }
566
-
567
- export class User {
568
- public constructor(
569
- public readonly id: string,
570
- public readonly name: UserName,
571
- public readonly email: Email,
572
- public readonly active: NullableBoolean
573
- ) {}
574
- }
575
-
576
- export class CreateUserValidation implements Validatable {
577
- public constructor(
578
- private readonly data: CreateUserData
579
- ) {}
580
-
581
- public isValid(): boolean {
582
- return this.data.name.trim().length > 0 &&
583
- Email.isValid(this.data.email)
584
- }
585
- }
586
-
587
- export interface UserWriter {
588
- save(data: { user: User }): Promise<void>
589
- }
590
-
591
- export type CreateUserResult = {
592
- user: {
593
- id: string
594
- name: string
595
- email: string
596
- active: boolean | null
597
- }
598
- }
599
-
600
- export class CreateUserService extends Service {
601
- public constructor(private readonly users: UserWriter) {
602
- super()
603
- }
604
-
605
- public async execute(data: CreateUserData): Promise<CreateUserResult> {
606
- const validation = new CreateUserValidation(data)
607
-
608
- if (validation.isValid() === false) {
609
- throw new Error('The name or email is invalid.')
610
- }
611
-
612
- const user = new User(
613
- data.id,
614
- UserName.from(data.name),
615
- Email.from(data.email),
616
- NullableBoolean.from(true)
617
- )
618
-
619
- await this.users.save({ user })
620
-
621
- return {
622
- user: {
623
- id: user.id,
624
- name: user.name.value,
625
- email: user.email.value,
626
- active: user.active.value
627
- }
628
- }
629
- }
630
- }
631
- ```
632
-
633
- In this example, the service receives one object, validates it, constructs the entity, delegates persistence, and returns another object with the result.
634
-
635
- ### HTTP responses
636
-
637
- The `shared/application/http.ts` file generates lightweight HTTP contracts: `HttpRequest`, `HttpResponse`, `HttpResponseBody`, `HttpRequestHandler`, and `HttpMiddleware`.
638
-
639
- `HttpResponseBody` is an interface organized into three properties:
640
-
641
- ```ts
642
- import type { HttpResponseBody } from '../../shared/application/http.js'
643
-
644
- const body: HttpResponseBody = {
645
- data,
646
- errors,
647
- links
648
- }
649
- ```
650
-
651
- `data` contains the operation data and accepts `null` when the response has no data:
652
-
653
- ```ts
654
- const body: HttpResponseBody = {
655
- data: {
656
- id: 'user-1',
657
- name: 'Ana'
658
- },
659
- errors: null,
660
- links: null
661
- }
662
- ```
663
-
664
- `errors` contains a list of messages:
665
-
666
- ```ts
667
- const body: HttpResponseBody = {
668
- data: null,
669
- errors: ['The email is invalid.'],
670
- links: null
671
- }
672
- ```
673
-
674
- `links` contains HATEOAS links related to the resource and its available operations:
675
-
676
- ```ts
677
- const body: HttpResponseBody = {
678
- data: {
679
- id: 'user-1',
680
- name: 'Ana'
681
- },
682
- errors: null,
683
- links: {
684
- self: new URL('https://api.example.com/users/user-1')
685
- }
686
- }
687
- ```
688
-
689
- An entity's plain output can be used as `data`:
690
-
691
- ```ts
692
- const body: HttpResponseBody = {
693
- data: user.toJSON(),
694
- errors: null,
695
- links: null
696
- }
697
- ```
698
-
699
- `HttpRequestHandler` and `HttpMiddleware` define the contracts for adapters that receive a request, delegate to a handler, and produce a response.
700
-
701
- ### Logs
702
-
703
- The `shared/application/loggers.ts` file declares the `Logger` contract. The application uses this abstraction to produce logs that an adapter sends to external services.
704
-
705
- The contract includes the `debug`, `info`, `warning`, `error`, and `critical` levels, together with the numeric constants `DEBUG`, `INFO`, `WARNING`, `ERROR`, and `CRITICAL`.
706
-
707
- We will create a simple adapter that connects the contract to the console.
708
-
709
- ```ts
710
- import { Logger } from '../../shared/application/loggers.js'
711
-
712
- export class ConsoleLoggerAdapter extends Logger {
713
- public debug(data: unknown): void {
714
- console.debug(data)
715
- }
716
-
717
- public info(data: unknown): void {
718
- console.info(data)
719
- }
720
-
721
- public warning(data: unknown): void {
722
- console.warn(data)
723
- }
724
-
725
- public error(data: unknown): void {
726
- console.error(data)
727
- }
728
-
729
- public critical(data: unknown): void {
730
- console.error(data)
731
- }
732
- }
733
- ```
734
-
735
- In this example, the service receives `Logger` as a dependency. The adapter decides where to send each level.
736
-
737
- ### Events
738
-
739
- The `shared/application/events.ts` file contains the contracts that connect the application to an event bus.
740
-
741
- The flow starts with an event, continues through the dispatcher, and ends in one or more handlers:
742
-
743
- ```text
744
- Service
745
- ↓ creates
746
- Event
747
- ↓ passes to
748
- EventDispatcher
749
- ↓ publishes to
750
- Event bus
751
- ↓ executes
752
- EventHandler
753
- ```
754
-
755
- #### Event
756
-
757
- `Event` represents something that occurred in the application. It contains the event time and its plain details.
758
-
759
- ```ts
760
- import { Event } from '../../shared/application/events.js'
761
-
762
- export class UserCreated extends Event {
763
- public constructor(details: { userId: string }) {
764
- super(Date.now(), details)
765
- }
766
- }
767
- ```
768
-
769
- #### Handler
770
-
771
- `EventHandler` represents a reaction to the event.
772
-
773
- ```ts
774
- import {
775
- Event,
776
- EventHandler
777
- } from '../../shared/application/events.js'
778
- import { Logger } from '../../shared/application/loggers.js'
779
-
780
- export class LogUserCreated extends EventHandler {
781
- public constructor(private readonly logger: Logger) {
782
- super()
783
- }
784
-
785
- public async handle(event: Event): Promise<void> {
786
- this.logger.info(event.details)
787
- }
788
- }
789
- ```
790
-
791
- #### Dispatcher
792
-
793
- `EventDispatcher` represents the interaction contract with the event bus. Its concrete implementation subscribes handlers, removes subscriptions, and dispatches events.
794
-
795
- ```ts
796
- subscribe(key, handler)
797
- unsubscribe(key, handler)
798
- dispatch(event)
799
- ```
800
-
801
- In one possible implementation, the service can receive `EventDispatcher` and publish `UserCreated` after completing the use case.
802
-
803
- ## Data sources
804
-
805
- The `shared/application/data/` directory organizes access to a data source into three modules: `drivers.ts`, `managers.ts`, and `repositories.ts`. Together, they define `DriverAdapter`, `DataManager`, `DatasetManager`, and `Repository`.
806
-
807
- The complete flow looks like this:
808
-
809
- ```text
810
- DriverAdapter
811
- ↓ connects and enables
812
- DataManager or DatasetManager
813
- ↓ provides plain data to
814
- Repository
815
- ↓ transforms
816
- Domain objects
817
- ↓ used by
818
- Service
819
- ```
820
-
821
- We will begin with the connection and proceed to the use case.
822
-
823
- ### Driver adapter
824
-
825
- `DriverAdapter` connects and disconnects the source through a driver. When the connection is available, `connect()` returns an enabled data manager.
826
-
827
- ```ts
828
- connect(...args): Promise<DataManager>
829
- disconnect(): Promise<unknown>
830
- ```
831
-
832
- We will create an adapter for an in-memory collection of users. This example avoids a database so the focus stays on the adapter's responsibility, not on infrastructure details.
833
-
834
- ```ts
835
- import { DriverAdapter } from '../../shared/application/data/drivers.js'
836
- import { DataManager } from '../../shared/application/data/managers.js'
837
-
838
- type UserRecord = {
839
- id: string
840
- name: string
841
- email: string
842
- active: boolean | null
843
- }
844
-
845
- class UserDataManager extends DataManager<UserRecord> {
846
- public constructor(private readonly records: UserRecord[]) {
847
- super()
848
- }
849
-
850
- public async all(): Promise<UserRecord[]> {
851
- return this.records
852
- }
853
- }
854
-
855
- export class UserDriverAdapter
856
- extends DriverAdapter<UserDataManager> {
857
-
858
- public constructor(private readonly records: UserRecord[]) {
859
- super()
860
- }
861
-
862
- public async connect(): Promise<UserDataManager> {
863
- return new UserDataManager(this.records)
864
- }
865
-
866
- public async disconnect(): Promise<void> {
867
- }
868
- }
869
- ```
870
-
871
- The concrete implementation can encapsulate a database driver, an HTTP client, a file system, or another source.
872
-
873
- ### Data manager
874
-
875
- `DataManager` operates on the source and works with plain objects and arrays. Its base form exposes one abstract operation and one default operation:
876
-
877
- ```ts
878
- all(): Promise<Array<T>>
879
- none(): Array<T>
880
- ```
881
-
882
- `all()` retrieves the available records. `none()` already creates an empty typed collection.
883
-
884
- First, we will define the shape used by the source:
885
-
886
- ```ts
887
- type UserRecord = {
888
- id: string
889
- name: string
890
- email: string
891
- active: boolean | null
892
- }
893
- ```
894
-
895
- Now we will implement the data manager.
896
-
897
- ```ts
898
- import { DataManager } from '../../shared/application/data/managers.js'
899
-
900
- export type UserRecord = {
901
- id: string
902
- name: string
903
- email: string
904
- active: boolean | null
905
- }
906
-
907
- export class UserDataManager
908
- extends DataManager<UserRecord> {
909
-
910
- public constructor(private readonly records: UserRecord[]) {
911
- super()
912
- }
913
-
914
- public async all(): Promise<UserRecord[]> {
915
- return this.records
916
- }
917
- }
918
- ```
919
-
920
- The data manager reflects the structure of the source. In this example, it only exposes plain records.
921
-
922
- #### Source operations
923
-
924
- The file also declares interfaces that extend a data manager's capabilities:
925
-
926
- | Interface | Operation |
927
- | --- | --- |
928
- | `Filterable` | Filters records. |
929
- | `Sortable` | Sorts records. |
930
- | `Creatable` | Creates records. |
931
- | `Updatable` | Updates records. |
932
- | `Deletable` | Deletes records. |
933
- | `Aggregatable` | Performs aggregations. |
934
- | `Relatable` | Selects or preloads relationships. |
935
-
936
- A data manager can implement the interfaces required by its source:
937
-
938
- ```ts
939
- import {
940
- Creatable,
941
- DataManager,
942
- Filterable
943
- } from '../../shared/application/data/managers.js'
944
-
945
- export class UserDataManager
946
- extends DataManager<UserRecord>
947
- implements
948
- Filterable<Partial<UserRecord>>,
949
- Creatable<UserRecord> {
950
-
951
- public constructor(private readonly records: UserRecord[]) {
952
- super()
953
- }
954
-
955
- public async all(): Promise<UserRecord[]> {
956
- return this.records
957
- }
958
-
959
- public async filter(
960
- selector: Partial<UserRecord>
961
- ): Promise<UserRecord[]> {
962
- return this.records.filter((record) =>
963
- (selector.id === undefined || record.id === selector.id) &&
964
- (selector.email === undefined || record.email === selector.email)
965
- )
966
- }
967
-
968
- public async create(data: UserRecord): Promise<void> {
969
- this.records.push(data)
970
- }
971
- }
972
- ```
973
-
974
- It can also declare operations specific to source queries:
975
-
976
- ```ts
977
- public async findByEmail(query: { email: string }): Promise<UserRecord[]> {
978
- return this.filter({ email: query.email })
979
- }
980
- ```
981
-
982
- The application layer decides when to execute these operations, combines their results, and catches errors produced by the source.
983
-
984
- ### Dataset manager
985
-
986
- `DatasetManager` extends `DataManager` with set operations:
987
-
988
- ```ts
989
- union()
990
- intersection()
991
- difference()
992
- symmetricDifference()
993
- complement()
994
- ```
995
-
996
- This implementation is useful when an operation works with unions, intersections, differences, and complements between data collections.
997
-
998
- ### Repository
999
-
1000
- `Repository` acts as an intermediary between plain data and domain objects.
1001
-
1002
- ```text
1003
- UserRecord
1004
- ↓ transform
1005
- User
1006
- ```
1007
-
1008
- The generated base class focuses on reading plain data and transforming it into domain objects. Using the `UserDriverAdapter` from the previous example, a repository can stay very small.
1009
-
1010
- Now we will implement the repository.
1011
-
1012
- ```ts
1013
- import { DriverAdapter } from '../../shared/application/data/drivers.js'
1014
- import { DataManager } from '../../shared/application/data/managers.js'
1015
- import { Repository } from '../../shared/application/data/repositories.js'
1016
- import {
1017
- Email,
1018
- NullableBoolean
1019
- } from '../../shared/domain/value-objects.js'
1020
-
1021
- type UserRecord = {
1022
- id: string
1023
- name: string
1024
- email: string
1025
- active: boolean | null
1026
- }
1027
-
1028
- class UserName {
1029
- public constructor(public readonly value: string) {}
1030
-
1031
- public static from(value: string): UserName {
1032
- return new UserName(value)
1033
- }
1034
- }
1035
-
1036
- class User {
1037
- public constructor(
1038
- public readonly id: string,
1039
- public readonly name: UserName,
1040
- public readonly email: Email,
1041
- public readonly active: NullableBoolean
1042
- ) {}
1043
- }
1044
-
1045
- export class UserRepository
1046
- extends Repository<UserRecord, User> {
1047
-
1048
- public constructor(
1049
- driver: DriverAdapter<DataManager<UserRecord>>
1050
- ) {
1051
- super(driver)
1052
- }
1053
-
1054
- protected override transform(data: UserRecord): User {
1055
- return new User(
1056
- data.id,
1057
- UserName.from(data.name),
1058
- Email.from(data.email),
1059
- NullableBoolean.from(data.active)
1060
- )
1061
- }
1062
- }
1063
- ```
1064
-
1065
- Here, `transform()` converts each record into an entity. The constructor receives any driver compatible with `DataManager<UserRecord>`, and the inherited `all()` method already handles the connect, read, transform, and disconnect flow.
1066
-
1067
- ### Queries and errors in the application
1068
-
1069
- Application services coordinate queries and catch errors from data sources. The process expresses the collaboration capabilities it needs through application-specific contracts. Adapters materialize those contracts.
1070
-
1071
- ```ts
1072
- import { Service } from '../../shared/application/services.js'
1073
-
1074
- type User = {
1075
- id: string
1076
- }
1077
-
1078
- export interface UsersReader {
1079
- all(): Promise<User[]>
1080
- }
1081
-
1082
- export class ListUsersService extends Service {
1083
- public constructor(private readonly users: UsersReader) {
1084
- super()
1085
- }
1086
-
1087
- public async execute(): Promise<{ users: User[] }> {
1088
- try {
1089
- return {
1090
- users: await this.users.all()
1091
- }
1092
- } catch {
1093
- throw new Error('Could not list users.')
1094
- }
1095
- }
1096
- }
1097
- ```
1098
-
1099
- `UsersReader` expresses the collaboration required by the process. `UserRepository`, located in `adapters`, already satisfies that collaboration through the inherited `all()` method.
1100
-
1101
- ## Context ports
1102
-
1103
- Ports describe communication between the context and other systems. Each port defines the shape of an interaction at the boundary: input data, output data, and the available operation.
1104
-
1105
- The context generates `example-ports.ts` as a root-level starting point. As the context grows, you can add `index.ts` as the main port file or split ports across multiple `.ts` files. Adapters import the ports that define the communication they materialize.
1106
-
1107
- ### Main port
1108
-
1109
- The generated `example-ports.ts` file is only a placeholder:
1110
-
1111
- ```ts
1112
- export function example(): void {
1113
- // ...
1114
- }
1115
- ```
1116
-
1117
- You can replace it with your own root-level contract. A minimal option for creating a user is:
1118
-
1119
- ```ts
1120
- export type CreateUserRequest = {
1121
- user: {
1122
- id: string
1123
- name: string
1124
- email: string
1125
- }
1126
- }
1127
-
1128
- export type CreateUserResponse = {
1129
- user: {
1130
- id: string
1131
- name: string
1132
- email: string
1133
- active: boolean | null
1134
- }
1135
- }
1136
-
1137
- export interface CreateUserPort {
1138
- create(
1139
- request: CreateUserRequest
1140
- ): Promise<CreateUserResponse>
1141
- }
1142
- ```
1143
-
1144
- `CreateUserRequest` represents the information received from another system. `CreateUserResponse` represents the returned response. `CreateUserPort` defines the operation available at the context boundary.
1145
-
1146
- ### Adapt the port to the application process
1147
-
1148
- In this example, the adapter imports the port and the service. Its job here is to translate the root-level contract into the application input object and return the service result.
1149
-
1150
- ```ts
1151
- import type {
1152
- CreateUserPort,
1153
- CreateUserRequest,
1154
- CreateUserResponse
1155
- } from '../example-ports.js'
1156
-
1157
- type CreateUserService = {
1158
- execute(data: {
1159
- id: string
1160
- name: string
1161
- email: string
1162
- }): Promise<CreateUserResponse>
1163
- }
1164
-
1165
- export class CreateUserAdapter implements CreateUserPort {
1166
- public constructor(
1167
- private readonly service: CreateUserService
1168
- ) {}
1169
-
1170
- public async create(
1171
- request: CreateUserRequest
1172
- ): Promise<CreateUserResponse> {
1173
- return this.service.execute({
1174
- id: request.user.id,
1175
- name: request.user.name,
1176
- email: request.user.email
1177
- })
1178
- }
1179
- }
1180
- ```
1181
-
1182
- In this example, the port expresses the communication, the adapter implements it, the service executes the process, and the domain provides the capabilities used by that process.
1183
-
1184
- ### Additional ports
1185
-
1186
- A context can organize its communications across several files at the root. Each file declares the ports for a group of interactions.
1187
-
1188
- ```ts
1189
- export type ListUsersRequest = {
1190
- active: boolean | null
1191
- }
1192
-
1193
- export type ListUsersResponse = {
1194
- users: Array<{
1195
- id: string
1196
- name: string
1197
- email: string
1198
- active: boolean | null
1199
- }>
1200
- }
1201
-
1202
- export interface ListUsersPort {
1203
- list(request: ListUsersRequest): Promise<ListUsersResponse>
1204
- }
1205
- ```
1206
-
1207
- An adapter can import the contract from the file where it is declared:
1208
-
1209
- ```ts
1210
- import type {
1211
- ListUsersPort,
1212
- ListUsersResponse
1213
- } from '../example-ports.js'
1214
- ```
1215
-
1216
- > **Tip:** group ports that form a coherent communication in the same file. Use additional files when the context grows and groups of interactions with their own responsibilities emerge.
1217
-
1218
- ## Generated file reference
1219
-
1220
- | File | Purpose |
1221
- | --- | --- |
1222
- | `core/index.d.ts` | Declares `Generic<T>` for plain objects. |
1223
- | `core/main.ts` | Starts as a placeholder for the library's main implementation and exports. |
1224
- | `shared/domain/value-objects.ts` | Declares `ValueObject<T>` and implements `Email` and `NullableBoolean`. |
1225
- | `shared/domain/entities.ts` | Declares the `Entity` base class. |
1226
- | `shared/domain/aggregates.ts` | Declares the `Aggregate` base class. |
1227
- | `shared/domain/errors.ts` | Implements `ValueError`. |
1228
- | `shared/application/validations.ts` | Declares `Validatable`. |
1229
- | `shared/application/services.ts` | Declares `Service` as the base class for use cases. |
1230
- | `shared/application/http.ts` | Declares HTTP request, response, body, handler, and middleware contracts. |
1231
- | `shared/application/loggers.ts` | Declares log levels and the `Logger` contract. |
1232
- | `shared/application/events.ts` | Declares `Event`, `EventHandler`, and `EventDispatcher`. |
1233
- | `shared/application/data/drivers.ts` | Declares the `DriverAdapter` contract used to connect to a data source. |
1234
- | `shared/application/data/managers.ts` | Declares source operations together with `DataManager` and `DatasetManager`. |
1235
- | `shared/application/data/repositories.ts` | Declares the `Repository` base class for transforming records into domain objects. |
1236
- | `users/example-ports.ts` | Provides a root-level starter file with a placeholder export for your context ports. |
1237
- | `users/domain/` | Contains the context's capabilities. |
1238
- | `users/application/` | Contains processes that apply domain capabilities to fulfill purposes. |
1239
- | `users/adapters/` | Contains integrations that import ports and connect the context with other systems. |
165
+ - [shared/domain/aggregates.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/domain/aggregates.md)
166
+ - [shared/domain/entities.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/domain/entities.md)
167
+ - [shared/domain/errors.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/domain/errors.md)
168
+ - [shared/domain/value-objects.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/domain/value-objects.md)