onecore 0.6.3 → 0.6.5
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 +543 -129
- package/lib/index.js +84 -0
- package/package.json +1 -1
- package/src/db.ts +2 -2
- package/src/formatter.ts +13 -14
- package/src/index.ts +2 -2
- package/src/service.ts +1 -1
package/README.md
CHANGED
|
@@ -4,34 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
`onecore` provides the common contracts, metadata model, and reusable application-layer base classes used throughout the Core TS ecosystem.
|
|
6
6
|
|
|
7
|
-
It
|
|
7
|
+
It is designed to let infrastructure libraries and applications share the same abstractions without coupling application code to a specific database, messaging system, validation implementation, cache, or framework.
|
|
8
8
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
* Common interfaces for enterprise applications
|
|
12
|
-
* Unified metadata model (`Attribute`)
|
|
13
|
-
* Reusable CRUD and Search use-case base classes
|
|
14
|
-
* Zero-cost abstractions through TypeScript interfaces
|
|
15
|
-
* Tree-shakeable architecture
|
|
16
|
-
* Works in both backend and frontend applications
|
|
17
|
-
* No framework dependency
|
|
18
|
-
* Fully written in TypeScript
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
# Installation
|
|
9
|
+
## Installation
|
|
23
10
|
|
|
24
11
|
```bash
|
|
25
12
|
npm install onecore
|
|
26
13
|
```
|
|
27
14
|
|
|
28
|
-
|
|
15
|
+
## What is onecore?
|
|
29
16
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Enterprise applications often use many infrastructure libraries.
|
|
33
|
-
|
|
34
|
-
For example:
|
|
17
|
+
Large applications commonly depend on many infrastructure libraries:
|
|
35
18
|
|
|
36
19
|
* Database
|
|
37
20
|
* Validation
|
|
@@ -39,108 +22,102 @@ For example:
|
|
|
39
22
|
* Message Queue
|
|
40
23
|
* Logging
|
|
41
24
|
* Health Check
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```text
|
|
47
|
-
Repository (sql)
|
|
48
|
-
Validator (validation)
|
|
49
|
-
Producer (mq)
|
|
50
|
-
Logger (logger)
|
|
51
|
-
Cache (cache)
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
`onecore` provides a single common language for the entire ecosystem.
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
import {
|
|
58
|
-
Repository,
|
|
59
|
-
Validator,
|
|
60
|
-
Producer,
|
|
61
|
-
CachePort,
|
|
62
|
-
Logger,
|
|
63
|
-
HealthChecker
|
|
64
|
-
} from "onecore";
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Applications only depend on **onecore**, while implementation libraries implement these contracts.
|
|
68
|
-
|
|
69
|
-
---
|
|
25
|
+
* HTTP clients
|
|
26
|
+
* Import / Export
|
|
27
|
+
* Localization
|
|
70
28
|
|
|
71
|
-
|
|
29
|
+
Without a shared foundation, each library tends to define its own interfaces and metadata.
|
|
72
30
|
|
|
73
|
-
|
|
74
|
-
Applications
|
|
75
|
-
┌───────────────────────────────────────────────────────────────┐
|
|
76
|
-
│ │
|
|
77
|
-
│ React Apps • REST APIs • Microservices • Workers • CLI │
|
|
78
|
-
│ │
|
|
79
|
-
└───────────────────────────────────────────────────────────────┘
|
|
80
|
-
▲
|
|
81
|
-
│
|
|
82
|
-
Reusable Use Cases (Optional)
|
|
83
|
-
CRUD • Search • View • Generic Services
|
|
84
|
-
▲
|
|
85
|
-
│
|
|
86
|
-
┌─────────────────────────────────────────────────────────────────────────────────┐
|
|
87
|
-
│ onecore │
|
|
88
|
-
│ │
|
|
89
|
-
│ • Common Interfaces │
|
|
90
|
-
│ • Unified Attribute Metadata │
|
|
91
|
-
│ • Locale & Currency │
|
|
92
|
-
│ • Shared Models │
|
|
93
|
-
│ • Reusable Service Layer │
|
|
94
|
-
└─────────────────────────────────────────────────────────────────────────────────┘
|
|
95
|
-
▲ ▲ ▲ ▲ ▲
|
|
96
|
-
│ │ │ │ │
|
|
97
|
-
│ │ │ │ │
|
|
98
|
-
┌────────────────┐ ┌────────────────┐ ┌──────────────────┐ ┌────────────────┐ ┌────────────────┐
|
|
99
|
-
│ Database │ │ Validation │ │ Messaging │ │ Import / Export│ │ Cache │
|
|
100
|
-
├────────────────┤ ├────────────────┤ ├──────────────────┤ ├────────────────┤ ├────────────────┤
|
|
101
|
-
│ sql-core │ │validation-core │ │message-processing│ │import-service │ │ cache-plus │
|
|
102
|
-
│ mysql2-core │ │ │ │nats-plus │ │export-kit │ │ redis-plus │
|
|
103
|
-
│ postgres-kit │ │ │ │redis-messaging │ │ │ │ │
|
|
104
|
-
│ oracle-core │ │ │ │rabbitmq-transport│ │ │ │ │
|
|
105
|
-
│ mssql-core │ │ │ │activemq │ │ │ │ │
|
|
106
|
-
│ mongodb-kit │ │ │ │ibmmq-plus │ │ │ │ │
|
|
107
|
-
└────────────────┘ └────────────────┘ └──────────────────┘ └────────────────┘ └────────────────┘
|
|
31
|
+
`onecore` provides a common set of contracts so that these libraries can work together through a consistent API.
|
|
108
32
|
|
|
33
|
+
```text
|
|
34
|
+
Application
|
|
35
|
+
│
|
|
36
|
+
Use Cases
|
|
37
|
+
│
|
|
38
|
+
┌─────────┴─────────┐
|
|
39
|
+
│ onecore │
|
|
40
|
+
│ │
|
|
41
|
+
│ Interfaces │
|
|
42
|
+
│ Attribute │
|
|
43
|
+
│ Repository │
|
|
44
|
+
│ Transaction │
|
|
45
|
+
│ Use Cases │
|
|
46
|
+
└─────────┬─────────┘
|
|
47
|
+
│
|
|
48
|
+
┌──────────┬──────────┼──────────┬──────────┐
|
|
49
|
+
│ │ │ │ │
|
|
50
|
+
Database Validation Cache Messaging Logging
|
|
109
51
|
```
|
|
110
52
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
---
|
|
53
|
+
The implementation packages depend on these contracts, while application code can depend on the contracts rather than concrete infrastructure implementations.
|
|
114
54
|
|
|
115
|
-
|
|
55
|
+
## Features
|
|
116
56
|
|
|
117
|
-
|
|
57
|
+
* Shared TypeScript interfaces for the Core TS ecosystem
|
|
58
|
+
* Unified `Attribute` metadata model
|
|
59
|
+
* Database executor and transaction abstractions
|
|
60
|
+
* Repository abstractions for CRUD and search
|
|
61
|
+
* 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
|
|
118
68
|
|
|
119
|
-
##
|
|
69
|
+
## Unified metadata
|
|
120
70
|
|
|
121
|
-
|
|
71
|
+
The central concept in `onecore` is `Attribute`.
|
|
122
72
|
|
|
123
73
|
```ts
|
|
124
|
-
|
|
74
|
+
export interface Attribute {
|
|
75
|
+
name?: string
|
|
76
|
+
field?: string
|
|
77
|
+
column?: string
|
|
78
|
+
type?: DataType
|
|
79
|
+
format?: FormatType
|
|
80
|
+
required?: boolean
|
|
81
|
+
|
|
82
|
+
key?: boolean
|
|
83
|
+
unique?: boolean
|
|
84
|
+
|
|
85
|
+
enum?: string[] | number[]
|
|
86
|
+
|
|
87
|
+
noinsert?: boolean
|
|
88
|
+
noupdate?: boolean
|
|
89
|
+
nopatch?: boolean
|
|
90
|
+
|
|
91
|
+
version?: boolean
|
|
92
|
+
createdAt?: boolean
|
|
93
|
+
updatedAt?: boolean
|
|
94
|
+
|
|
95
|
+
length?: number
|
|
96
|
+
min?: number | Date | string
|
|
97
|
+
max?: number | Date | string
|
|
98
|
+
gt?: number | Date | string
|
|
99
|
+
lt?: number | Date | string
|
|
100
|
+
|
|
101
|
+
precision?: number
|
|
102
|
+
scale?: number
|
|
103
|
+
}
|
|
125
104
|
```
|
|
126
105
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
## Base Classes
|
|
130
|
-
|
|
131
|
-
If an application extends one of the reusable CRUD or Search use-case classes, only those classes are included in the final bundle.
|
|
106
|
+
Unlike persistence-specific or validation-specific metadata, `Attribute` is the canonical metadata model shared across the entire ecosystem.
|
|
132
107
|
|
|
133
|
-
|
|
108
|
+
`Attribute` is intended to be shared by multiple parts of an application instead of maintaining separate metadata definitions for every subsystem.
|
|
134
109
|
|
|
135
|
-
|
|
110
|
+
For example, the same metadata can describe:
|
|
136
111
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
112
|
+
* Database mapping
|
|
113
|
+
* Validation
|
|
114
|
+
* Search
|
|
115
|
+
* Formatting
|
|
116
|
+
* Import
|
|
117
|
+
* Export
|
|
118
|
+
* Serialization
|
|
140
119
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Unlike persistence-specific or validation-specific metadata, `Attribute` is the canonical metadata model shared across the entire ecosystem.
|
|
120
|
+
Individual libraries consume only the properties relevant to them.
|
|
144
121
|
|
|
145
122
|
```
|
|
146
123
|
Attribute
|
|
@@ -151,7 +128,10 @@ Unlike persistence-specific or validation-specific metadata, `Attribute` is the
|
|
|
151
128
|
│ │ │
|
|
152
129
|
validation-core sql-core import-service
|
|
153
130
|
mysql2-core export-kit
|
|
131
|
+
mssql-core
|
|
132
|
+
oracle-core
|
|
154
133
|
postgres-kit
|
|
134
|
+
cassandra-core
|
|
155
135
|
mongodb-kit
|
|
156
136
|
```
|
|
157
137
|
|
|
@@ -190,17 +170,397 @@ The same metadata definition can therefore be used for
|
|
|
190
170
|
|
|
191
171
|
without duplication.
|
|
192
172
|
|
|
193
|
-
|
|
173
|
+
## Data types
|
|
194
174
|
|
|
195
|
-
|
|
175
|
+
`onecore` defines a common `DataType` model:
|
|
196
176
|
|
|
197
|
-
|
|
177
|
+
```ts
|
|
178
|
+
export type DataType =
|
|
179
|
+
| 'ObjectId'
|
|
180
|
+
| 'date'
|
|
181
|
+
| 'datetime'
|
|
182
|
+
| 'time'
|
|
183
|
+
| 'boolean'
|
|
184
|
+
| 'number'
|
|
185
|
+
| 'integer'
|
|
186
|
+
| 'string'
|
|
187
|
+
| 'text'
|
|
188
|
+
| 'object'
|
|
189
|
+
| 'array'
|
|
190
|
+
| 'binary'
|
|
191
|
+
| 'primitives'
|
|
192
|
+
| 'booleans'
|
|
193
|
+
| 'numbers'
|
|
194
|
+
| 'integers'
|
|
195
|
+
| 'strings'
|
|
196
|
+
| 'dates'
|
|
197
|
+
| 'datetimes'
|
|
198
|
+
| 'times'
|
|
199
|
+
```
|
|
198
200
|
|
|
199
|
-
|
|
201
|
+
Scalar and collection types can therefore share the same metadata system.
|
|
200
202
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
Formatting is represented separately:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
export type FormatType =
|
|
207
|
+
| 'currency'
|
|
208
|
+
| 'percentage'
|
|
209
|
+
| 'email'
|
|
210
|
+
| 'url'
|
|
211
|
+
| 'phone'
|
|
212
|
+
| 'fax'
|
|
213
|
+
| 'ipv4'
|
|
214
|
+
| 'ipv6'
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Database abstraction
|
|
218
|
+
|
|
219
|
+
`onecore` provides a small database-independent execution contract.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
export interface Executor {
|
|
223
|
+
driver: string
|
|
224
|
+
param(i: number): string
|
|
225
|
+
execute(sql: string, args?: any[]): Promise<number>
|
|
226
|
+
executeBatch(statements: Statement[], requireFirstAffected?: boolean): Promise<number>
|
|
227
|
+
query<T>(sql: string, args?: any[], m?: StringMap, bools?: Attribute[]): Promise<T[]>
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
A database implementation is responsible for adapting this interface to its native driver.
|
|
232
|
+
|
|
233
|
+
### Parameter abstraction
|
|
234
|
+
|
|
235
|
+
Different databases use different parameter syntaxes.
|
|
236
|
+
|
|
237
|
+
For example:
|
|
238
|
+
|
|
239
|
+
```text
|
|
240
|
+
PostgreSQL $1 $2 $3
|
|
241
|
+
MySQL ? ? ?
|
|
242
|
+
Oracle :1 :2 :3
|
|
243
|
+
SQL Server @p1 @p2 @p3
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`Executor.param()` allows higher-level SQL-building code to generate database-specific parameters without depending on a particular driver.
|
|
247
|
+
|
|
248
|
+
### Statements
|
|
249
|
+
|
|
250
|
+
A batch operation is represented by:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
export interface Statement {
|
|
254
|
+
query: string
|
|
255
|
+
params?: any[]
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
This keeps batch execution independent of the underlying database implementation.
|
|
260
|
+
|
|
261
|
+
## Transactions
|
|
262
|
+
|
|
263
|
+
Transactions extend the same executor abstraction:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
export interface Transaction extends Executor {
|
|
267
|
+
commit(): Promise<void>
|
|
268
|
+
rollback(): Promise<void>
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
A database provides transactions through:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
export interface DB extends Executor {
|
|
276
|
+
beginTransaction(): Promise<Transaction>
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
This allows repository code to use either the normal database executor or a transaction without knowing the underlying connection type.
|
|
281
|
+
|
|
282
|
+
For example:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
const tx = await db.beginTransaction()
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
await userRepository.create(user, tx)
|
|
289
|
+
await accountRepository.create(account, tx)
|
|
290
|
+
|
|
291
|
+
await tx.commit()
|
|
292
|
+
} catch (err) {
|
|
293
|
+
await tx.rollback()
|
|
294
|
+
throw err
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The transaction abstraction is particularly useful when several repositories participate in the same operation.
|
|
299
|
+
|
|
300
|
+
## Repository abstraction
|
|
301
|
+
|
|
302
|
+
`onecore` defines generic repository contracts for CRUD and search operations.
|
|
303
|
+
|
|
304
|
+
### CRUD
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
export interface CRUDRepository<T, ID> {
|
|
308
|
+
load(id: ID, tx?: Transaction): Promise<T | null>
|
|
309
|
+
create(obj: T, tx?: Transaction): Promise<number>
|
|
310
|
+
update(obj: T, tx?: Transaction): Promise<number>
|
|
311
|
+
patch(obj: Partial<T>, tx?: Transaction): Promise<number>
|
|
312
|
+
delete(id: ID, tx?: Transaction): Promise<number>
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The optional `Transaction` parameter allows the same repository method to participate in a larger transaction.
|
|
317
|
+
|
|
318
|
+
Normal operation:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
await repository.create(user)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Transactional operation:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
await repository.create(user, tx)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Search
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
export interface SearchRepository<T, F extends Filter> {
|
|
334
|
+
search(
|
|
335
|
+
s: F,
|
|
336
|
+
limit: number,
|
|
337
|
+
offset?: number | string,
|
|
338
|
+
fields?: string[],
|
|
339
|
+
tx?: Transaction
|
|
340
|
+
): Promise<SearchResult<T>>
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The generic filter model is:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
export interface Filter {
|
|
348
|
+
page?: number
|
|
349
|
+
limit: number
|
|
350
|
+
fields?: string[]
|
|
351
|
+
sort?: string
|
|
352
|
+
q?: string
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Search results are represented as:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
export interface SearchResult<T> {
|
|
360
|
+
list: T[]
|
|
361
|
+
total?: number
|
|
362
|
+
next?: string
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
This allows implementations to support result counts as well as cursor-style pagination.
|
|
367
|
+
|
|
368
|
+
## Combined repositories
|
|
369
|
+
|
|
370
|
+
CRUD and search repositories can be combined:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
export interface Repository<T, ID, F extends Filter>
|
|
374
|
+
extends CRUDRepository<T, ID>, SearchRepository<T, F> {
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
An application can therefore define:
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
interface UserFilter extends Filter {
|
|
382
|
+
active?: boolean
|
|
383
|
+
role?: string
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
type UserRepository = Repository<User, string, UserFilter>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Reusable use cases
|
|
390
|
+
|
|
391
|
+
`onecore` includes reusable application-layer classes so applications do not have to repeatedly write trivial forwarding logic.
|
|
392
|
+
|
|
393
|
+
### SearchUseCase
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
export class SearchUseCase<T, F extends Filter> {
|
|
397
|
+
constructor(
|
|
398
|
+
protected repository: SearchRepository<T, F>
|
|
399
|
+
) {}
|
|
400
|
+
|
|
401
|
+
search(filter: F, limit: number, page?: number | string,
|
|
402
|
+
fields?: string[]): Promise<SearchResult<T>> {
|
|
403
|
+
return this.repository.search(filter, limit, page, fields)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### CRUDUseCase
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
export class CRUDUseCase<T, ID> {
|
|
412
|
+
constructor(protected repository: CRUDRepository<T, ID>) {}
|
|
413
|
+
|
|
414
|
+
load(id: ID): Promise<T | null> {
|
|
415
|
+
return this.repository.load(id)
|
|
416
|
+
}
|
|
417
|
+
create(obj: T): Promise<number> {
|
|
418
|
+
return this.repository.create(obj)
|
|
419
|
+
}
|
|
420
|
+
update(obj: T): Promise<number> {
|
|
421
|
+
return this.repository.update(obj)
|
|
422
|
+
}
|
|
423
|
+
patch(obj: Partial<T>): Promise<number> {
|
|
424
|
+
return this.repository.patch(obj)
|
|
425
|
+
}
|
|
426
|
+
delete(id: ID): Promise<number> {
|
|
427
|
+
return this.repository.delete(id)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Combined UseCase
|
|
433
|
+
|
|
434
|
+
For applications that need both CRUD and search operations:
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
export class UseCase<T, ID, F extends Filter> {
|
|
438
|
+
constructor(protected repository: Repository<T, ID, F>) {}
|
|
439
|
+
|
|
440
|
+
search(filter: F, limit: number, page?: number | string,
|
|
441
|
+
fields?: string[]): Promise<SearchResult<T>> {
|
|
442
|
+
return this.repository.search(filter, limit, page, fields)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
load(id: ID): Promise<T | null> {
|
|
446
|
+
return this.repository.load(id)
|
|
447
|
+
}
|
|
448
|
+
create(obj: T): Promise<number> {
|
|
449
|
+
return this.repository.create(obj)
|
|
450
|
+
}
|
|
451
|
+
update(obj: T): Promise<number> {
|
|
452
|
+
return this.repository.update(obj)
|
|
453
|
+
}
|
|
454
|
+
patch(obj: Partial<T>): Promise<number> {
|
|
455
|
+
return this.repository.patch(obj)
|
|
456
|
+
}
|
|
457
|
+
delete(id: ID): Promise<number> {
|
|
458
|
+
return this.repository.delete(id)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Applications can extend these classes when the default repository-to-use-case behavior is sufficient, and override methods when additional business logic is required.
|
|
464
|
+
|
|
465
|
+
## Services and results
|
|
466
|
+
|
|
467
|
+
Service contracts can be defined independently from the repository layer:
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
export interface SearchService<T, F extends Filter> {
|
|
471
|
+
search(
|
|
472
|
+
filter: F,
|
|
473
|
+
limit: number,
|
|
474
|
+
page?: number | string,
|
|
475
|
+
fields?: string[]
|
|
476
|
+
): Promise<SearchResult<T>>
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
CRUD services expose application-facing operations:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
export interface CRUDService<T, ID> {
|
|
484
|
+
load(id: ID): Promise<T | null>
|
|
485
|
+
create(obj: T): Promise<Result<T>>
|
|
486
|
+
update(obj: T): Promise<Result<T>>
|
|
487
|
+
patch(obj: Partial<T>): Promise<Result<T>>
|
|
488
|
+
delete(id: ID): Promise<number>
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Validation or application errors can be represented by:
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
export interface ErrorMessage {
|
|
496
|
+
field: string
|
|
497
|
+
code: string
|
|
498
|
+
param?: string | number | Date
|
|
499
|
+
message?: string
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
with:
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
export type Result<T> =
|
|
507
|
+
| number
|
|
508
|
+
| T
|
|
509
|
+
| ErrorMessage[]
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
This allows an application layer to return structured field-level errors without coupling the service interface to a particular validation library.
|
|
513
|
+
|
|
514
|
+
## Ecosystem
|
|
515
|
+
|
|
516
|
+
`onecore` is intended to be the common foundation for the Core TS packages.
|
|
517
|
+
|
|
518
|
+
Examples include:
|
|
519
|
+
|
|
520
|
+
```text
|
|
521
|
+
Database
|
|
522
|
+
├── sql-core
|
|
523
|
+
├── mysql2-core
|
|
524
|
+
├── postgres-kit
|
|
525
|
+
├── oracle-core
|
|
526
|
+
├── mssql-core
|
|
527
|
+
└── mongodb-kit
|
|
528
|
+
|
|
529
|
+
Validation
|
|
530
|
+
└── validation-core
|
|
531
|
+
|
|
532
|
+
Messaging
|
|
533
|
+
├── message-processing
|
|
534
|
+
├── nats-plus
|
|
535
|
+
├── redis-messaging
|
|
536
|
+
├── rabbitmq-transport
|
|
537
|
+
├── activemq
|
|
538
|
+
└── ibmmq-plus
|
|
539
|
+
|
|
540
|
+
Cache
|
|
541
|
+
├── cache-plus
|
|
542
|
+
└── redis-plus
|
|
543
|
+
|
|
544
|
+
Other
|
|
545
|
+
├── logger-core
|
|
546
|
+
├── health-service
|
|
547
|
+
├── web-clients
|
|
548
|
+
├── locale-service
|
|
549
|
+
├── import-service
|
|
550
|
+
└── export-kit
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
These packages can share the same contracts and metadata model while remaining independently implemented.
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
## Shared Interfaces
|
|
557
|
+
|
|
558
|
+
`onecore` defines the common contracts used by the ecosystem.
|
|
559
|
+
|
|
560
|
+
### Database
|
|
203
561
|
* SearchRepository
|
|
562
|
+
* CRUDRepository
|
|
563
|
+
* Repository
|
|
204
564
|
* Transaction
|
|
205
565
|
* Statement
|
|
206
566
|
* QueryBuilder
|
|
@@ -209,12 +569,15 @@ Implemented by
|
|
|
209
569
|
|
|
210
570
|
* sql-core
|
|
211
571
|
* mysql2-core
|
|
572
|
+
* oracle-core
|
|
573
|
+
* mssql-core
|
|
212
574
|
* postgres-kit
|
|
575
|
+
* cassandra-core
|
|
213
576
|
* mongodb-kit
|
|
214
577
|
|
|
215
578
|
---
|
|
216
579
|
|
|
217
|
-
|
|
580
|
+
### Validation
|
|
218
581
|
|
|
219
582
|
* Validator<T>
|
|
220
583
|
|
|
@@ -224,7 +587,7 @@ Implemented by
|
|
|
224
587
|
|
|
225
588
|
---
|
|
226
589
|
|
|
227
|
-
|
|
590
|
+
### Cache
|
|
228
591
|
|
|
229
592
|
* CachePort
|
|
230
593
|
|
|
@@ -235,7 +598,7 @@ Implemented by
|
|
|
235
598
|
|
|
236
599
|
---
|
|
237
600
|
|
|
238
|
-
|
|
601
|
+
### Message Queue
|
|
239
602
|
|
|
240
603
|
* Producer
|
|
241
604
|
* Consumer
|
|
@@ -252,7 +615,7 @@ Implemented by
|
|
|
252
615
|
|
|
253
616
|
---
|
|
254
617
|
|
|
255
|
-
|
|
618
|
+
### Logging
|
|
256
619
|
|
|
257
620
|
* Logger
|
|
258
621
|
|
|
@@ -262,7 +625,7 @@ Implemented by
|
|
|
262
625
|
|
|
263
626
|
---
|
|
264
627
|
|
|
265
|
-
|
|
628
|
+
### Health Check
|
|
266
629
|
|
|
267
630
|
* HealthChecker
|
|
268
631
|
|
|
@@ -272,7 +635,7 @@ Implemented by
|
|
|
272
635
|
|
|
273
636
|
---
|
|
274
637
|
|
|
275
|
-
|
|
638
|
+
### HTTP
|
|
276
639
|
|
|
277
640
|
* HttpRequest
|
|
278
641
|
* HttpOptionsService
|
|
@@ -283,7 +646,7 @@ Implemented by
|
|
|
283
646
|
|
|
284
647
|
---
|
|
285
648
|
|
|
286
|
-
|
|
649
|
+
### Localization
|
|
287
650
|
|
|
288
651
|
* Locale
|
|
289
652
|
* Currency
|
|
@@ -292,23 +655,74 @@ Implemented by
|
|
|
292
655
|
|
|
293
656
|
* locale-service
|
|
294
657
|
|
|
295
|
-
|
|
658
|
+
## Zero-cost abstractions
|
|
296
659
|
|
|
297
|
-
|
|
660
|
+
One of the design goals of `onecore` is to provide shared APIs without imposing framework-level runtime overhead.
|
|
298
661
|
|
|
299
|
-
|
|
662
|
+
TypeScript interfaces such as:
|
|
300
663
|
|
|
301
|
-
|
|
664
|
+
```ts
|
|
665
|
+
Repository
|
|
666
|
+
Transaction
|
|
667
|
+
Executor
|
|
668
|
+
Validator
|
|
669
|
+
```
|
|
302
670
|
|
|
303
|
-
|
|
304
|
-
* Search services
|
|
305
|
-
* View services
|
|
671
|
+
do not exist in the generated JavaScript.
|
|
306
672
|
|
|
307
|
-
|
|
673
|
+
Reusable classes such as `UseCase` are only included when they are actually imported and used, allowing modern bundlers to tree-shake unused code.
|
|
308
674
|
|
|
309
|
-
|
|
675
|
+
This makes the package suitable for both backend and frontend projects.
|
|
310
676
|
|
|
311
|
-
|
|
677
|
+
## Backend and frontend
|
|
678
|
+
|
|
679
|
+
Backend applications can use `onecore` for:
|
|
680
|
+
|
|
681
|
+
* Repository contracts
|
|
682
|
+
* Database transactions
|
|
683
|
+
* Shared metadata
|
|
684
|
+
* Application use cases
|
|
685
|
+
* Infrastructure interfaces
|
|
686
|
+
|
|
687
|
+
Frontend applications can use shared models and metadata without needing to import backend implementations.
|
|
688
|
+
|
|
689
|
+
For example:
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
import type {
|
|
693
|
+
Attribute,
|
|
694
|
+
Filter,
|
|
695
|
+
SearchResult
|
|
696
|
+
} from 'onecore'
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
This allows a frontend and backend to share the same domain metadata and contracts.
|
|
700
|
+
|
|
701
|
+
## Design principles
|
|
702
|
+
|
|
703
|
+
### One common contract
|
|
704
|
+
|
|
705
|
+
Infrastructure implementations should depend on common interfaces instead of forcing applications to depend on implementation details.
|
|
706
|
+
|
|
707
|
+
### Metadata reuse
|
|
708
|
+
|
|
709
|
+
Define metadata once and reuse it across database mapping, validation, search, serialization, import/export, and formatting.
|
|
710
|
+
|
|
711
|
+
### Database independence
|
|
712
|
+
|
|
713
|
+
Application and repository contracts should not expose a PostgreSQL client, MySQL connection, Oracle connection, or other driver-specific resource.
|
|
714
|
+
|
|
715
|
+
### Explicit transactions
|
|
716
|
+
|
|
717
|
+
Transactions are passed explicitly to repository operations when multiple operations must participate in the same atomic unit of work.
|
|
718
|
+
|
|
719
|
+
### Minimal runtime
|
|
720
|
+
|
|
721
|
+
Keep shared contracts in TypeScript interfaces and keep runtime implementations small and reusable.
|
|
722
|
+
|
|
723
|
+
## License
|
|
724
|
+
|
|
725
|
+
MIT
|
|
312
726
|
|
|
313
727
|
# Frontend Support
|
|
314
728
|
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export class SearchUseCase {
|
|
2
|
+
constructor(repository) {
|
|
3
|
+
this.repository = repository;
|
|
4
|
+
this.search = this.search.bind(this);
|
|
5
|
+
}
|
|
6
|
+
search(s, limit, page, fields) {
|
|
7
|
+
return this.repository.search(s, limit, page, fields);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export const SearchManager = SearchUseCase;
|
|
11
|
+
export class Writer {
|
|
12
|
+
constructor(repository) {
|
|
13
|
+
this.repository = repository;
|
|
14
|
+
this.create = this.create.bind(this);
|
|
15
|
+
this.update = this.update.bind(this);
|
|
16
|
+
this.patch = this.patch.bind(this);
|
|
17
|
+
}
|
|
18
|
+
create(obj) {
|
|
19
|
+
return this.repository.create(obj);
|
|
20
|
+
}
|
|
21
|
+
update(obj) {
|
|
22
|
+
return this.repository.update(obj);
|
|
23
|
+
}
|
|
24
|
+
patch(obj) {
|
|
25
|
+
return (this.repository.patch ? this.repository.patch(obj) : Promise.resolve(-1));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class SearchWriter extends Writer {
|
|
29
|
+
constructor(repository) {
|
|
30
|
+
super(repository);
|
|
31
|
+
this.repository = repository;
|
|
32
|
+
this.search = this.search.bind(this);
|
|
33
|
+
}
|
|
34
|
+
search(filter, limit, page, fields) {
|
|
35
|
+
return this.repository.search(filter, limit, page, fields);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export class UseCase extends SearchWriter {
|
|
39
|
+
constructor(repository) {
|
|
40
|
+
super(repository);
|
|
41
|
+
this.repository = repository;
|
|
42
|
+
this.metadata = this.metadata.bind(this);
|
|
43
|
+
this.keys = this.keys.bind(this);
|
|
44
|
+
this.load = this.load.bind(this);
|
|
45
|
+
this.delete = this.delete.bind(this);
|
|
46
|
+
}
|
|
47
|
+
metadata() {
|
|
48
|
+
return (this.repository.metadata ? this.repository.metadata() : undefined);
|
|
49
|
+
}
|
|
50
|
+
keys() {
|
|
51
|
+
return (this.repository.keys ? this.repository.keys() : []);
|
|
52
|
+
}
|
|
53
|
+
load(id) {
|
|
54
|
+
return this.repository.load(id);
|
|
55
|
+
}
|
|
56
|
+
delete(id) {
|
|
57
|
+
return this.repository.delete(id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class CRUDUseCase extends Writer {
|
|
61
|
+
constructor(repository) {
|
|
62
|
+
super(repository);
|
|
63
|
+
this.repository = repository;
|
|
64
|
+
this.metadata = this.metadata.bind(this);
|
|
65
|
+
this.keys = this.keys.bind(this);
|
|
66
|
+
this.load = this.load.bind(this);
|
|
67
|
+
this.delete = this.delete.bind(this);
|
|
68
|
+
}
|
|
69
|
+
metadata() {
|
|
70
|
+
return (this.repository.metadata ? this.repository.metadata() : undefined);
|
|
71
|
+
}
|
|
72
|
+
keys() {
|
|
73
|
+
return (this.repository.keys ? this.repository.keys() : []);
|
|
74
|
+
}
|
|
75
|
+
load(id) {
|
|
76
|
+
return this.repository.load(id);
|
|
77
|
+
}
|
|
78
|
+
delete(id) {
|
|
79
|
+
return this.repository.delete(id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export const GenericUseCase = CRUDUseCase;
|
|
83
|
+
export const GenericManager = CRUDUseCase;
|
|
84
|
+
export const CRUDManager = CRUDUseCase;
|
package/package.json
CHANGED
package/src/db.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface Executor {
|
|
|
18
18
|
driver: string
|
|
19
19
|
param(i: number): string
|
|
20
20
|
execute(sql: string, args?: any[]): Promise<number>
|
|
21
|
-
executeBatch(statements: Statement[],
|
|
21
|
+
executeBatch(statements: Statement[], requireFirstAffected?: boolean): Promise<number>
|
|
22
22
|
query<T>(sql: string, args?: any[], m?: StringMap, bools?: Attribute[]): Promise<T[]>
|
|
23
23
|
}
|
|
24
24
|
export interface Transaction extends Executor {
|
|
@@ -35,7 +35,7 @@ export interface ViewRepository<T, ID> {
|
|
|
35
35
|
load(id: ID, tx?: Transaction): Promise<T | null>;
|
|
36
36
|
}
|
|
37
37
|
export interface SearchRepository<T, F extends Filter> {
|
|
38
|
-
search(
|
|
38
|
+
search(filter: F, limit: number, offset?: number|string, fields?: string[], tx?: Transaction): Promise<SearchResult<T>>;
|
|
39
39
|
}
|
|
40
40
|
export interface ViewSearchRepository<T, ID, F extends Filter>
|
|
41
41
|
extends ViewRepository<T, ID>, SearchRepository<T, F> {
|
package/src/formatter.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
export interface Locale {
|
|
2
|
-
|
|
3
|
-
countryCode: string
|
|
4
|
-
dateFormat: string
|
|
5
|
-
firstDayOfWeek: number
|
|
6
|
-
decimalSeparator: string
|
|
7
|
-
groupSeparator: string
|
|
8
|
-
decimalDigits: number
|
|
9
|
-
currencyCode: string
|
|
10
|
-
currencySymbol: string
|
|
11
|
-
currencyPattern: number
|
|
12
|
-
currencySample?: string;
|
|
2
|
+
code: string
|
|
3
|
+
countryCode: string
|
|
4
|
+
dateFormat: string
|
|
5
|
+
firstDayOfWeek: number
|
|
6
|
+
decimalSeparator: string
|
|
7
|
+
groupSeparator: string
|
|
8
|
+
decimalDigits: number
|
|
9
|
+
currencyCode: string
|
|
10
|
+
currencySymbol: string
|
|
11
|
+
currencyPattern: number
|
|
13
12
|
}
|
|
14
13
|
export interface Currency {
|
|
15
|
-
code: string
|
|
16
|
-
decimalDigits: number
|
|
17
|
-
symbol: string
|
|
14
|
+
code: string
|
|
15
|
+
decimalDigits: number
|
|
16
|
+
symbol: string
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
export interface StringFormatter<T> {
|
package/src/index.ts
CHANGED
|
@@ -171,8 +171,8 @@ export class SearchWriter<T, F extends Filter> extends Writer<T> {
|
|
|
171
171
|
super(repository)
|
|
172
172
|
this.search = this.search.bind(this);
|
|
173
173
|
}
|
|
174
|
-
search(
|
|
175
|
-
return this.repository.search(
|
|
174
|
+
search(filter: F, limit: number, page?: number|string, fields?: string[]): Promise<SearchResult<T>> {
|
|
175
|
+
return this.repository.search(filter, limit, page, fields);
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
package/src/service.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface ViewService<T, ID> {
|
|
|
9
9
|
|
|
10
10
|
export interface SearchService<T, F extends Filter> {
|
|
11
11
|
keys?(): string[];
|
|
12
|
-
search(
|
|
12
|
+
search(filter: F, limit: number, page?: number|string, fields?: string[]): Promise<SearchResult<T>>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface Query<T, ID, F extends Filter> extends SearchService<T, F> {
|