onecore 0.6.6 → 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 CHANGED
@@ -57,6 +57,22 @@ Without a shared foundation, each library tends to define its own interfaces and
57
57
 
58
58
  The implementation packages depend on these contracts, while application code can depend on the contracts rather than concrete infrastructure implementations.
59
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
+
60
76
  ## Features
61
77
 
62
78
  * Shared TypeScript interfaces for the Core TS ecosystem
@@ -776,3 +792,1618 @@ These projects depend only on interfaces and the shared `Attribute` metadata mod
776
792
  MIT
777
793
 
778
794
 
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.
2407
+
2408
+
2409
+ MIT