onecore 0.6.5 → 0.6.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.
- package/README.md +1706 -111
- package/package.json +1 -1
- package/src/db.ts +2 -2
- package/src/index.ts +9 -5
- package/src/io.ts +5 -0
- package/src/mq.ts +7 -7
- package/src/service.ts +1 -1
package/README.md
CHANGED
|
@@ -16,14 +16,19 @@ npm install onecore
|
|
|
16
16
|
|
|
17
17
|
Large applications commonly depend on many infrastructure libraries:
|
|
18
18
|
|
|
19
|
-
* Database
|
|
20
|
-
* Validation
|
|
21
19
|
* Cache
|
|
22
|
-
* Message Queue
|
|
23
20
|
* Logging
|
|
24
21
|
* Health Check
|
|
25
|
-
*
|
|
22
|
+
* Validation
|
|
23
|
+
* Database
|
|
24
|
+
* DB
|
|
25
|
+
* Transaction
|
|
26
|
+
* Statement
|
|
27
|
+
* Repository
|
|
28
|
+
* Service
|
|
29
|
+
* Message Queue
|
|
26
30
|
* Import / Export
|
|
31
|
+
* HTTP clients
|
|
27
32
|
* Localization
|
|
28
33
|
|
|
29
34
|
Without a shared foundation, each library tends to define its own interfaces and metadata.
|
|
@@ -38,11 +43,11 @@ Without a shared foundation, each library tends to define its own interfaces and
|
|
|
38
43
|
┌─────────┴─────────┐
|
|
39
44
|
│ onecore │
|
|
40
45
|
│ │
|
|
41
|
-
│
|
|
42
|
-
│
|
|
43
|
-
│
|
|
44
|
-
│
|
|
45
|
-
│
|
|
46
|
+
│ Interfaces │
|
|
47
|
+
│ Attribute │
|
|
48
|
+
│ Transaction │
|
|
49
|
+
│ Repository │
|
|
50
|
+
│ Use Cases │
|
|
46
51
|
└─────────┬─────────┘
|
|
47
52
|
│
|
|
48
53
|
┌──────────┬──────────┼──────────┬──────────┐
|
|
@@ -52,20 +57,37 @@ Without a shared foundation, each library tends to define its own interfaces and
|
|
|
52
57
|
|
|
53
58
|
The implementation packages depend on these contracts, while application code can depend on the contracts rather than concrete infrastructure implementations.
|
|
54
59
|
|
|
60
|
+
```text
|
|
61
|
+
Application
|
|
62
|
+
│
|
|
63
|
+
▼
|
|
64
|
+
UseCase / Service
|
|
65
|
+
│
|
|
66
|
+
▼
|
|
67
|
+
Repository
|
|
68
|
+
│
|
|
69
|
+
▼
|
|
70
|
+
Executor
|
|
71
|
+
│
|
|
72
|
+
├── DB
|
|
73
|
+
└── Transaction
|
|
74
|
+
```
|
|
75
|
+
|
|
55
76
|
## Features
|
|
56
77
|
|
|
57
78
|
* Shared TypeScript interfaces for the Core TS ecosystem
|
|
58
|
-
* Unified `Attribute` metadata model
|
|
79
|
+
* Unified `Attribute` metadata model
|
|
80
|
+
* `ErrorMessage` for data validation
|
|
81
|
+
* Generic `Filter` and `SearchResult` models
|
|
59
82
|
* Database executor and transaction abstractions
|
|
60
|
-
*
|
|
83
|
+
* Type-safe database parameter abstraction
|
|
84
|
+
* Repository abstractions for CRUD and search
|
|
61
85
|
* Reusable CRUD and Search use-case classes
|
|
62
|
-
* Generic filtering and search result models
|
|
63
|
-
* Type-safe database parameter abstraction
|
|
64
|
-
* No framework dependency
|
|
65
|
-
* Interfaces with no runtime representation after TypeScript compilation
|
|
66
|
-
* Suitable for both backend and frontend projects
|
|
67
|
-
* Tree-shakeable runtime code
|
|
68
86
|
|
|
87
|
+
* Interfaces with no runtime representation after TypeScript compilation
|
|
88
|
+
* No framework dependency
|
|
89
|
+
* Suitable for both backend and frontend projects
|
|
90
|
+
* Tree-shakeable runtime code
|
|
69
91
|
## Unified metadata
|
|
70
92
|
|
|
71
93
|
The central concept in `onecore` is `Attribute`.
|
|
@@ -124,13 +146,13 @@ Individual libraries consume only the properties relevant to them.
|
|
|
124
146
|
│
|
|
125
147
|
┌───────────────┼────────────────┐
|
|
126
148
|
│ │ │
|
|
127
|
-
|
|
149
|
+
Validation Database Import/Export
|
|
128
150
|
│ │ │
|
|
129
|
-
validation-core sql-core
|
|
130
|
-
|
|
131
|
-
|
|
151
|
+
validation-core sql-core import-service
|
|
152
|
+
postgres-kit export-kit
|
|
153
|
+
mysql2-core
|
|
132
154
|
oracle-core
|
|
133
|
-
|
|
155
|
+
mssql-core
|
|
134
156
|
cassandra-core
|
|
135
157
|
mongodb-kit
|
|
136
158
|
```
|
|
@@ -222,9 +244,9 @@ export type FormatType =
|
|
|
222
244
|
export interface Executor {
|
|
223
245
|
driver: string
|
|
224
246
|
param(i: number): string
|
|
247
|
+
query<T>(sql: string, args?: any[], m?: StringMap, bools?: Attribute[]): Promise<T[]>
|
|
225
248
|
execute(sql: string, args?: any[]): Promise<number>
|
|
226
249
|
executeBatch(statements: Statement[], requireFirstAffected?: boolean): Promise<number>
|
|
227
|
-
query<T>(sql: string, args?: any[], m?: StringMap, bools?: Attribute[]): Promise<T[]>
|
|
228
250
|
}
|
|
229
251
|
```
|
|
230
252
|
|
|
@@ -301,7 +323,7 @@ The transaction abstraction is particularly useful when several repositories par
|
|
|
301
323
|
|
|
302
324
|
`onecore` defines generic repository contracts for CRUD and search operations.
|
|
303
325
|
|
|
304
|
-
###
|
|
326
|
+
### CRUDRepository
|
|
305
327
|
|
|
306
328
|
```ts
|
|
307
329
|
export interface CRUDRepository<T, ID> {
|
|
@@ -327,12 +349,12 @@ Transactional operation:
|
|
|
327
349
|
await repository.create(user, tx)
|
|
328
350
|
```
|
|
329
351
|
|
|
330
|
-
###
|
|
352
|
+
### SearchRepository
|
|
331
353
|
|
|
332
354
|
```ts
|
|
333
355
|
export interface SearchRepository<T, F extends Filter> {
|
|
334
356
|
search(
|
|
335
|
-
|
|
357
|
+
filter: F,
|
|
336
358
|
limit: number,
|
|
337
359
|
offset?: number | string,
|
|
338
360
|
fields?: string[],
|
|
@@ -365,7 +387,7 @@ export interface SearchResult<T> {
|
|
|
365
387
|
|
|
366
388
|
This allows implementations to support result counts as well as cursor-style pagination.
|
|
367
389
|
|
|
368
|
-
##
|
|
390
|
+
## Repository
|
|
369
391
|
|
|
370
392
|
CRUD and search repositories can be combined:
|
|
371
393
|
|
|
@@ -429,7 +451,7 @@ export class CRUDUseCase<T, ID> {
|
|
|
429
451
|
}
|
|
430
452
|
```
|
|
431
453
|
|
|
432
|
-
###
|
|
454
|
+
### UseCase
|
|
433
455
|
|
|
434
456
|
For applications that need both CRUD and search operations:
|
|
435
457
|
|
|
@@ -503,10 +525,7 @@ export interface ErrorMessage {
|
|
|
503
525
|
with:
|
|
504
526
|
|
|
505
527
|
```ts
|
|
506
|
-
export type Result<T> =
|
|
507
|
-
| number
|
|
508
|
-
| T
|
|
509
|
-
| ErrorMessage[]
|
|
528
|
+
export type Result<T> = number | T | ErrorMessage[]
|
|
510
529
|
```
|
|
511
530
|
|
|
512
531
|
This allows an application layer to return structured field-level errors without coupling the service interface to a particular validation library.
|
|
@@ -520,10 +539,11 @@ Examples include:
|
|
|
520
539
|
```text
|
|
521
540
|
Database
|
|
522
541
|
├── sql-core
|
|
523
|
-
├── mysql2-core
|
|
524
542
|
├── postgres-kit
|
|
543
|
+
├── mysql2-core
|
|
525
544
|
├── oracle-core
|
|
526
545
|
├── mssql-core
|
|
546
|
+
├── cassandra-core
|
|
527
547
|
└── mongodb-kit
|
|
528
548
|
|
|
529
549
|
Validation
|
|
@@ -535,6 +555,7 @@ Messaging
|
|
|
535
555
|
├── redis-messaging
|
|
536
556
|
├── rabbitmq-transport
|
|
537
557
|
├── activemq
|
|
558
|
+
├── kafka-plus
|
|
538
559
|
└── ibmmq-plus
|
|
539
560
|
|
|
540
561
|
Cache
|
|
@@ -542,12 +563,12 @@ Cache
|
|
|
542
563
|
└── redis-plus
|
|
543
564
|
|
|
544
565
|
Other
|
|
566
|
+
├── import-service
|
|
567
|
+
├── export-kit
|
|
545
568
|
├── logger-core
|
|
546
569
|
├── health-service
|
|
547
|
-
├── web-clients
|
|
548
570
|
├── locale-service
|
|
549
|
-
|
|
550
|
-
└── export-kit
|
|
571
|
+
└── web-clients
|
|
551
572
|
```
|
|
552
573
|
|
|
553
574
|
These packages can share the same contracts and metadata model while remaining independently implemented.
|
|
@@ -558,12 +579,12 @@ These packages can share the same contracts and metadata model while remaining i
|
|
|
558
579
|
`onecore` defines the common contracts used by the ecosystem.
|
|
559
580
|
|
|
560
581
|
### Database
|
|
582
|
+
* Statement
|
|
583
|
+
* DB
|
|
584
|
+
* Transaction
|
|
561
585
|
* SearchRepository
|
|
562
586
|
* CRUDRepository
|
|
563
587
|
* Repository
|
|
564
|
-
* Transaction
|
|
565
|
-
* Statement
|
|
566
|
-
* QueryBuilder
|
|
567
588
|
|
|
568
589
|
Implemented by
|
|
569
590
|
|
|
@@ -575,8 +596,6 @@ Implemented by
|
|
|
575
596
|
* cassandra-core
|
|
576
597
|
* mongodb-kit
|
|
577
598
|
|
|
578
|
-
---
|
|
579
|
-
|
|
580
599
|
### Validation
|
|
581
600
|
|
|
582
601
|
* Validator<T>
|
|
@@ -585,8 +604,6 @@ Implemented by
|
|
|
585
604
|
|
|
586
605
|
* validation-core
|
|
587
606
|
|
|
588
|
-
---
|
|
589
|
-
|
|
590
607
|
### Cache
|
|
591
608
|
|
|
592
609
|
* CachePort
|
|
@@ -596,8 +613,6 @@ Implemented by
|
|
|
596
613
|
* cache-plus
|
|
597
614
|
* redis-plus
|
|
598
615
|
|
|
599
|
-
---
|
|
600
|
-
|
|
601
616
|
### Message Queue
|
|
602
617
|
|
|
603
618
|
* Producer
|
|
@@ -611,9 +626,9 @@ Implemented by
|
|
|
611
626
|
* redis-messaging
|
|
612
627
|
* nats-plus
|
|
613
628
|
* rabbitmq-transport
|
|
614
|
-
*
|
|
615
|
-
|
|
616
|
-
|
|
629
|
+
* activemq
|
|
630
|
+
* kafka-plus
|
|
631
|
+
* ibmmq-plus
|
|
617
632
|
|
|
618
633
|
### Logging
|
|
619
634
|
|
|
@@ -623,8 +638,6 @@ Implemented by
|
|
|
623
638
|
|
|
624
639
|
* logger-core
|
|
625
640
|
|
|
626
|
-
---
|
|
627
|
-
|
|
628
641
|
### Health Check
|
|
629
642
|
|
|
630
643
|
* HealthChecker
|
|
@@ -633,8 +646,6 @@ Implemented by
|
|
|
633
646
|
|
|
634
647
|
* health-service
|
|
635
648
|
|
|
636
|
-
---
|
|
637
|
-
|
|
638
649
|
### HTTP
|
|
639
650
|
|
|
640
651
|
* HttpRequest
|
|
@@ -644,8 +655,6 @@ Implemented by
|
|
|
644
655
|
|
|
645
656
|
* web-clients
|
|
646
657
|
|
|
647
|
-
---
|
|
648
|
-
|
|
649
658
|
### Localization
|
|
650
659
|
|
|
651
660
|
* Locale
|
|
@@ -674,17 +683,34 @@ Reusable classes such as `UseCase` are only included when they are actually impo
|
|
|
674
683
|
|
|
675
684
|
This makes the package suitable for both backend and frontend projects.
|
|
676
685
|
|
|
677
|
-
## Backend
|
|
686
|
+
## Backend Support
|
|
678
687
|
|
|
679
|
-
|
|
688
|
+
Infrastructure services often depend only on interfaces.
|
|
689
|
+
|
|
690
|
+
Examples include
|
|
680
691
|
|
|
681
|
-
* Repository contracts
|
|
682
|
-
* Database transactions
|
|
683
692
|
* Shared metadata
|
|
693
|
+
* Database transactions
|
|
694
|
+
* Repository contracts
|
|
684
695
|
* Application use cases
|
|
685
|
-
*
|
|
696
|
+
* Message Queue services
|
|
697
|
+
* Import services / Export services
|
|
698
|
+
* Infrastructure interfaces: Logging, Cache, Validation...
|
|
699
|
+
|
|
700
|
+
Since these projects do not use application-layer base classes, the generated JavaScript contains virtually no runtime code from `onecore`.
|
|
701
|
+
|
|
702
|
+
## Frontend Support
|
|
703
|
+
|
|
704
|
+
`onecore` works in frontend applications.
|
|
705
|
+
|
|
706
|
+
React applications commonly use
|
|
707
|
+
|
|
708
|
+
* Attribute
|
|
709
|
+
* Locale
|
|
710
|
+
* Currency
|
|
711
|
+
* Validator interfaces
|
|
686
712
|
|
|
687
|
-
|
|
713
|
+
without importing any backend service classes.
|
|
688
714
|
|
|
689
715
|
For example:
|
|
690
716
|
|
|
@@ -698,6 +724,8 @@ import type {
|
|
|
698
724
|
|
|
699
725
|
This allows a frontend and backend to share the same domain metadata and contracts.
|
|
700
726
|
|
|
727
|
+
The final JavaScript bundle remains minimal because interfaces are removed during compilation and unused code is tree-shaken.
|
|
728
|
+
|
|
701
729
|
## Design principles
|
|
702
730
|
|
|
703
731
|
### One common contract
|
|
@@ -720,51 +748,16 @@ Transactions are passed explicitly to repository operations when multiple operat
|
|
|
720
748
|
|
|
721
749
|
Keep shared contracts in TypeScript interfaces and keep runtime implementations small and reusable.
|
|
722
750
|
|
|
723
|
-
## License
|
|
724
|
-
|
|
725
|
-
MIT
|
|
726
|
-
|
|
727
|
-
# Frontend Support
|
|
728
|
-
|
|
729
|
-
`onecore` works in frontend applications.
|
|
730
|
-
|
|
731
|
-
React applications commonly use
|
|
732
|
-
|
|
733
|
-
* Attribute
|
|
734
|
-
* Locale
|
|
735
|
-
* Currency
|
|
736
|
-
* Validator interfaces
|
|
737
|
-
|
|
738
|
-
without importing any backend service classes.
|
|
739
|
-
|
|
740
|
-
The final JavaScript bundle remains minimal because interfaces are removed during compilation and unused code is tree-shaken.
|
|
741
|
-
|
|
742
|
-
---
|
|
743
|
-
|
|
744
|
-
# Backend Support
|
|
745
|
-
|
|
746
|
-
Infrastructure services often depend only on interfaces.
|
|
747
|
-
|
|
748
|
-
Examples include
|
|
749
|
-
|
|
750
|
-
* Message Queue services
|
|
751
|
-
* Import services
|
|
752
|
-
* Export services
|
|
753
|
-
* Background workers
|
|
754
|
-
|
|
755
|
-
Since these projects do not use application-layer base classes, the generated JavaScript contains virtually no runtime code from `onecore`.
|
|
756
|
-
|
|
757
|
-
---
|
|
758
751
|
|
|
759
|
-
|
|
752
|
+
## Sample Projects
|
|
760
753
|
|
|
761
754
|
The following sample applications demonstrate different ways of using `onecore`.
|
|
762
755
|
|
|
763
756
|
### CRUD Applications
|
|
764
757
|
|
|
765
|
-
* [
|
|
766
|
-
* [
|
|
767
|
-
* [
|
|
758
|
+
* [sql-modular-sample](https://github.com/source-code-template/sql-modular-sample) — SQL modular microservice using MySQL
|
|
759
|
+
* [sql-simple-modular-sample](https://github.com/source-code-template/sql-simple-modular-sample) — SQL modular microservice using PosgreSQL
|
|
760
|
+
* [mongo-simple-modular-sample](https://github.com/source-code-template/mongo-simple-modular-sample) — MongoDB modular microservice
|
|
768
761
|
|
|
769
762
|
These projects extend the reusable CRUD and Search use-case classes.
|
|
770
763
|
|
|
@@ -794,21 +787,1623 @@ These projects depend only on interfaces and the shared `Attribute` metadata mod
|
|
|
794
787
|
|
|
795
788
|
These projects depend only on interfaces and the shared `Attribute` metadata model.
|
|
796
789
|
|
|
797
|
-
|
|
790
|
+
## License
|
|
798
791
|
|
|
799
|
-
|
|
792
|
+
MIT
|
|
800
793
|
|
|
801
|
-
* Interface-first architecture
|
|
802
|
-
* Dependency inversion
|
|
803
|
-
* Clean Architecture
|
|
804
|
-
* Hexagonal Architecture
|
|
805
|
-
* Tree-shakeable design
|
|
806
|
-
* Shared metadata model
|
|
807
|
-
* Framework independent
|
|
808
|
-
* Enterprise ready
|
|
809
794
|
|
|
810
|
-
|
|
795
|
+
# TypeScript Repository & Service Abstraction
|
|
796
|
+
|
|
797
|
+
A lightweight TypeScript abstraction for building database-backed applications using repositories, services/use cases, transactions, and metadata-driven attributes.
|
|
798
|
+
|
|
799
|
+
The library separates database execution from persistence and application-facing operations while keeping the API generic across database implementations.
|
|
800
|
+
|
|
801
|
+
## Architecture
|
|
802
|
+
|
|
803
|
+
```text
|
|
804
|
+
┌──────────────────────────────┐
|
|
805
|
+
│ Service / UseCase │
|
|
806
|
+
│ SearchUseCase │
|
|
807
|
+
│ CRUDUseCase │
|
|
808
|
+
│ UseCase │
|
|
809
|
+
└──────────────┬───────────────┘
|
|
810
|
+
│
|
|
811
|
+
▼
|
|
812
|
+
┌──────────────────────────────┐
|
|
813
|
+
│ Repository │
|
|
814
|
+
│ SearchRepository │
|
|
815
|
+
│ CRUDRepository │
|
|
816
|
+
│ Repository │
|
|
817
|
+
└──────────────┬───────────────┘
|
|
818
|
+
│
|
|
819
|
+
▼
|
|
820
|
+
┌──────────────────────────────┐
|
|
821
|
+
│ Database Layer │
|
|
822
|
+
│ Executor │
|
|
823
|
+
│ Transaction │
|
|
824
|
+
│ DB │
|
|
825
|
+
└──────────────┬───────────────┘
|
|
826
|
+
│
|
|
827
|
+
▼
|
|
828
|
+
Database Driver
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
## Core Concepts
|
|
832
|
+
|
|
833
|
+
### Attribute metadata
|
|
834
|
+
|
|
835
|
+
`Attribute` describes how an application field should be mapped, validated, searched, formatted, or persisted.
|
|
836
|
+
|
|
837
|
+
```ts
|
|
838
|
+
const attributes: Attributes = {
|
|
839
|
+
id: {
|
|
840
|
+
field: 'id',
|
|
841
|
+
column: 'user_id',
|
|
842
|
+
type: 'ObjectId',
|
|
843
|
+
key: true,
|
|
844
|
+
},
|
|
845
|
+
|
|
846
|
+
email: {
|
|
847
|
+
field: 'email',
|
|
848
|
+
column: 'email_address',
|
|
849
|
+
type: 'string',
|
|
850
|
+
format: 'email',
|
|
851
|
+
required: true,
|
|
852
|
+
unique: true,
|
|
853
|
+
},
|
|
854
|
+
|
|
855
|
+
age: {
|
|
856
|
+
field: 'age',
|
|
857
|
+
type: 'integer',
|
|
858
|
+
min: 0,
|
|
859
|
+
max: 150,
|
|
860
|
+
},
|
|
861
|
+
};
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
Supported data types include:
|
|
865
|
+
|
|
866
|
+
```ts
|
|
867
|
+
type DataType =
|
|
868
|
+
| 'ObjectId'
|
|
869
|
+
| 'date'
|
|
870
|
+
| 'datetime'
|
|
871
|
+
| 'time'
|
|
872
|
+
| 'boolean'
|
|
873
|
+
| 'number'
|
|
874
|
+
| 'integer'
|
|
875
|
+
| 'string'
|
|
876
|
+
| 'text'
|
|
877
|
+
| 'object'
|
|
878
|
+
| 'array'
|
|
879
|
+
| 'binary'
|
|
880
|
+
| 'primitives'
|
|
881
|
+
| 'booleans'
|
|
882
|
+
| 'numbers'
|
|
883
|
+
| 'integers'
|
|
884
|
+
| 'strings'
|
|
885
|
+
| 'dates'
|
|
886
|
+
| 'datetimes'
|
|
887
|
+
| 'times';
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
Supported formats include:
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
type FormatType =
|
|
894
|
+
| 'currency'
|
|
895
|
+
| 'percentage'
|
|
896
|
+
| 'email'
|
|
897
|
+
| 'url'
|
|
898
|
+
| 'phone'
|
|
899
|
+
| 'fax'
|
|
900
|
+
| 'ipv4'
|
|
901
|
+
| 'ipv6';
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
## Database Executor
|
|
905
|
+
|
|
906
|
+
`Executor` is the low-level database abstraction.
|
|
907
|
+
|
|
908
|
+
```ts
|
|
909
|
+
export interface Executor {
|
|
910
|
+
driver: string;
|
|
911
|
+
|
|
912
|
+
param(i: number): string;
|
|
913
|
+
|
|
914
|
+
execute(
|
|
915
|
+
sql: string,
|
|
916
|
+
args?: any[]
|
|
917
|
+
): Promise<number>;
|
|
918
|
+
|
|
919
|
+
executeBatch(
|
|
920
|
+
statements: Statement[],
|
|
921
|
+
firstSuccess?: boolean
|
|
922
|
+
): Promise<number>;
|
|
923
|
+
|
|
924
|
+
query<T>(
|
|
925
|
+
sql: string,
|
|
926
|
+
args?: any[],
|
|
927
|
+
m?: StringMap,
|
|
928
|
+
bools?: Attribute[]
|
|
929
|
+
): Promise<T[]>;
|
|
930
|
+
}
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
The application can therefore depend on `Executor` rather than directly depending on a specific database driver.
|
|
934
|
+
|
|
935
|
+
### Statements
|
|
936
|
+
|
|
937
|
+
A batch statement is represented by:
|
|
938
|
+
|
|
939
|
+
```ts
|
|
940
|
+
export interface Statement {
|
|
941
|
+
query: string;
|
|
942
|
+
params?: any[];
|
|
943
|
+
}
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
Example:
|
|
947
|
+
|
|
948
|
+
```ts
|
|
949
|
+
const statements: Statement[] = [
|
|
950
|
+
{
|
|
951
|
+
query: 'UPDATE users SET active = ? WHERE id = ?',
|
|
952
|
+
params: [true, 1],
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
query: 'INSERT INTO audit_log(action) VALUES (?)',
|
|
956
|
+
params: ['activate-user'],
|
|
957
|
+
},
|
|
958
|
+
];
|
|
959
|
+
|
|
960
|
+
await executor.executeBatch(statements);
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
## Transactions
|
|
964
|
+
|
|
965
|
+
A transaction extends `Executor`:
|
|
966
|
+
|
|
967
|
+
```ts
|
|
968
|
+
export interface Transaction extends Executor {
|
|
969
|
+
commit(): Promise<void>;
|
|
970
|
+
rollback(): Promise<void>;
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
Transactions can be passed explicitly to repository operations.
|
|
975
|
+
|
|
976
|
+
```ts
|
|
977
|
+
const tx = await db.beginTransaction();
|
|
978
|
+
|
|
979
|
+
try {
|
|
980
|
+
await userRepository.create(user, tx);
|
|
981
|
+
await auditRepository.create(audit, tx);
|
|
982
|
+
|
|
983
|
+
await tx.commit();
|
|
984
|
+
} catch (error) {
|
|
985
|
+
await tx.rollback();
|
|
986
|
+
throw error;
|
|
987
|
+
}
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
### Transaction ownership
|
|
991
|
+
|
|
992
|
+
The recommended ownership model is:
|
|
993
|
+
|
|
994
|
+
> The caller that creates the transaction owns the transaction and is responsible for committing or rolling it back.
|
|
995
|
+
|
|
996
|
+
A repository should not commit or roll back a transaction supplied by its caller.
|
|
997
|
+
|
|
998
|
+
## Repository Layer
|
|
999
|
+
|
|
1000
|
+
### CRUD repository
|
|
1001
|
+
|
|
1002
|
+
```ts
|
|
1003
|
+
export interface CRUDRepository<T, ID> {
|
|
1004
|
+
load(id: ID, tx?: Transaction): Promise<T | null>;
|
|
1005
|
+
create(obj: T, tx?: Transaction): Promise<number>;
|
|
1006
|
+
update(obj: T, tx?: Transaction): Promise<number>;
|
|
1007
|
+
patch(obj: Partial<T>, tx?: Transaction): Promise<number>;
|
|
1008
|
+
delete(id: ID, tx?: Transaction): Promise<number>;
|
|
1009
|
+
}
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
Typical usage:
|
|
1013
|
+
|
|
1014
|
+
```ts
|
|
1015
|
+
const user = await repository.load(userId);
|
|
1016
|
+
|
|
1017
|
+
if (!user) {
|
|
1018
|
+
// not found
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
await repository.create(user);
|
|
1022
|
+
|
|
1023
|
+
await repository.update(user);
|
|
1024
|
+
|
|
1025
|
+
await repository.patch({
|
|
1026
|
+
id: user.id,
|
|
1027
|
+
name: 'Updated name',
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
await repository.delete(user.id);
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
The optional `Transaction` allows multiple repository operations to participate in the same transaction.
|
|
1034
|
+
|
|
1035
|
+
## Search Repository
|
|
1036
|
+
|
|
1037
|
+
Search is separated from CRUD:
|
|
1038
|
+
|
|
1039
|
+
```ts
|
|
1040
|
+
export interface SearchRepository<T, F extends Filter> {
|
|
1041
|
+
search(
|
|
1042
|
+
s: F,
|
|
1043
|
+
limit: number,
|
|
1044
|
+
offset?: number | string,
|
|
1045
|
+
fields?: string[],
|
|
1046
|
+
tx?: Transaction
|
|
1047
|
+
): Promise<SearchResult<T>>;
|
|
1048
|
+
}
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
A typical filter contains:
|
|
1052
|
+
|
|
1053
|
+
```ts
|
|
1054
|
+
export interface Filter {
|
|
1055
|
+
page?: number;
|
|
1056
|
+
limit: number;
|
|
1057
|
+
fields?: string[];
|
|
1058
|
+
sort?: string;
|
|
1059
|
+
q?: string;
|
|
1060
|
+
}
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
Search results are represented by:
|
|
1064
|
+
|
|
1065
|
+
```ts
|
|
1066
|
+
export interface SearchResult<T> {
|
|
1067
|
+
list: T[];
|
|
1068
|
+
total?: number;
|
|
1069
|
+
next?: string;
|
|
1070
|
+
}
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
Example:
|
|
1074
|
+
|
|
1075
|
+
```ts
|
|
1076
|
+
const result = await userRepository.search(
|
|
1077
|
+
{
|
|
1078
|
+
q: 'john',
|
|
1079
|
+
limit: 20,
|
|
1080
|
+
page: 1,
|
|
1081
|
+
sort: 'name',
|
|
1082
|
+
},
|
|
1083
|
+
20,
|
|
1084
|
+
);
|
|
1085
|
+
|
|
1086
|
+
console.log(result.list);
|
|
1087
|
+
console.log(result.total);
|
|
1088
|
+
console.log(result.next);
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
## Combined Repository
|
|
1092
|
+
|
|
1093
|
+
For entities that support both CRUD and search:
|
|
1094
|
+
|
|
1095
|
+
```ts
|
|
1096
|
+
export interface Repository<T, ID, F extends Filter>
|
|
1097
|
+
extends CRUDRepository<T, ID>,
|
|
1098
|
+
SearchRepository<T, F> {
|
|
1099
|
+
}
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
Example:
|
|
1103
|
+
|
|
1104
|
+
```ts
|
|
1105
|
+
interface UserFilter extends Filter {
|
|
1106
|
+
active?: boolean;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
interface User {
|
|
1110
|
+
id: string;
|
|
1111
|
+
name: string;
|
|
1112
|
+
email: string;
|
|
1113
|
+
active: boolean;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
class UserRepository
|
|
1117
|
+
implements Repository<User, string, UserFilter> {
|
|
1118
|
+
|
|
1119
|
+
// implementation
|
|
1120
|
+
}
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
## Service / Use Case Layer
|
|
1124
|
+
|
|
1125
|
+
The service layer exposes repository operations without exposing transactions or database details to the caller.
|
|
1126
|
+
|
|
1127
|
+
### CRUD service
|
|
1128
|
+
|
|
1129
|
+
```ts
|
|
1130
|
+
export interface CRUDService<T, ID> {
|
|
1131
|
+
load(id: ID): Promise<T | null>;
|
|
1132
|
+
create(obj: T): Promise<Result<T>>;
|
|
1133
|
+
update(obj: T): Promise<Result<T>>;
|
|
1134
|
+
patch(obj: Partial<T>): Promise<Result<T>>;
|
|
1135
|
+
delete(id: ID): Promise<number>;
|
|
1136
|
+
}
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
### Search service
|
|
1140
|
+
|
|
1141
|
+
```ts
|
|
1142
|
+
export interface SearchService<T, F extends Filter> {
|
|
1143
|
+
search(
|
|
1144
|
+
s: F,
|
|
1145
|
+
limit: number,
|
|
1146
|
+
page?: number | string,
|
|
1147
|
+
fields?: string[]
|
|
1148
|
+
): Promise<SearchResult<T>>;
|
|
1149
|
+
}
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
### Combined service
|
|
1153
|
+
|
|
1154
|
+
```ts
|
|
1155
|
+
export interface Service<T, ID, F extends Filter>
|
|
1156
|
+
extends CRUDService<T, ID>,
|
|
1157
|
+
SearchService<T, F> {
|
|
1158
|
+
}
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
## Use Case Implementations
|
|
1162
|
+
|
|
1163
|
+
`SearchUseCase` delegates search operations:
|
|
1164
|
+
|
|
1165
|
+
```ts
|
|
1166
|
+
const useCase = new SearchUseCase(userRepository);
|
|
1167
|
+
|
|
1168
|
+
const result = await useCase.search(
|
|
1169
|
+
{
|
|
1170
|
+
q: 'john',
|
|
1171
|
+
limit: 20,
|
|
1172
|
+
},
|
|
1173
|
+
20,
|
|
1174
|
+
);
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
`CRUDUseCase` delegates CRUD operations:
|
|
1178
|
+
|
|
1179
|
+
```ts
|
|
1180
|
+
const useCase = new CRUDUseCase(userRepository);
|
|
1181
|
+
|
|
1182
|
+
const user = await useCase.load(id);
|
|
1183
|
+
|
|
1184
|
+
await useCase.create(userData);
|
|
1185
|
+
await useCase.update(userData);
|
|
1186
|
+
await useCase.patch({ id, name: 'New name' });
|
|
1187
|
+
await useCase.delete(id);
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
`UseCase` combines both APIs:
|
|
1191
|
+
|
|
1192
|
+
```ts
|
|
1193
|
+
const useCase = new UseCase(userRepository);
|
|
1194
|
+
|
|
1195
|
+
const result = await useCase.search(
|
|
1196
|
+
{
|
|
1197
|
+
q: 'john',
|
|
1198
|
+
limit: 20,
|
|
1199
|
+
},
|
|
1200
|
+
20,
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
const user = await useCase.load(id);
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
These classes are intentionally thin. They provide an application-facing boundary where business rules can be added without coupling controllers or handlers directly to repositories.
|
|
1207
|
+
|
|
1208
|
+
## Error Model
|
|
1209
|
+
|
|
1210
|
+
Validation/application errors are represented by:
|
|
1211
|
+
|
|
1212
|
+
```ts
|
|
1213
|
+
export interface ErrorMessage {
|
|
1214
|
+
field: string;
|
|
1215
|
+
code: string;
|
|
1216
|
+
param?: string | number | Date;
|
|
1217
|
+
message?: string;
|
|
1218
|
+
}
|
|
1219
|
+
```
|
|
1220
|
+
|
|
1221
|
+
The current result type is:
|
|
1222
|
+
|
|
1223
|
+
```ts
|
|
1224
|
+
export type Result<T> =
|
|
1225
|
+
| number
|
|
1226
|
+
| T
|
|
1227
|
+
| ErrorMessage[];
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
For example, an implementation may return:
|
|
1231
|
+
|
|
1232
|
+
```ts
|
|
1233
|
+
[
|
|
1234
|
+
{
|
|
1235
|
+
field: 'email',
|
|
1236
|
+
code: 'invalid',
|
|
1237
|
+
message: 'Invalid email address',
|
|
1238
|
+
},
|
|
1239
|
+
]
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
For larger applications, a discriminated result type is recommended because it makes success and failure states easier to distinguish.
|
|
1243
|
+
|
|
1244
|
+
## Security Considerations
|
|
1245
|
+
|
|
1246
|
+
### Always parameterize values
|
|
1247
|
+
|
|
1248
|
+
Do not construct SQL by interpolating user-provided values.
|
|
1249
|
+
|
|
1250
|
+
Prefer:
|
|
1251
|
+
|
|
1252
|
+
```ts
|
|
1253
|
+
await executor.execute(
|
|
1254
|
+
'UPDATE users SET name = ? WHERE id = ?',
|
|
1255
|
+
[name, id],
|
|
1256
|
+
);
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
Avoid:
|
|
1260
|
+
|
|
1261
|
+
```ts
|
|
1262
|
+
await executor.execute(
|
|
1263
|
+
`UPDATE users SET name = '${name}' WHERE id = '${id}'`,
|
|
1264
|
+
);
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
### Validate SQL identifiers
|
|
1268
|
+
|
|
1269
|
+
Fields, columns, and sort expressions are different from ordinary values.
|
|
1270
|
+
|
|
1271
|
+
For example:
|
|
1272
|
+
|
|
1273
|
+
```ts
|
|
1274
|
+
filter.sort
|
|
1275
|
+
filter.fields
|
|
1276
|
+
attribute.column
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
should not be inserted into SQL without validation or an allowlist.
|
|
1280
|
+
|
|
1281
|
+
Prefer mapping application-level names to trusted database columns:
|
|
1282
|
+
|
|
1283
|
+
```ts
|
|
1284
|
+
const columns = {
|
|
1285
|
+
id: 'user_id',
|
|
1286
|
+
name: 'user_name',
|
|
1287
|
+
createdAt: 'created_at',
|
|
1288
|
+
};
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
Never treat an arbitrary `sort` or `fields` value as trusted SQL.
|
|
1292
|
+
|
|
1293
|
+
## Design Guidelines
|
|
1294
|
+
|
|
1295
|
+
### Repository responsibilities
|
|
1296
|
+
|
|
1297
|
+
Repositories should focus on:
|
|
1298
|
+
|
|
1299
|
+
* persistence
|
|
1300
|
+
* SQL generation/execution
|
|
1301
|
+
* mapping database rows to application objects
|
|
1302
|
+
* transaction participation
|
|
1303
|
+
|
|
1304
|
+
They should not normally contain HTTP concerns or presentation logic.
|
|
1305
|
+
|
|
1306
|
+
### Use case responsibilities
|
|
1307
|
+
|
|
1308
|
+
Use cases are the preferred location for:
|
|
1309
|
+
|
|
1310
|
+
* business rules
|
|
1311
|
+
* orchestration across multiple repositories
|
|
1312
|
+
* authorization decisions
|
|
1313
|
+
* transaction coordination
|
|
1314
|
+
* application-level validation
|
|
1315
|
+
|
|
1316
|
+
### Transaction coordination
|
|
1317
|
+
|
|
1318
|
+
For operations spanning multiple repositories:
|
|
1319
|
+
|
|
1320
|
+
```ts
|
|
1321
|
+
const tx = await db.beginTransaction();
|
|
1322
|
+
|
|
1323
|
+
try {
|
|
1324
|
+
await repositoryA.update(a, tx);
|
|
1325
|
+
await repositoryB.create(b, tx);
|
|
1326
|
+
|
|
1327
|
+
await tx.commit();
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
await tx.rollback();
|
|
1330
|
+
throw error;
|
|
1331
|
+
}
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
This keeps transaction lifecycle management outside individual repositories.
|
|
1335
|
+
|
|
1336
|
+
## Known API Considerations
|
|
1337
|
+
|
|
1338
|
+
The current API has several areas that should be kept in mind when extending the library.
|
|
1339
|
+
|
|
1340
|
+
### Filter and search pagination
|
|
1341
|
+
|
|
1342
|
+
`Filter` already contains:
|
|
1343
|
+
|
|
1344
|
+
```ts
|
|
1345
|
+
page
|
|
1346
|
+
limit
|
|
1347
|
+
fields
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
while `SearchRepository.search()` also accepts:
|
|
1351
|
+
|
|
1352
|
+
```ts
|
|
1353
|
+
limit
|
|
1354
|
+
offset
|
|
1355
|
+
fields
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
These represent overlapping concepts. A future API revision should establish a single source of truth for pagination and selected fields.
|
|
1359
|
+
|
|
1360
|
+
### Attribute responsibilities
|
|
1361
|
+
|
|
1362
|
+
`Attribute` currently combines:
|
|
1363
|
+
|
|
1364
|
+
* database mapping
|
|
1365
|
+
* validation
|
|
1366
|
+
* formatting
|
|
1367
|
+
* search metadata
|
|
1368
|
+
* CRUD restrictions
|
|
1369
|
+
* serialization
|
|
1370
|
+
* relationship metadata
|
|
1371
|
+
|
|
1372
|
+
This is convenient for a small framework but may become difficult to maintain as the number of features grows.
|
|
1373
|
+
|
|
1374
|
+
### Result type
|
|
1375
|
+
|
|
1376
|
+
```ts
|
|
1377
|
+
number | T | ErrorMessage[]
|
|
1378
|
+
```
|
|
1379
|
+
|
|
1380
|
+
is intentionally flexible but can be difficult for callers to discriminate safely. A future version may use discriminated result objects.
|
|
1381
|
+
|
|
1382
|
+
### Patch behavior
|
|
1383
|
+
|
|
1384
|
+
`patch()` uses `Partial<T>`. This is convenient, but it does not express rules such as:
|
|
1385
|
+
|
|
1386
|
+
* immutable fields
|
|
1387
|
+
* required primary keys
|
|
1388
|
+
* fields that cannot be patched
|
|
1389
|
+
* fields that are read-only
|
|
1390
|
+
|
|
1391
|
+
Those rules currently need to be enforced through metadata or repository logic.
|
|
1392
|
+
|
|
1393
|
+
## Recommended Improvements
|
|
1394
|
+
|
|
1395
|
+
For future versions, consider:
|
|
1396
|
+
|
|
1397
|
+
1. Use `unknown[]` instead of `any[]` for SQL parameters.
|
|
1398
|
+
2. Define explicit search pagination semantics.
|
|
1399
|
+
3. Replace free-form `sort` strings with validated sort definitions.
|
|
1400
|
+
4. Separate validation, persistence, search, and formatting metadata from `Attribute`.
|
|
1401
|
+
5. Replace `Result<T>` with a discriminated result type.
|
|
1402
|
+
6. Remove defensive checks for methods that are already required by TypeScript interfaces.
|
|
1403
|
+
7. Clearly document transaction ownership.
|
|
1404
|
+
8. Consider separate types for database entities, create inputs, update inputs, and patch inputs.
|
|
1405
|
+
9. Export `StringMap` if it is intended to be part of the public API.
|
|
1406
|
+
10. Use descriptive parameter/property names instead of ambiguous names such as `bools`, `true`, `false`, and `typeof`.
|
|
1407
|
+
|
|
1408
|
+
## Summary
|
|
1409
|
+
|
|
1410
|
+
This library provides a clean foundation for a TypeScript application using:
|
|
1411
|
+
|
|
1412
|
+
```text
|
|
1413
|
+
Database
|
|
1414
|
+
↓
|
|
1415
|
+
Executor
|
|
1416
|
+
↓
|
|
1417
|
+
Transaction / Repository
|
|
1418
|
+
↓
|
|
1419
|
+
Use Case / Service
|
|
1420
|
+
↓
|
|
1421
|
+
Application
|
|
1422
|
+
```
|
|
1423
|
+
|
|
1424
|
+
Its main strengths are its small API surface, generic repository interfaces, explicit transaction support, and separation between persistence and application layers.
|
|
1425
|
+
|
|
1426
|
+
The most important areas to control as the framework evolves are SQL identifier safety, search/pagination consistency, transaction ownership, the overloaded `Attribute` metadata model, and the `Result<T>` error contract.
|
|
1427
|
+
|
|
1428
|
+
MIT
|
|
1429
|
+
|
|
1430
|
+
# TypeScript Application Core
|
|
1431
|
+
|
|
1432
|
+
A set of TypeScript interfaces, types, and lightweight use-case implementations for building applications around repositories, databases, validation, caching, logging, and messaging.
|
|
1433
|
+
|
|
1434
|
+
The library is designed around **interfaces and dependency inversion**, allowing application code to depend on abstractions rather than concrete infrastructure implementations.
|
|
1435
|
+
|
|
1436
|
+
## Features
|
|
1437
|
+
|
|
1438
|
+
* Generic CRUD and search repositories
|
|
1439
|
+
* Database executor and transaction abstractions
|
|
1440
|
+
* Application service/use-case contracts
|
|
1441
|
+
* Validation contracts and structured validation errors
|
|
1442
|
+
* Cache port abstraction
|
|
1443
|
+
* Structured logger abstraction
|
|
1444
|
+
* Generic messaging and message-envelope types
|
|
1445
|
+
* Shared schema and attribute metadata
|
|
1446
|
+
* Strong generic typing for entities, IDs, filters, and messages
|
|
1447
|
+
|
|
1448
|
+
## Architecture
|
|
1449
|
+
|
|
1450
|
+
The main dependency flow is:
|
|
1451
|
+
|
|
1452
|
+
```text
|
|
1453
|
+
Application
|
|
1454
|
+
│
|
|
1455
|
+
▼
|
|
1456
|
+
UseCase / Service
|
|
1457
|
+
│
|
|
1458
|
+
▼
|
|
1459
|
+
Repository
|
|
1460
|
+
│
|
|
1461
|
+
▼
|
|
1462
|
+
Executor
|
|
1463
|
+
│
|
|
1464
|
+
├── DB
|
|
1465
|
+
└── Transaction
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
Infrastructure services are exposed through ports:
|
|
1469
|
+
|
|
1470
|
+
```text
|
|
1471
|
+
Application
|
|
1472
|
+
├── CachePort
|
|
1473
|
+
├── Logger
|
|
1474
|
+
└── Messaging ports
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
This keeps business/application code independent from concrete database, cache, logging, and messaging implementations.
|
|
1478
|
+
|
|
1479
|
+
---
|
|
1480
|
+
|
|
1481
|
+
# Core Types
|
|
1482
|
+
|
|
1483
|
+
## Data types
|
|
1484
|
+
|
|
1485
|
+
`DataType` describes supported field types:
|
|
1486
|
+
|
|
1487
|
+
```ts
|
|
1488
|
+
type DataType =
|
|
1489
|
+
| 'ObjectId'
|
|
1490
|
+
| 'date'
|
|
1491
|
+
| 'datetime'
|
|
1492
|
+
| 'time'
|
|
1493
|
+
| 'boolean'
|
|
1494
|
+
| 'number'
|
|
1495
|
+
| 'integer'
|
|
1496
|
+
| 'string'
|
|
1497
|
+
| 'text'
|
|
1498
|
+
| 'object'
|
|
1499
|
+
| 'array'
|
|
1500
|
+
| 'binary'
|
|
1501
|
+
| 'primitives'
|
|
1502
|
+
| 'booleans'
|
|
1503
|
+
| 'numbers'
|
|
1504
|
+
| 'integers'
|
|
1505
|
+
| 'strings'
|
|
1506
|
+
| 'dates'
|
|
1507
|
+
| 'datetimes'
|
|
1508
|
+
| 'times';
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
`FormatType` provides common semantic formats:
|
|
1512
|
+
|
|
1513
|
+
```ts
|
|
1514
|
+
type FormatType =
|
|
1515
|
+
| 'currency'
|
|
1516
|
+
| 'percentage'
|
|
1517
|
+
| 'email'
|
|
1518
|
+
| 'url'
|
|
1519
|
+
| 'phone'
|
|
1520
|
+
| 'fax'
|
|
1521
|
+
| 'ipv4'
|
|
1522
|
+
| 'ipv6';
|
|
1523
|
+
```
|
|
1524
|
+
|
|
1525
|
+
`Operator` defines supported query operations:
|
|
1526
|
+
|
|
1527
|
+
```ts
|
|
1528
|
+
type Operator =
|
|
1529
|
+
| '='
|
|
1530
|
+
| 'like'
|
|
1531
|
+
| '!='
|
|
1532
|
+
| '<>'
|
|
1533
|
+
| '>'
|
|
1534
|
+
| '>='
|
|
1535
|
+
| '<'
|
|
1536
|
+
| '<=';
|
|
1537
|
+
```
|
|
1538
|
+
|
|
1539
|
+
## Attribute metadata
|
|
1540
|
+
|
|
1541
|
+
`Attribute` describes a model field and can contain database, validation, search, formatting, and persistence metadata.
|
|
1542
|
+
|
|
1543
|
+
```ts
|
|
1544
|
+
const attributes: Attributes = {
|
|
1545
|
+
email: {
|
|
1546
|
+
field: 'email',
|
|
1547
|
+
type: 'string',
|
|
1548
|
+
format: 'email',
|
|
1549
|
+
required: true,
|
|
1550
|
+
unique: true
|
|
1551
|
+
},
|
|
1552
|
+
|
|
1553
|
+
age: {
|
|
1554
|
+
field: 'age',
|
|
1555
|
+
type: 'integer',
|
|
1556
|
+
min: 0
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
```
|
|
1560
|
+
|
|
1561
|
+
Common attributes include:
|
|
1562
|
+
|
|
1563
|
+
| Property | Purpose |
|
|
1564
|
+
| ------------------------- | ----------------------------- |
|
|
1565
|
+
| `field` | Application field name |
|
|
1566
|
+
| `column` | Database column |
|
|
1567
|
+
| `type` | Field data type |
|
|
1568
|
+
| `format` | Semantic/display format |
|
|
1569
|
+
| `required` | Required-field validation |
|
|
1570
|
+
| `operator` | Default query operator |
|
|
1571
|
+
| `enum` | Allowed values |
|
|
1572
|
+
| `min` / `max` | Range validation |
|
|
1573
|
+
| `gt` / `lt` | Exclusive range validation |
|
|
1574
|
+
| `length` | Length constraint |
|
|
1575
|
+
| `precision` / `scale` | Numeric precision |
|
|
1576
|
+
| `exp` | Regular-expression validation |
|
|
1577
|
+
| `key` | Primary/key metadata |
|
|
1578
|
+
| `unique` | Uniqueness metadata |
|
|
1579
|
+
| `version` | Version field |
|
|
1580
|
+
| `createdAt` / `updatedAt` | Audit timestamps |
|
|
1581
|
+
| `noinsert` | Exclude from insert |
|
|
1582
|
+
| `noupdate` | Exclude from update |
|
|
1583
|
+
| `nopatch` | Exclude from patch |
|
|
1584
|
+
| `ignored` | Ignore the field |
|
|
1585
|
+
| `jsonField` | JSON field mapping |
|
|
1586
|
+
| `link` | Related-resource metadata |
|
|
1587
|
+
| `getString` | Custom string conversion |
|
|
1588
|
+
|
|
1589
|
+
---
|
|
1590
|
+
|
|
1591
|
+
# Filtering and Search
|
|
1592
|
+
|
|
1593
|
+
`Filter` represents a generic search filter:
|
|
1594
|
+
|
|
1595
|
+
```ts
|
|
1596
|
+
interface Filter {
|
|
1597
|
+
page?: number;
|
|
1598
|
+
limit: number;
|
|
1599
|
+
fields?: string[];
|
|
1600
|
+
sort?: string;
|
|
1601
|
+
q?: string;
|
|
1602
|
+
}
|
|
1603
|
+
```
|
|
1604
|
+
|
|
1605
|
+
A search returns:
|
|
1606
|
+
|
|
1607
|
+
```ts
|
|
1608
|
+
interface SearchResult<T> {
|
|
1609
|
+
list: T[];
|
|
1610
|
+
total?: number;
|
|
1611
|
+
next?: string;
|
|
1612
|
+
}
|
|
1613
|
+
```
|
|
1614
|
+
|
|
1615
|
+
Example:
|
|
1616
|
+
|
|
1617
|
+
```ts
|
|
1618
|
+
interface UserFilter extends Filter {
|
|
1619
|
+
status?: string;
|
|
1620
|
+
role?: string;
|
|
1621
|
+
}
|
|
1622
|
+
```
|
|
1623
|
+
|
|
1624
|
+
Repositories can then provide strongly typed search:
|
|
1625
|
+
|
|
1626
|
+
```ts
|
|
1627
|
+
interface UserRepository
|
|
1628
|
+
extends Repository<User, string, UserFilter> {}
|
|
1629
|
+
```
|
|
1630
|
+
|
|
1631
|
+
---
|
|
1632
|
+
|
|
1633
|
+
# Database Abstractions
|
|
1634
|
+
|
|
1635
|
+
## Executor
|
|
1636
|
+
|
|
1637
|
+
`Executor` abstracts database operations:
|
|
1638
|
+
|
|
1639
|
+
```ts
|
|
1640
|
+
interface Executor {
|
|
1641
|
+
driver: string;
|
|
1642
|
+
|
|
1643
|
+
param(i: number): string;
|
|
1644
|
+
|
|
1645
|
+
execute(
|
|
1646
|
+
sql: string,
|
|
1647
|
+
args?: any[]
|
|
1648
|
+
): Promise<number>;
|
|
1649
|
+
|
|
1650
|
+
executeBatch(
|
|
1651
|
+
statements: Statement[],
|
|
1652
|
+
firstSuccess?: boolean
|
|
1653
|
+
): Promise<number>;
|
|
1654
|
+
|
|
1655
|
+
query<T>(
|
|
1656
|
+
sql: string,
|
|
1657
|
+
args?: any[],
|
|
1658
|
+
m?: StringMap,
|
|
1659
|
+
bools?: Attribute[]
|
|
1660
|
+
): Promise<T[]>;
|
|
1661
|
+
}
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
A repository does not need to know which database driver is being used.
|
|
1665
|
+
|
|
1666
|
+
## Transaction
|
|
1667
|
+
|
|
1668
|
+
`Transaction` extends `Executor`:
|
|
1669
|
+
|
|
1670
|
+
```ts
|
|
1671
|
+
interface Transaction extends Executor {
|
|
1672
|
+
commit(): Promise<void>;
|
|
1673
|
+
rollback(): Promise<void>;
|
|
1674
|
+
}
|
|
1675
|
+
```
|
|
1676
|
+
|
|
1677
|
+
Typical usage:
|
|
1678
|
+
|
|
1679
|
+
```ts
|
|
1680
|
+
const tx = await db.beginTransaction();
|
|
1681
|
+
|
|
1682
|
+
try {
|
|
1683
|
+
await tx.execute(...);
|
|
1684
|
+
await tx.execute(...);
|
|
1685
|
+
|
|
1686
|
+
await tx.commit();
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
await tx.rollback();
|
|
1689
|
+
throw error;
|
|
1690
|
+
}
|
|
1691
|
+
```
|
|
1692
|
+
|
|
1693
|
+
Transaction state management remains the responsibility of the concrete implementation.
|
|
1694
|
+
|
|
1695
|
+
## DB
|
|
1696
|
+
|
|
1697
|
+
```ts
|
|
1698
|
+
interface DB extends Executor {
|
|
1699
|
+
beginTransaction(): Promise<Transaction>;
|
|
1700
|
+
}
|
|
1701
|
+
```
|
|
1702
|
+
|
|
1703
|
+
A concrete database adapter can implement `DB` while repositories depend only on the interface.
|
|
1704
|
+
|
|
1705
|
+
---
|
|
1706
|
+
|
|
1707
|
+
# Repositories
|
|
1708
|
+
|
|
1709
|
+
The repository layer separates persistence operations from application logic.
|
|
1710
|
+
|
|
1711
|
+
## CRUD repository
|
|
1712
|
+
|
|
1713
|
+
```ts
|
|
1714
|
+
interface CRUDRepository<T, ID> {
|
|
1715
|
+
load(id: ID, tx?: Transaction): Promise<T | null>;
|
|
1716
|
+
create(obj: T, tx?: Transaction): Promise<number>;
|
|
1717
|
+
update(obj: T, tx?: Transaction): Promise<number>;
|
|
1718
|
+
patch(obj: Partial<T>, tx?: Transaction): Promise<number>;
|
|
1719
|
+
delete(id: ID, tx?: Transaction): Promise<number>;
|
|
1720
|
+
}
|
|
1721
|
+
```
|
|
1722
|
+
|
|
1723
|
+
## Search repository
|
|
1724
|
+
|
|
1725
|
+
```ts
|
|
1726
|
+
interface SearchRepository<T, F extends Filter> {
|
|
1727
|
+
search(
|
|
1728
|
+
s: F,
|
|
1729
|
+
limit: number,
|
|
1730
|
+
offset?: number | string,
|
|
1731
|
+
fields?: string[],
|
|
1732
|
+
tx?: Transaction
|
|
1733
|
+
): Promise<SearchResult<T>>;
|
|
1734
|
+
}
|
|
1735
|
+
```
|
|
1736
|
+
|
|
1737
|
+
## Combined repository
|
|
1738
|
+
|
|
1739
|
+
```ts
|
|
1740
|
+
interface Repository<T, ID, F extends Filter>
|
|
1741
|
+
extends CRUDRepository<T, ID>,
|
|
1742
|
+
SearchRepository<T, F> {}
|
|
1743
|
+
```
|
|
1744
|
+
|
|
1745
|
+
This allows CRUD and search capabilities to be combined without forcing unrelated implementations into one interface.
|
|
1746
|
+
|
|
1747
|
+
---
|
|
1748
|
+
|
|
1749
|
+
# Use Cases and Services
|
|
1750
|
+
|
|
1751
|
+
The library provides thin application-layer implementations.
|
|
1752
|
+
|
|
1753
|
+
## SearchUseCase
|
|
1754
|
+
|
|
1755
|
+
```ts
|
|
1756
|
+
class SearchUseCase<T, F extends Filter>
|
|
1757
|
+
implements SearchService<T, F> {
|
|
1758
|
+
|
|
1759
|
+
constructor(
|
|
1760
|
+
protected repository: SearchRepository<T, F>
|
|
1761
|
+
) {}
|
|
1762
|
+
|
|
1763
|
+
search(
|
|
1764
|
+
filter: F,
|
|
1765
|
+
limit: number,
|
|
1766
|
+
page?: number | string,
|
|
1767
|
+
fields?: string[]
|
|
1768
|
+
): Promise<SearchResult<T>> {
|
|
1769
|
+
return this.repository.search(
|
|
1770
|
+
filter,
|
|
1771
|
+
limit,
|
|
1772
|
+
page,
|
|
1773
|
+
fields
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
```
|
|
1778
|
+
|
|
1779
|
+
## CRUDUseCase
|
|
1780
|
+
|
|
1781
|
+
```ts
|
|
1782
|
+
class CRUDUseCase<T, ID>
|
|
1783
|
+
implements CRUDService<T, ID> {
|
|
1784
|
+
|
|
1785
|
+
constructor(
|
|
1786
|
+
protected repository: CRUDRepository<T, ID>
|
|
1787
|
+
) {}
|
|
1788
|
+
|
|
1789
|
+
load(id: ID) {
|
|
1790
|
+
return this.repository.load(id);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
create(obj: T) {
|
|
1794
|
+
return this.repository.create(obj);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
update(obj: T) {
|
|
1798
|
+
return this.repository.update(obj);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
patch(obj: Partial<T>) {
|
|
1802
|
+
return this.repository.patch(obj);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
delete(id: ID) {
|
|
1806
|
+
return this.repository.delete(id);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
```
|
|
1810
|
+
|
|
1811
|
+
## Combined UseCase
|
|
1812
|
+
|
|
1813
|
+
```ts
|
|
1814
|
+
class UseCase<T, ID, F extends Filter>
|
|
1815
|
+
implements Service<T, ID, F> {
|
|
1816
|
+
|
|
1817
|
+
constructor(
|
|
1818
|
+
protected repository: Repository<T, ID, F>
|
|
1819
|
+
) {}
|
|
1820
|
+
|
|
1821
|
+
// CRUD + search operations
|
|
1822
|
+
}
|
|
1823
|
+
```
|
|
1824
|
+
|
|
1825
|
+
These classes are intentionally thin. Business rules can be added at the use-case layer without coupling them to database implementations.
|
|
1826
|
+
|
|
1827
|
+
---
|
|
1828
|
+
|
|
1829
|
+
# Services
|
|
1830
|
+
|
|
1831
|
+
The service contracts provide an application-facing API:
|
|
1832
|
+
|
|
1833
|
+
```ts
|
|
1834
|
+
interface SearchService<T, F extends Filter> {
|
|
1835
|
+
search(
|
|
1836
|
+
s: F,
|
|
1837
|
+
limit: number,
|
|
1838
|
+
page?: number | string,
|
|
1839
|
+
fields?: string[]
|
|
1840
|
+
): Promise<SearchResult<T>>;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
interface CRUDService<T, ID> {
|
|
1844
|
+
load(id: ID): Promise<T | null>;
|
|
1845
|
+
create(obj: T): Promise<Result<T>>;
|
|
1846
|
+
update(obj: T): Promise<Result<T>>;
|
|
1847
|
+
patch(obj: Partial<T>): Promise<Result<T>>;
|
|
1848
|
+
delete(id: ID): Promise<number>;
|
|
1849
|
+
}
|
|
1850
|
+
```
|
|
1851
|
+
|
|
1852
|
+
The combined service is:
|
|
1853
|
+
|
|
1854
|
+
```ts
|
|
1855
|
+
interface Service<T, ID, F extends Filter>
|
|
1856
|
+
extends CRUDService<T, ID>,
|
|
1857
|
+
SearchService<T, F> {}
|
|
1858
|
+
```
|
|
1859
|
+
|
|
1860
|
+
---
|
|
1861
|
+
|
|
1862
|
+
# Validation
|
|
1863
|
+
|
|
1864
|
+
Validation is abstracted behind:
|
|
1865
|
+
|
|
1866
|
+
```ts
|
|
1867
|
+
interface Validator<T> {
|
|
1868
|
+
validate(
|
|
1869
|
+
model: T,
|
|
1870
|
+
resource?: StringMap,
|
|
1871
|
+
patch?: boolean
|
|
1872
|
+
): Promise<ErrorMessage[]>;
|
|
1873
|
+
}
|
|
1874
|
+
```
|
|
1875
|
+
|
|
1876
|
+
Validation errors use:
|
|
1877
|
+
|
|
1878
|
+
```ts
|
|
1879
|
+
interface ErrorMessage {
|
|
1880
|
+
field: string;
|
|
1881
|
+
code: string;
|
|
1882
|
+
param?: string | number | Date;
|
|
1883
|
+
message?: string;
|
|
1884
|
+
}
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
Example:
|
|
1888
|
+
|
|
1889
|
+
```ts
|
|
1890
|
+
[
|
|
1891
|
+
{
|
|
1892
|
+
field: 'email',
|
|
1893
|
+
code: 'format',
|
|
1894
|
+
message: 'Invalid email address'
|
|
1895
|
+
},
|
|
1896
|
+
{
|
|
1897
|
+
field: 'age',
|
|
1898
|
+
code: 'min',
|
|
1899
|
+
param: 18,
|
|
1900
|
+
message: 'Age must be at least 18'
|
|
1901
|
+
}
|
|
1902
|
+
]
|
|
1903
|
+
```
|
|
1904
|
+
|
|
1905
|
+
---
|
|
1906
|
+
|
|
1907
|
+
# Result Type
|
|
1908
|
+
|
|
1909
|
+
Service methods use:
|
|
1910
|
+
|
|
1911
|
+
```ts
|
|
1912
|
+
type Result<T> =
|
|
1913
|
+
| number
|
|
1914
|
+
| T
|
|
1915
|
+
| ErrorMessage[];
|
|
1916
|
+
```
|
|
1917
|
+
|
|
1918
|
+
This allows a service to represent an operation result, returned model, or validation errors.
|
|
1919
|
+
|
|
1920
|
+
Because this is a broad union, consumers should define clear runtime handling conventions. A discriminated result type is preferable for new APIs where backwards compatibility is not required.
|
|
1921
|
+
|
|
1922
|
+
---
|
|
1923
|
+
|
|
1924
|
+
# Caching
|
|
1925
|
+
|
|
1926
|
+
`CachePort<K, V>` provides a database-independent cache abstraction:
|
|
1927
|
+
|
|
1928
|
+
```ts
|
|
1929
|
+
interface CachePort<K, V> {
|
|
1930
|
+
isEnabled?(): boolean;
|
|
1931
|
+
|
|
1932
|
+
put(
|
|
1933
|
+
key: K,
|
|
1934
|
+
obj: V,
|
|
1935
|
+
expiresInSeconds?: number
|
|
1936
|
+
): Promise<boolean>;
|
|
1937
|
+
|
|
1938
|
+
expire(
|
|
1939
|
+
key: K,
|
|
1940
|
+
timeToLive: number
|
|
1941
|
+
): Promise<boolean>;
|
|
1942
|
+
|
|
1943
|
+
get(key: K): Promise<V>;
|
|
1944
|
+
|
|
1945
|
+
getMany(keys: K[]): Promise<V[]>;
|
|
1946
|
+
|
|
1947
|
+
containsKey(key: K): Promise<boolean>;
|
|
1948
|
+
|
|
1949
|
+
remove(key: K): Promise<boolean>;
|
|
1950
|
+
|
|
1951
|
+
clear(): Promise<boolean>;
|
|
1952
|
+
|
|
1953
|
+
keys(): Promise<string[]>;
|
|
1954
|
+
|
|
1955
|
+
count(): Promise<number>;
|
|
1956
|
+
|
|
1957
|
+
size(): Promise<number>;
|
|
1958
|
+
}
|
|
1959
|
+
```
|
|
1960
|
+
|
|
1961
|
+
This allows implementations for Redis, in-memory caches, distributed caches, or other storage systems without changing application code.
|
|
1962
|
+
|
|
1963
|
+
---
|
|
1964
|
+
|
|
1965
|
+
# Logging
|
|
1966
|
+
|
|
1967
|
+
The logging layer consists of configuration types and a `Logger` interface.
|
|
1968
|
+
|
|
1969
|
+
## Logger
|
|
1970
|
+
|
|
1971
|
+
```ts
|
|
1972
|
+
interface Logger {
|
|
1973
|
+
name: Name;
|
|
1974
|
+
level: number;
|
|
1975
|
+
map: LogMap;
|
|
1976
|
+
constants?: SimpleMap;
|
|
1977
|
+
|
|
1978
|
+
trace(
|
|
1979
|
+
msg: string,
|
|
1980
|
+
m?: SimpleMap,
|
|
1981
|
+
ctx?: any
|
|
1982
|
+
): void;
|
|
1983
|
+
|
|
1984
|
+
debug(
|
|
1985
|
+
msg: string,
|
|
1986
|
+
m?: SimpleMap,
|
|
1987
|
+
ctx?: any
|
|
1988
|
+
): void;
|
|
1989
|
+
|
|
1990
|
+
info(
|
|
1991
|
+
msg: string,
|
|
1992
|
+
m?: SimpleMap,
|
|
1993
|
+
ctx?: any
|
|
1994
|
+
): void;
|
|
1995
|
+
|
|
1996
|
+
warn(
|
|
1997
|
+
msg: string,
|
|
1998
|
+
m?: SimpleMap,
|
|
1999
|
+
ctx?: any
|
|
2000
|
+
): void;
|
|
2001
|
+
|
|
2002
|
+
error(
|
|
2003
|
+
msg: string,
|
|
2004
|
+
m?: SimpleMap,
|
|
2005
|
+
ctx?: any
|
|
2006
|
+
): void;
|
|
2007
|
+
|
|
2008
|
+
panic(
|
|
2009
|
+
msg: string,
|
|
2010
|
+
m?: SimpleMap,
|
|
2011
|
+
ctx?: any
|
|
2012
|
+
): void;
|
|
2013
|
+
|
|
2014
|
+
fatal(
|
|
2015
|
+
msg: string,
|
|
2016
|
+
m?: SimpleMap,
|
|
2017
|
+
ctx?: any
|
|
2018
|
+
): void;
|
|
2019
|
+
|
|
2020
|
+
isLevelEnabled(level: number): boolean;
|
|
2021
|
+
isTraceEnabled(): boolean;
|
|
2022
|
+
isDebugEnabled(): boolean;
|
|
2023
|
+
isInfoEnabled(): boolean;
|
|
2024
|
+
isWarnEnabled(): boolean;
|
|
2025
|
+
isErrorEnabled(): boolean;
|
|
2026
|
+
isPanicEnabled(): boolean;
|
|
2027
|
+
isFatalEnabled(): boolean;
|
|
2028
|
+
}
|
|
2029
|
+
```
|
|
2030
|
+
|
|
2031
|
+
Logging configuration supports level names, field mappings, and constant metadata.
|
|
2032
|
+
|
|
2033
|
+
Example:
|
|
2034
|
+
|
|
2035
|
+
```ts
|
|
2036
|
+
const config: LogConfig = {
|
|
2037
|
+
log: {
|
|
2038
|
+
level: 'info',
|
|
2039
|
+
map: {
|
|
2040
|
+
time: 'timestamp',
|
|
2041
|
+
level: 'level',
|
|
2042
|
+
msg: 'message'
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
};
|
|
2046
|
+
```
|
|
2047
|
+
|
|
2048
|
+
---
|
|
2049
|
+
|
|
2050
|
+
# Messaging
|
|
2051
|
+
|
|
2052
|
+
The library provides generic producer and consumer contracts.
|
|
2053
|
+
|
|
2054
|
+
## Processing functions
|
|
2055
|
+
|
|
2056
|
+
```ts
|
|
2057
|
+
type Process<T, R, M> =
|
|
2058
|
+
(
|
|
2059
|
+
data: T,
|
|
2060
|
+
attributes?: StringMap,
|
|
2061
|
+
msg?: M
|
|
2062
|
+
) => Promise<R>;
|
|
2063
|
+
|
|
2064
|
+
type Handle<T, R, M> =
|
|
2065
|
+
(
|
|
2066
|
+
data: T,
|
|
2067
|
+
attributes?: StringMap,
|
|
2068
|
+
msg?: M
|
|
2069
|
+
) => Promise<R>;
|
|
2070
|
+
```
|
|
2071
|
+
|
|
2072
|
+
## Produce / write operations
|
|
2073
|
+
|
|
2074
|
+
```ts
|
|
2075
|
+
type Produce<T, R> =
|
|
2076
|
+
(data: T) => Promise<R>;
|
|
2077
|
+
```
|
|
2078
|
+
|
|
2079
|
+
Aliases are provided for common terminology:
|
|
2080
|
+
|
|
2081
|
+
```ts
|
|
2082
|
+
type Write<T, R> = Produce<T, R>;
|
|
2083
|
+
type Publish<T, R> = Produce<T, R>;
|
|
2084
|
+
type Send<T, R> = Produce<T, R>;
|
|
2085
|
+
type Put<T, R> = Produce<T, R>;
|
|
2086
|
+
type Set<T, R> = Produce<T, R>;
|
|
2087
|
+
```
|
|
2088
|
+
|
|
2089
|
+
## Consume operations
|
|
2090
|
+
|
|
2091
|
+
```ts
|
|
2092
|
+
type Consume<T> =
|
|
2093
|
+
(
|
|
2094
|
+
handle: (
|
|
2095
|
+
data: T,
|
|
2096
|
+
header?: StringMap
|
|
2097
|
+
) => Promise<number>
|
|
2098
|
+
) => void;
|
|
2099
|
+
```
|
|
2100
|
+
|
|
2101
|
+
Aliases include:
|
|
2102
|
+
|
|
2103
|
+
```ts
|
|
2104
|
+
type Read<T> = Consume<T>;
|
|
2105
|
+
type Subscribe<T> = Consume<T>;
|
|
2106
|
+
type Receive<T> = Consume<T>;
|
|
2107
|
+
type Get<T> = Consume<T>;
|
|
2108
|
+
type Fetch<T> = Consume<T>;
|
|
2109
|
+
```
|
|
2110
|
+
|
|
2111
|
+
---
|
|
2112
|
+
|
|
2113
|
+
# Message Envelope
|
|
2114
|
+
|
|
2115
|
+
`Message<T, ID, R>` represents a generic transport message:
|
|
2116
|
+
|
|
2117
|
+
```ts
|
|
2118
|
+
interface Message<T, ID, R> {
|
|
2119
|
+
id?: ID;
|
|
2120
|
+
data?: T;
|
|
2121
|
+
attributes?: StringMap;
|
|
2122
|
+
timestamp?: Date;
|
|
2123
|
+
raw?: R;
|
|
2124
|
+
}
|
|
2125
|
+
```
|
|
2126
|
+
|
|
2127
|
+
This allows application code to work with a normalized message representation while retaining the original transport payload through `raw`.
|
|
2128
|
+
|
|
2129
|
+
---
|
|
2130
|
+
|
|
2131
|
+
# Producers
|
|
2132
|
+
|
|
2133
|
+
```ts
|
|
2134
|
+
interface Producer<T, R, ID> {
|
|
2135
|
+
produce(
|
|
2136
|
+
to: string,
|
|
2137
|
+
data: T,
|
|
2138
|
+
attributes?: StringMap,
|
|
2139
|
+
id?: ID
|
|
2140
|
+
): Promise<R>;
|
|
2141
|
+
}
|
|
2142
|
+
```
|
|
2143
|
+
|
|
2144
|
+
Equivalent concepts are provided for publishing and sending:
|
|
2145
|
+
|
|
2146
|
+
```ts
|
|
2147
|
+
interface Publisher<T, R, ID> {
|
|
2148
|
+
publish(
|
|
2149
|
+
to: string,
|
|
2150
|
+
data: T,
|
|
2151
|
+
attributes?: StringMap,
|
|
2152
|
+
id?: ID
|
|
2153
|
+
): Promise<R>;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
interface Sender<T, R, ID> {
|
|
2157
|
+
send(
|
|
2158
|
+
to: string,
|
|
2159
|
+
data: T,
|
|
2160
|
+
attributes?: StringMap,
|
|
2161
|
+
id?: ID
|
|
2162
|
+
): Promise<R>;
|
|
2163
|
+
}
|
|
2164
|
+
```
|
|
2165
|
+
|
|
2166
|
+
---
|
|
2167
|
+
|
|
2168
|
+
# Consumers
|
|
2169
|
+
|
|
2170
|
+
Several consumer styles are available:
|
|
2171
|
+
|
|
2172
|
+
```ts
|
|
2173
|
+
interface SimpleConsumer<T, R> {
|
|
2174
|
+
consume(
|
|
2175
|
+
process: (
|
|
2176
|
+
data: T,
|
|
2177
|
+
attributes?: StringMap,
|
|
2178
|
+
raw?: R
|
|
2179
|
+
) => Promise<number>
|
|
2180
|
+
): void;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
interface SimpleSubscriber<T, R> {
|
|
2184
|
+
subscribe(
|
|
2185
|
+
process: (
|
|
2186
|
+
data: T,
|
|
2187
|
+
attributes?: StringMap,
|
|
2188
|
+
raw?: R
|
|
2189
|
+
) => Promise<number>
|
|
2190
|
+
): void;
|
|
2191
|
+
}
|
|
2192
|
+
```
|
|
2193
|
+
|
|
2194
|
+
Message-envelope based consumers are also available:
|
|
2195
|
+
|
|
2196
|
+
```ts
|
|
2197
|
+
interface MessageSubscriber<T, ID, R> {
|
|
2198
|
+
subscribe(
|
|
2199
|
+
process: (
|
|
2200
|
+
data: Message<T, ID, R>
|
|
2201
|
+
) => Promise<number>
|
|
2202
|
+
): void;
|
|
2203
|
+
}
|
|
2204
|
+
```
|
|
2205
|
+
|
|
2206
|
+
---
|
|
2207
|
+
|
|
2208
|
+
# Retry
|
|
2209
|
+
|
|
2210
|
+
Retry configuration is represented by:
|
|
2211
|
+
|
|
2212
|
+
```ts
|
|
2213
|
+
interface RetryConfig {
|
|
2214
|
+
name: string;
|
|
2215
|
+
limit: number;
|
|
2216
|
+
}
|
|
2217
|
+
```
|
|
2218
|
+
|
|
2219
|
+
Concrete messaging implementations can use this configuration to determine retry policies.
|
|
2220
|
+
|
|
2221
|
+
---
|
|
2222
|
+
|
|
2223
|
+
# Design Principles
|
|
2224
|
+
|
|
2225
|
+
## Dependency inversion
|
|
2226
|
+
|
|
2227
|
+
Application code should depend on interfaces such as:
|
|
2228
|
+
|
|
2229
|
+
```text
|
|
2230
|
+
Repository
|
|
2231
|
+
CachePort
|
|
2232
|
+
Validator
|
|
2233
|
+
Logger
|
|
2234
|
+
Producer
|
|
2235
|
+
Consumer
|
|
2236
|
+
```
|
|
2237
|
+
|
|
2238
|
+
rather than concrete infrastructure implementations.
|
|
2239
|
+
|
|
2240
|
+
## Generic programming
|
|
2241
|
+
|
|
2242
|
+
Most contracts are generic:
|
|
2243
|
+
|
|
2244
|
+
```text
|
|
2245
|
+
T = entity/message data
|
|
2246
|
+
ID = entity/message identifier
|
|
2247
|
+
F = search-filter type
|
|
2248
|
+
R = raw/transport/result type
|
|
2249
|
+
```
|
|
2250
|
+
|
|
2251
|
+
This allows the same abstractions to be reused across different domains.
|
|
2252
|
+
|
|
2253
|
+
## Separation of concerns
|
|
2254
|
+
|
|
2255
|
+
Persistence, validation, application logic, infrastructure, and messaging are deliberately represented as separate contracts.
|
|
2256
|
+
|
|
2257
|
+
---
|
|
2258
|
+
|
|
2259
|
+
# Important Implementation Considerations
|
|
2260
|
+
|
|
2261
|
+
## SQL safety
|
|
2262
|
+
|
|
2263
|
+
Dynamic SQL identifiers such as:
|
|
2264
|
+
|
|
2265
|
+
```text
|
|
2266
|
+
field
|
|
2267
|
+
column
|
|
2268
|
+
sort
|
|
2269
|
+
fields
|
|
2270
|
+
```
|
|
2271
|
+
|
|
2272
|
+
must be validated against an allowlist before being included in SQL.
|
|
2273
|
+
|
|
2274
|
+
Query values should always be passed as parameters.
|
|
2275
|
+
|
|
2276
|
+
Do not build SQL by directly concatenating untrusted values.
|
|
2277
|
+
|
|
2278
|
+
## Transaction lifecycle
|
|
2279
|
+
|
|
2280
|
+
A failed `commit()` does not necessarily mean that a subsequent `rollback()` can safely or successfully complete the transaction. Concrete transaction implementations should explicitly manage transaction state and connection cleanup.
|
|
2281
|
+
|
|
2282
|
+
## Cache misses
|
|
2283
|
+
|
|
2284
|
+
Concrete `CachePort` implementations should define and document how `get()` behaves when a key does not exist.
|
|
2285
|
+
|
|
2286
|
+
## Message lifecycle
|
|
2287
|
+
|
|
2288
|
+
Messaging implementations should document:
|
|
2289
|
+
|
|
2290
|
+
* acknowledgment behavior
|
|
2291
|
+
* retry behavior
|
|
2292
|
+
* concurrency
|
|
2293
|
+
* ordering guarantees
|
|
2294
|
+
* error handling
|
|
2295
|
+
* cancellation/shutdown
|
|
2296
|
+
* backpressure
|
|
2297
|
+
|
|
2298
|
+
The interfaces themselves intentionally leave these transport-specific details open.
|
|
2299
|
+
|
|
2300
|
+
---
|
|
2301
|
+
|
|
2302
|
+
# Example Domain
|
|
2303
|
+
|
|
2304
|
+
A simple user repository can be modeled as:
|
|
2305
|
+
|
|
2306
|
+
```ts
|
|
2307
|
+
interface User {
|
|
2308
|
+
id: string;
|
|
2309
|
+
name: string;
|
|
2310
|
+
email: string;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
interface UserFilter extends Filter {
|
|
2314
|
+
role?: string;
|
|
2315
|
+
active?: boolean;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
interface UserRepository
|
|
2319
|
+
extends Repository<User, string, UserFilter> {}
|
|
2320
|
+
```
|
|
2321
|
+
|
|
2322
|
+
The application layer can then depend on:
|
|
2323
|
+
|
|
2324
|
+
```ts
|
|
2325
|
+
class UserUseCase
|
|
2326
|
+
extends UseCase<User, string, UserFilter> {}
|
|
2327
|
+
```
|
|
2328
|
+
|
|
2329
|
+
A concrete infrastructure adapter implements `UserRepository` using the database implementation behind `Executor`.
|
|
2330
|
+
|
|
2331
|
+
---
|
|
2332
|
+
|
|
2333
|
+
# Recommended Project Structure
|
|
2334
|
+
|
|
2335
|
+
A project using this library can be organized as:
|
|
2336
|
+
|
|
2337
|
+
```text
|
|
2338
|
+
src/
|
|
2339
|
+
├── domain/
|
|
2340
|
+
│ └── user/
|
|
2341
|
+
│ ├── user.ts
|
|
2342
|
+
│ └── user-filter.ts
|
|
2343
|
+
│
|
|
2344
|
+
├── application/
|
|
2345
|
+
│ └── user/
|
|
2346
|
+
│ └── user-use-case.ts
|
|
2347
|
+
│
|
|
2348
|
+
├── repository/
|
|
2349
|
+
│ └── user-repository.ts
|
|
2350
|
+
│
|
|
2351
|
+
├── infrastructure/
|
|
2352
|
+
│ ├── database/
|
|
2353
|
+
│ │ ├── executor.ts
|
|
2354
|
+
│ │ └── transaction.ts
|
|
2355
|
+
│ ├── cache/
|
|
2356
|
+
│ ├── logging/
|
|
2357
|
+
│ └── messaging/
|
|
2358
|
+
│
|
|
2359
|
+
└── validation/
|
|
2360
|
+
└── user-validator.ts
|
|
2361
|
+
```
|
|
2362
|
+
|
|
2363
|
+
---
|
|
2364
|
+
|
|
2365
|
+
# Summary
|
|
2366
|
+
|
|
2367
|
+
This package provides a reusable set of TypeScript contracts for applications that follow a layered or ports-and-adapters architecture.
|
|
2368
|
+
|
|
2369
|
+
The central flow is:
|
|
2370
|
+
|
|
2371
|
+
```text
|
|
2372
|
+
┌──────────────┐
|
|
2373
|
+
│ Controller │
|
|
2374
|
+
└──────┬───────┘
|
|
2375
|
+
│
|
|
2376
|
+
▼
|
|
2377
|
+
┌──────────────┐
|
|
2378
|
+
│ UseCase │
|
|
2379
|
+
└──────┬───────┘
|
|
2380
|
+
│
|
|
2381
|
+
▼
|
|
2382
|
+
┌──────────────┐
|
|
2383
|
+
│ Repository │
|
|
2384
|
+
└──────┬───────┘
|
|
2385
|
+
│
|
|
2386
|
+
▼
|
|
2387
|
+
┌──────────────┐
|
|
2388
|
+
│ Executor │
|
|
2389
|
+
└──────┬───────┘
|
|
2390
|
+
│
|
|
2391
|
+
▼
|
|
2392
|
+
Database
|
|
2393
|
+
```
|
|
2394
|
+
|
|
2395
|
+
Supporting infrastructure is exposed through independent ports:
|
|
2396
|
+
|
|
2397
|
+
```text
|
|
2398
|
+
Application
|
|
2399
|
+
/ | \
|
|
2400
|
+
/ | \
|
|
2401
|
+
Validator Cache Logger
|
|
2402
|
+
\
|
|
2403
|
+
Messaging
|
|
2404
|
+
```
|
|
2405
|
+
|
|
2406
|
+
The interfaces are intentionally implementation-independent, making the package suitable as a shared foundation for multiple services, databases, cache systems, and messaging transports.
|
|
811
2407
|
|
|
812
|
-
# License
|
|
813
2408
|
|
|
814
2409
|
MIT
|