@naturalcycles/db-lib 8.50.2 → 8.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/adapter/file/file.db.d.ts +8 -8
  2. package/dist/adapter/file/file.db.model.d.ts +4 -4
  3. package/dist/adapter/file/inMemory.persistence.plugin.d.ts +1 -1
  4. package/dist/adapter/file/localFile.persistence.plugin.d.ts +1 -1
  5. package/dist/adapter/file/noop.persistence.plugin.d.ts +1 -1
  6. package/dist/adapter/inmemory/inMemory.db.d.ts +8 -8
  7. package/dist/adapter/inmemory/queryInMemory.d.ts +1 -1
  8. package/dist/common.db.d.ts +12 -12
  9. package/dist/commondao/common.dao.d.ts +17 -6
  10. package/dist/commondao/common.dao.js +75 -1
  11. package/dist/commondao/common.dao.model.d.ts +20 -23
  12. package/dist/db.model.d.ts +2 -1
  13. package/dist/db.model.js +1 -0
  14. package/dist/kv/commonKeyValueDB.d.ts +9 -9
  15. package/dist/kv/commonKeyValueDao.d.ts +3 -3
  16. package/dist/query/dbQuery.d.ts +10 -8
  17. package/dist/query/dbQuery.js +6 -0
  18. package/dist/testing/daoTest.js +1 -0
  19. package/package.json +1 -1
  20. package/src/adapter/file/file.db.model.ts +4 -4
  21. package/src/adapter/file/file.db.ts +12 -12
  22. package/src/adapter/file/inMemory.persistence.plugin.ts +1 -1
  23. package/src/adapter/file/localFile.persistence.plugin.ts +1 -1
  24. package/src/adapter/file/noop.persistence.plugin.ts +1 -1
  25. package/src/adapter/inmemory/inMemory.db.ts +11 -11
  26. package/src/adapter/inmemory/queryInMemory.ts +1 -4
  27. package/src/common.db.ts +24 -18
  28. package/src/commondao/common.dao.model.ts +32 -27
  29. package/src/commondao/common.dao.ts +108 -10
  30. package/src/db.model.ts +1 -0
  31. package/src/kv/commonKeyValueDB.ts +9 -9
  32. package/src/kv/commonKeyValueDao.ts +3 -3
  33. package/src/query/dbQuery.ts +18 -8
  34. package/src/testing/daoTest.ts +2 -0
@@ -3,10 +3,10 @@ import { DBSaveBatchOperation } from '../../db.model'
3
3
  import type { DBQueryOrder } from '../../query/dbQuery'
4
4
 
5
5
  export interface FileDBPersistencePlugin {
6
- ping(): Promise<void>
7
- getTables(): Promise<string[]>
8
- loadFile<ROW extends Partial<ObjectWithId>>(table: string): Promise<ROW[]>
9
- saveFiles(ops: DBSaveBatchOperation<any>[]): Promise<void>
6
+ ping: () => Promise<void>
7
+ getTables: () => Promise<string[]>
8
+ loadFile: <ROW extends ObjectWithId>(table: string) => Promise<ROW[]>
9
+ saveFiles: (ops: DBSaveBatchOperation<any>[]) => Promise<void>
10
10
  }
11
11
 
12
12
  export interface FileDBCfg {
@@ -65,13 +65,13 @@ export class FileDB extends BaseCommonDB implements CommonDB {
65
65
  return tables
66
66
  }
67
67
 
68
- override async getByIds<ROW extends Partial<ObjectWithId>>(
68
+ override async getByIds<ROW extends ObjectWithId>(
69
69
  table: string,
70
70
  ids: ROW['id'][],
71
71
  _opt?: CommonDBOptions,
72
72
  ): Promise<ROW[]> {
73
73
  const byId = _by(await this.loadFile<ROW>(table), r => r.id)
74
- return ids.map(id => byId[id!]!).filter(Boolean)
74
+ return ids.map(id => byId[id]!).filter(Boolean)
75
75
  }
76
76
 
77
77
  override async saveBatch<ROW extends Partial<ObjectWithId>>(
@@ -115,7 +115,7 @@ export class FileDB extends BaseCommonDB implements CommonDB {
115
115
  await pMap(
116
116
  tables,
117
117
  async table => {
118
- const rows: ObjectWithId[] = await this.loadFile(table)
118
+ const rows = await this.loadFile(table)
119
119
  data[table] = _by(rows, r => r.id)
120
120
  },
121
121
  { concurrency: 16 },
@@ -168,7 +168,7 @@ export class FileDB extends BaseCommonDB implements CommonDB {
168
168
  }
169
169
  }
170
170
 
171
- override async runQuery<ROW extends Partial<ObjectWithId>>(
171
+ override async runQuery<ROW extends ObjectWithId>(
172
172
  q: DBQuery<ROW>,
173
173
  _opt?: CommonDBOptions,
174
174
  ): Promise<RunQueryResult<ROW>> {
@@ -177,14 +177,14 @@ export class FileDB extends BaseCommonDB implements CommonDB {
177
177
  }
178
178
  }
179
179
 
180
- override async runQueryCount<ROW extends Partial<ObjectWithId>>(
180
+ override async runQueryCount<ROW extends ObjectWithId>(
181
181
  q: DBQuery<ROW>,
182
182
  _opt?: CommonDBOptions,
183
183
  ): Promise<number> {
184
184
  return (await this.loadFile(q.table)).length
185
185
  }
186
186
 
187
- override streamQuery<ROW extends Partial<ObjectWithId>>(
187
+ override streamQuery<ROW extends ObjectWithId>(
188
188
  q: DBQuery<ROW>,
189
189
  opt?: CommonDBStreamOptions,
190
190
  ): ReadableTyped<ROW> {
@@ -198,7 +198,7 @@ export class FileDB extends BaseCommonDB implements CommonDB {
198
198
  return readable
199
199
  }
200
200
 
201
- override async deleteByQuery<ROW extends Partial<ObjectWithId>>(
201
+ override async deleteByQuery<ROW extends ObjectWithId>(
202
202
  q: DBQuery<ROW>,
203
203
  _opt?: CommonDBOptions,
204
204
  ): Promise<number> {
@@ -206,7 +206,7 @@ export class FileDB extends BaseCommonDB implements CommonDB {
206
206
 
207
207
  let deleted = 0
208
208
  queryInMemory(q, _stringMapValues(byId)).forEach(r => {
209
- delete byId[r.id!]
209
+ delete byId[r.id]
210
210
  deleted++
211
211
  })
212
212
 
@@ -217,7 +217,7 @@ export class FileDB extends BaseCommonDB implements CommonDB {
217
217
  return deleted
218
218
  }
219
219
 
220
- override async getTableSchema<ROW extends Partial<ObjectWithId>>(
220
+ override async getTableSchema<ROW extends ObjectWithId>(
221
221
  table: string,
222
222
  ): Promise<JsonSchemaRootObject<ROW>> {
223
223
  const rows = await this.loadFile(table)
@@ -228,7 +228,7 @@ export class FileDB extends BaseCommonDB implements CommonDB {
228
228
  }
229
229
 
230
230
  // wrapper, to handle logging
231
- async loadFile<ROW extends Partial<ObjectWithId>>(table: string): Promise<ROW[]> {
231
+ async loadFile<ROW extends ObjectWithId>(table: string): Promise<ROW[]> {
232
232
  const started = this.logStarted(`loadFile(${table})`)
233
233
  const rows = await this.cfg.plugin.loadFile<ROW>(table)
234
234
  this.logFinished(started, `loadFile(${table}) ${rows.length} row(s)`)
@@ -236,7 +236,7 @@ export class FileDB extends BaseCommonDB implements CommonDB {
236
236
  }
237
237
 
238
238
  // wrapper, to handle logging, sorting rows before saving
239
- async saveFile<ROW extends Partial<ObjectWithId>>(table: string, _rows: ROW[]): Promise<void> {
239
+ async saveFile<ROW extends ObjectWithId>(table: string, _rows: ROW[]): Promise<void> {
240
240
  // if (!_rows.length) return // NO, it should be able to save file with 0 rows!
241
241
 
242
242
  // Sort the rows, if needed
@@ -257,7 +257,7 @@ export class FileDB extends BaseCommonDB implements CommonDB {
257
257
  this.logFinished(started, op)
258
258
  }
259
259
 
260
- private sortRows<ROW extends Partial<ObjectWithId>>(rows: ROW[]): ROW[] {
260
+ private sortRows<ROW extends ObjectWithId>(rows: ROW[]): ROW[] {
261
261
  rows = rows.map(r => _filterUndefinedValues(r))
262
262
 
263
263
  if (this.cfg.sortOnSave) {
@@ -14,7 +14,7 @@ export class InMemoryPersistencePlugin implements FileDBPersistencePlugin {
14
14
  return Object.keys(this.data)
15
15
  }
16
16
 
17
- async loadFile<ROW extends Partial<ObjectWithId>>(table: string): Promise<ROW[]> {
17
+ async loadFile<ROW extends ObjectWithId>(table: string): Promise<ROW[]> {
18
18
  return Object.values(this.data[table] || ({} as any))
19
19
  }
20
20
 
@@ -46,7 +46,7 @@ export class LocalFilePersistencePlugin implements FileDBPersistencePlugin {
46
46
  .map(f => f.split('.ndjson')[0]!)
47
47
  }
48
48
 
49
- async loadFile<ROW extends Partial<ObjectWithId>>(table: string): Promise<ROW[]> {
49
+ async loadFile<ROW extends ObjectWithId>(table: string): Promise<ROW[]> {
50
50
  await fs.ensureDir(this.cfg.storagePath)
51
51
  const ext = `ndjson${this.cfg.gzip ? '.gz' : ''}`
52
52
  const filePath = `${this.cfg.storagePath}/${table}.${ext}`
@@ -9,7 +9,7 @@ export class NoopPersistencePlugin implements FileDBPersistencePlugin {
9
9
  return []
10
10
  }
11
11
 
12
- async loadFile<ROW extends Partial<ObjectWithId>>(_table: string): Promise<ROW[]> {
12
+ async loadFile<ROW extends ObjectWithId>(_table: string): Promise<ROW[]> {
13
13
  return []
14
14
  }
15
15
 
@@ -120,7 +120,7 @@ export class InMemoryDB implements CommonDB {
120
120
  return Object.keys(this.data).filter(t => t.startsWith(this.cfg.tablesPrefix))
121
121
  }
122
122
 
123
- async getTableSchema<ROW extends Partial<ObjectWithId>>(
123
+ async getTableSchema<ROW extends ObjectWithId>(
124
124
  _table: string,
125
125
  ): Promise<JsonSchemaRootObject<ROW>> {
126
126
  const table = this.cfg.tablesPrefix + _table
@@ -130,7 +130,7 @@ export class InMemoryDB implements CommonDB {
130
130
  }
131
131
  }
132
132
 
133
- async createTable<ROW extends Partial<ObjectWithId>>(
133
+ async createTable<ROW extends ObjectWithId>(
134
134
  _table: string,
135
135
  _schema: JsonSchemaObject<ROW>,
136
136
  opt: CommonDBCreateOptions = {},
@@ -143,14 +143,14 @@ export class InMemoryDB implements CommonDB {
143
143
  }
144
144
  }
145
145
 
146
- async getByIds<ROW extends Partial<ObjectWithId>>(
146
+ async getByIds<ROW extends ObjectWithId>(
147
147
  _table: string,
148
148
  ids: ROW['id'][],
149
149
  _opt?: CommonDBOptions,
150
150
  ): Promise<ROW[]> {
151
151
  const table = this.cfg.tablesPrefix + _table
152
152
  this.data[table] ||= {}
153
- return ids.map(id => this.data[table]![id!] as ROW).filter(Boolean)
153
+ return ids.map(id => this.data[table]![id] as ROW).filter(Boolean)
154
154
  }
155
155
 
156
156
  async saveBatch<ROW extends Partial<ObjectWithId>>(
@@ -184,7 +184,7 @@ export class InMemoryDB implements CommonDB {
184
184
  })
185
185
  }
186
186
 
187
- async deleteByQuery<ROW extends Partial<ObjectWithId>>(
187
+ async deleteByQuery<ROW extends ObjectWithId>(
188
188
  q: DBQuery<ROW>,
189
189
  _opt?: CommonDBOptions,
190
190
  ): Promise<number> {
@@ -192,14 +192,14 @@ export class InMemoryDB implements CommonDB {
192
192
  this.data[table] ||= {}
193
193
  let count = 0
194
194
  queryInMemory(q, Object.values(this.data[table] || {}) as ROW[]).forEach(r => {
195
- if (!this.data[table]![r.id!]) return
196
- delete this.data[table]![r.id!]
195
+ if (!this.data[table]![r.id]) return
196
+ delete this.data[table]![r.id]
197
197
  count++
198
198
  })
199
199
  return count
200
200
  }
201
201
 
202
- async updateByQuery<ROW extends Partial<ObjectWithId>>(
202
+ async updateByQuery<ROW extends ObjectWithId>(
203
203
  q: DBQuery<ROW>,
204
204
  patch: DBPatch<ROW>,
205
205
  ): Promise<number> {
@@ -221,7 +221,7 @@ export class InMemoryDB implements CommonDB {
221
221
  return rows.length
222
222
  }
223
223
 
224
- async runQuery<ROW extends Partial<ObjectWithId>>(
224
+ async runQuery<ROW extends ObjectWithId>(
225
225
  q: DBQuery<ROW>,
226
226
  _opt?: CommonDBOptions,
227
227
  ): Promise<RunQueryResult<ROW>> {
@@ -229,7 +229,7 @@ export class InMemoryDB implements CommonDB {
229
229
  return { rows: queryInMemory(q, Object.values(this.data[table] || {}) as ROW[]) }
230
230
  }
231
231
 
232
- async runQueryCount<ROW extends Partial<ObjectWithId>>(
232
+ async runQueryCount<ROW extends ObjectWithId>(
233
233
  q: DBQuery<ROW>,
234
234
  _opt?: CommonDBOptions,
235
235
  ): Promise<number> {
@@ -237,7 +237,7 @@ export class InMemoryDB implements CommonDB {
237
237
  return queryInMemory<any>(q, Object.values(this.data[table] || {})).length
238
238
  }
239
239
 
240
- streamQuery<ROW extends Partial<ObjectWithId>>(
240
+ streamQuery<ROW extends ObjectWithId>(
241
241
  q: DBQuery<ROW>,
242
242
  _opt?: CommonDBOptions,
243
243
  ): ReadableTyped<ROW> {
@@ -18,10 +18,7 @@ const FILTER_FNS: Record<DBQueryFilterOperator, FilterFn> = {
18
18
 
19
19
  // Important: q.table is not used in this function, so tablesPrefix is not needed.
20
20
  // But should be careful here..
21
- export function queryInMemory<ROW extends Partial<ObjectWithId>>(
22
- q: DBQuery<ROW>,
23
- rows: ROW[] = [],
24
- ): ROW[] {
21
+ export function queryInMemory<ROW extends ObjectWithId>(q: DBQuery<ROW>, rows: ROW[] = []): ROW[] {
25
22
  // .filter
26
23
  // eslint-disable-next-line unicorn/no-array-reduce
27
24
  rows = q._filters.reduce((rows, filter) => {
package/src/common.db.ts CHANGED
@@ -18,12 +18,12 @@ export interface CommonDB {
18
18
  * It SHOULD fail if DB setup is wrong (e.g on wrong credentials).
19
19
  * It SHOULD succeed if e.g getByIds(['nonExistingKey']) doesn't throw.
20
20
  */
21
- ping(): Promise<void>
21
+ ping: () => Promise<void>
22
22
 
23
23
  /**
24
24
  * Return all tables (table names) available in this DB.
25
25
  */
26
- getTables(): Promise<string[]>
26
+ getTables: () => Promise<string[]>
27
27
 
28
28
  /**
29
29
  * $id of the schema SHOULD be like this:
@@ -31,61 +31,67 @@ export interface CommonDB {
31
31
  *
32
32
  * This is important for the code to rely on it, and it's verified by dbTest
33
33
  */
34
- getTableSchema<ROW extends ObjectWithId>(table: string): Promise<JsonSchemaRootObject<ROW>>
34
+ getTableSchema: <ROW extends ObjectWithId>(table: string) => Promise<JsonSchemaRootObject<ROW>>
35
35
 
36
36
  /**
37
37
  * Will do like `create table ...` for mysql.
38
38
  * Caution! dropIfExists defaults to false. If set to true - will actually DROP the table!
39
39
  */
40
- createTable<ROW extends ObjectWithId>(
40
+ createTable: <ROW extends ObjectWithId>(
41
41
  table: string,
42
42
  schema: JsonSchemaObject<ROW>,
43
43
  opt?: CommonDBCreateOptions,
44
- ): Promise<void>
44
+ ) => Promise<void>
45
45
 
46
46
  // GET
47
47
  /**
48
48
  * Order of items returned is not guaranteed to match order of ids.
49
49
  * (Such limitation exists because Datastore doesn't support it).
50
50
  */
51
- getByIds<ROW extends ObjectWithId>(
51
+ getByIds: <ROW extends ObjectWithId>(
52
52
  table: string,
53
53
  ids: ROW['id'][],
54
54
  opt?: CommonDBOptions,
55
- ): Promise<ROW[]>
55
+ ) => Promise<ROW[]>
56
56
 
57
57
  // QUERY
58
58
  /**
59
59
  * Order by 'id' is not supported by all implementations (for example, Datastore doesn't support it).
60
60
  */
61
- runQuery<ROW extends ObjectWithId>(
61
+ runQuery: <ROW extends ObjectWithId>(
62
62
  q: DBQuery<ROW>,
63
63
  opt?: CommonDBOptions,
64
- ): Promise<RunQueryResult<ROW>>
64
+ ) => Promise<RunQueryResult<ROW>>
65
65
 
66
- runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>
66
+ runQueryCount: <ROW extends ObjectWithId>(
67
+ q: DBQuery<ROW>,
68
+ opt?: CommonDBOptions,
69
+ ) => Promise<number>
67
70
 
68
- streamQuery<ROW extends ObjectWithId>(
71
+ streamQuery: <ROW extends ObjectWithId>(
69
72
  q: DBQuery<ROW>,
70
73
  opt?: CommonDBStreamOptions,
71
- ): ReadableTyped<ROW>
74
+ ) => ReadableTyped<ROW>
72
75
 
73
76
  // SAVE
74
77
  /**
75
78
  * rows can have missing ids only if DB supports auto-generating them (like mysql auto_increment).
76
79
  */
77
- saveBatch<ROW extends Partial<ObjectWithId>>(
80
+ saveBatch: <ROW extends Partial<ObjectWithId>>(
78
81
  table: string,
79
82
  rows: ROW[],
80
83
  opt?: CommonDBSaveOptions<ROW>,
81
- ): Promise<void>
84
+ ) => Promise<void>
82
85
 
83
86
  // DELETE
84
87
  /**
85
88
  * Returns number of deleted items.
86
89
  * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
87
90
  */
88
- deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>
91
+ deleteByQuery: <ROW extends ObjectWithId>(
92
+ q: DBQuery<ROW>,
93
+ opt?: CommonDBOptions,
94
+ ) => Promise<number>
89
95
 
90
96
  /**
91
97
  * Applies patch to the rows returned by the query.
@@ -105,16 +111,16 @@ export interface CommonDB {
105
111
  *
106
112
  * Returns number of rows affected.
107
113
  */
108
- updateByQuery<ROW extends ObjectWithId>(
114
+ updateByQuery: <ROW extends ObjectWithId>(
109
115
  q: DBQuery<ROW>,
110
116
  patch: DBPatch<ROW>,
111
117
  opt?: CommonDBOptions,
112
- ): Promise<number>
118
+ ) => Promise<number>
113
119
 
114
120
  // TRANSACTION
115
121
  /**
116
122
  * Should be implemented as a Transaction (best effort), which means that
117
123
  * either ALL or NONE of the operations should be applied.
118
124
  */
119
- commitTransaction(tx: DBTransaction, opt?: CommonDBOptions): Promise<void>
125
+ commitTransaction: (tx: DBTransaction, opt?: CommonDBOptions) => Promise<void>
120
126
  }
@@ -1,4 +1,11 @@
1
- import { CommonLogger, ErrorMode, ObjectWithId } from '@naturalcycles/js-lib'
1
+ import {
2
+ CommonLogger,
3
+ ErrorMode,
4
+ ObjectWithId,
5
+ Saved,
6
+ ZodError,
7
+ ZodSchema,
8
+ } from '@naturalcycles/js-lib'
2
9
  import {
3
10
  AjvSchema,
4
11
  AjvValidationError,
@@ -12,25 +19,24 @@ import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions } from '../
12
19
 
13
20
  export interface CommonDaoHooks<
14
21
  BM extends Partial<ObjectWithId<ID>>,
15
- DBM extends Partial<ObjectWithId<ID>> = BM,
16
- ID extends string | number = NonNullable<BM['id']>,
22
+ DBM extends ObjectWithId<ID>,
23
+ TM,
24
+ ID extends string | number,
17
25
  > {
18
- createRandomId(): ID
26
+ createRandomId: () => ID
19
27
  /**
20
28
  * createNaturalId hook is called (tried) first.
21
29
  * If it doesn't exist - createRandomId is called.
22
30
  */
23
- createNaturalId(obj: DBM | BM): ID
24
- parseNaturalId(id: ID): Partial<DBM>
25
- beforeCreate(bm: Partial<BM>): Partial<BM>
26
- beforeDBMValidate(dbm: Partial<DBM>): Partial<DBM>
27
- beforeDBMToBM(dbm: DBM): Partial<BM> | Promise<Partial<BM>>
28
- beforeBMToDBM(bm: BM): Partial<DBM> | Promise<Partial<DBM>>
29
-
30
- /**
31
- * Applicable to **read** operations, never to **save** operations.
32
- */
33
- anonymize(dbm: DBM): DBM
31
+ createNaturalId: (obj: DBM | BM) => ID
32
+ parseNaturalId: (id: ID) => Partial<DBM>
33
+ beforeCreate: (bm: Partial<BM>) => Partial<BM>
34
+ beforeDBMValidate: (dbm: Partial<DBM>) => Partial<DBM>
35
+ beforeDBMToBM: (dbm: DBM) => Partial<BM> | Promise<Partial<BM>>
36
+ beforeBMToDBM: (bm: BM) => Partial<DBM> | Promise<Partial<DBM>>
37
+ beforeTMToBM: (tm: TM) => Partial<BM>
38
+ beforeBMToTM: (bm: BM) => Partial<TM>
39
+ anonymize: (dbm: DBM) => DBM
34
40
 
35
41
  /**
36
42
  * If hook is defined - allows to prevent or modify the error thrown.
@@ -38,7 +44,7 @@ export interface CommonDaoHooks<
38
44
  * Return original `err` to pass the error through (will be thrown in CommonDao).
39
45
  * Return modified/new `Error` if needed.
40
46
  */
41
- onValidationError(err: JoiValidationError | AjvValidationError): Error | false
47
+ onValidationError: (err: JoiValidationError | AjvValidationError | ZodError) => Error | false
42
48
  }
43
49
 
44
50
  export enum CommonDaoLogLevel {
@@ -61,18 +67,20 @@ export enum CommonDaoLogLevel {
61
67
  }
62
68
 
63
69
  export interface CommonDaoCfg<
64
- BM extends ObjectWithId<ID>,
65
- DBM extends ObjectWithId<ID> = BM,
66
- ID extends string | number = BM['id'],
70
+ BM extends Partial<ObjectWithId<ID>>,
71
+ DBM extends ObjectWithId<ID> = Saved<BM>,
72
+ TM = BM,
73
+ ID extends string | number = string,
67
74
  > {
68
75
  db: CommonDB
69
76
  table: string
70
77
 
71
78
  /**
72
- * Joi or AjvSchema are supported.
79
+ * Joi, AjvSchema or ZodSchema is supported.
73
80
  */
74
- dbmSchema?: ObjectSchemaTyped<DBM> | AjvSchema<DBM>
75
- bmSchema?: ObjectSchemaTyped<BM> | AjvSchema<BM>
81
+ dbmSchema?: ObjectSchemaTyped<DBM> | AjvSchema<DBM> | ZodSchema<DBM>
82
+ bmSchema?: ObjectSchemaTyped<BM> | AjvSchema<BM> | ZodSchema<BM>
83
+ tmSchema?: ObjectSchemaTyped<TM> | AjvSchema<TM> | ZodSchema<TM>
76
84
 
77
85
  excludeFromIndexes?: (keyof DBM)[]
78
86
 
@@ -84,8 +92,6 @@ export interface CommonDaoCfg<
84
92
  * `delete*` and `patch` will throw.
85
93
  *
86
94
  * You can still override saveMethod, or set opt.allowMutability to allow deletion.
87
- *
88
- * todo: consider merging it with readOnly, as it's almost the same
89
95
  */
90
96
  immutable?: boolean
91
97
 
@@ -111,7 +117,7 @@ export interface CommonDaoCfg<
111
117
  logStarted?: boolean
112
118
 
113
119
  // Hooks are designed with inspiration from got/ky interface
114
- hooks?: Partial<CommonDaoHooks<BM, DBM, ID>>
120
+ hooks?: Partial<CommonDaoHooks<BM, DBM, TM, ID>>
115
121
 
116
122
  /**
117
123
  * Defaults to 'string'
@@ -128,7 +134,6 @@ export interface CommonDaoCfg<
128
134
  /**
129
135
  * See the same option in CommonDB.
130
136
  * Defaults to false normally.
131
- * "Generated" means "generated by the underlying DB" (e.g MySQL autoincrement).
132
137
  */
133
138
  assignGeneratedIds?: boolean
134
139
 
@@ -226,7 +231,7 @@ export interface CommonDaoOptions extends CommonDBOptions {
226
231
  /**
227
232
  * All properties default to undefined.
228
233
  */
229
- export interface CommonDaoSaveOptions<DBM extends Partial<ObjectWithId>>
234
+ export interface CommonDaoSaveOptions<DBM extends ObjectWithId>
230
235
  extends CommonDaoOptions,
231
236
  CommonDBSaveOptions<DBM> {
232
237
  /**
@@ -6,6 +6,7 @@ import {
6
6
  _since,
7
7
  _truncate,
8
8
  _uniqBy,
9
+ AnyObject,
9
10
  AppError,
10
11
  AsyncMapper,
11
12
  ErrorMode,
@@ -15,6 +16,9 @@ import {
15
16
  pMap,
16
17
  Saved,
17
18
  Unsaved,
19
+ ZodSchema,
20
+ ZodValidationError,
21
+ zSafeValidate,
18
22
  } from '@naturalcycles/js-lib'
19
23
  import {
20
24
  _pipeline,
@@ -62,13 +66,15 @@ const isCI = !!process.env['CI']
62
66
  *
63
67
  * DBM = Database model (how it's stored in DB)
64
68
  * BM = Backend model (optimized for API access)
69
+ * TM = Transport model (optimized to be sent over the wire)
65
70
  */
66
71
  export class CommonDao<
67
- BM extends ObjectWithId<ID>,
68
- DBM extends ObjectWithId<ID> = BM,
69
- ID extends string | number = BM['id'],
72
+ BM extends Partial<ObjectWithId<ID>>,
73
+ DBM extends ObjectWithId<ID> = Saved<BM>,
74
+ TM extends AnyObject = BM,
75
+ ID extends string | number = NonNullable<BM['id']>,
70
76
  > {
71
- constructor(public cfg: CommonDaoCfg<BM, DBM, ID>) {
77
+ constructor(public cfg: CommonDaoCfg<BM, DBM, TM, ID>) {
72
78
  this.cfg = {
73
79
  // Default is to NOT log in AppEngine and in CI,
74
80
  // otherwise to log Operations
@@ -87,6 +93,8 @@ export class CommonDao<
87
93
  beforeDBMValidate: dbm => dbm,
88
94
  beforeDBMToBM: dbm => dbm as any,
89
95
  beforeBMToDBM: bm => bm as any,
96
+ beforeTMToBM: tm => tm as any,
97
+ beforeBMToTM: bm => bm as any,
90
98
  anonymize: dbm => dbm,
91
99
  onValidationError: err => err,
92
100
  ...cfg.hooks,
@@ -159,6 +167,24 @@ export class CommonDao<
159
167
  return dbm || null
160
168
  }
161
169
 
170
+ async getByIdAsTM(id: undefined | null, opt?: CommonDaoOptions): Promise<null>
171
+ async getByIdAsTM(id?: ID | null, opt?: CommonDaoOptions): Promise<TM | null>
172
+ async getByIdAsTM(id?: ID | null, opt: CommonDaoOptions = {}): Promise<TM | null> {
173
+ if (!id) return null
174
+ const op = `getByIdAsTM(${id})`
175
+ const table = opt.table || this.cfg.table
176
+ const started = this.logStarted(op, table)
177
+ const [dbm] = await this.cfg.db.getByIds<DBM>(table, [id])
178
+ if (opt.raw) {
179
+ this.logResult(started, op, dbm, table)
180
+ return (dbm as any) || null
181
+ }
182
+ const bm = await this.dbmToBM(dbm, opt)
183
+ const tm = this.bmToTM(bm, opt)
184
+ this.logResult(started, op, tm, table)
185
+ return tm || null
186
+ }
187
+
162
188
  async getByIds(ids: ID[], opt: CommonDaoOptions = {}): Promise<Saved<BM>[]> {
163
189
  const op = `getByIds ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`
164
190
  const table = opt.table || this.cfg.table
@@ -256,8 +282,8 @@ export class CommonDao<
256
282
  /**
257
283
  * Pass `table` to override table
258
284
  */
259
- query(table?: string): RunnableDBQuery<BM, DBM, ID> {
260
- return new RunnableDBQuery<BM, DBM, ID>(this, table)
285
+ query(table?: string): RunnableDBQuery<BM, DBM, TM, ID> {
286
+ return new RunnableDBQuery<BM, DBM, TM, ID>(this, table)
261
287
  }
262
288
 
263
289
  async runQuery(q: DBQuery<DBM>, opt?: CommonDaoOptions): Promise<Saved<BM>[]> {
@@ -325,6 +351,29 @@ export class CommonDao<
325
351
  return { rows: dbms, ...queryResult }
326
352
  }
327
353
 
354
+ async runQueryAsTM(q: DBQuery<DBM>, opt?: CommonDaoOptions): Promise<TM[]> {
355
+ const { rows } = await this.runQueryExtendedAsTM(q, opt)
356
+ return rows
357
+ }
358
+
359
+ async runQueryExtendedAsTM(
360
+ q: DBQuery<DBM>,
361
+ opt: CommonDaoOptions = {},
362
+ ): Promise<RunQueryResult<TM>> {
363
+ q.table = opt.table || q.table
364
+ const op = `runQueryAsTM(${q.pretty()})`
365
+ const started = this.logStarted(op, q.table)
366
+ const { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
367
+ const partialQuery = !!q._selectedFieldNames
368
+ const tms =
369
+ partialQuery || opt.raw ? (rows as any[]) : this.bmsToTM(await this.dbmsToBM(rows, opt), opt)
370
+ this.logResult(started, op, tms, q.table)
371
+ return {
372
+ rows: tms,
373
+ ...queryResult,
374
+ }
375
+ }
376
+
328
377
  async runQueryCount(q: DBQuery<DBM>, opt: CommonDaoOptions = {}): Promise<number> {
329
378
  q.table = opt.table || q.table
330
379
  const op = `runQueryCount(${q.pretty()})`
@@ -979,6 +1028,50 @@ export class CommonDao<
979
1028
  return entities.map(entity => this.anyToDBM(entity, opt))
980
1029
  }
981
1030
 
1031
+ bmToTM(bm: undefined, opt?: CommonDaoOptions): TM | undefined
1032
+ bmToTM(bm?: Saved<BM>, opt?: CommonDaoOptions): TM
1033
+ bmToTM(bm?: Saved<BM>, opt?: CommonDaoOptions): TM | undefined {
1034
+ if (bm === undefined) return
1035
+
1036
+ // optimization: 1 validation is enough
1037
+ // Validate/convert BM
1038
+ // bm gets assigned to the new reference
1039
+ // bm = this.validateAndConvert(bm, this.cfg.bmSchema, DBModelType.BM, opt)
1040
+
1041
+ // BM > TM
1042
+ const tm = this.cfg.hooks!.beforeBMToTM!(bm as any)
1043
+
1044
+ // Validate/convert DBM
1045
+ return this.validateAndConvert(tm, this.cfg.tmSchema, DBModelType.TM, opt)
1046
+ }
1047
+
1048
+ bmsToTM(bms: Saved<BM>[], opt: CommonDaoOptions = {}): TM[] {
1049
+ // try/catch?
1050
+ return bms.map(bm => this.bmToTM(bm, opt))
1051
+ }
1052
+
1053
+ tmToBM(tm: undefined, opt?: CommonDaoOptions): undefined
1054
+ tmToBM(tm?: TM, opt?: CommonDaoOptions): BM
1055
+ tmToBM(tm?: TM, opt: CommonDaoOptions = {}): BM | undefined {
1056
+ if (!tm) return
1057
+
1058
+ // optimization: 1 validation is enough
1059
+ // Validate/convert TM
1060
+ // bm gets assigned to the new reference
1061
+ // tm = this.validateAndConvert(tm, this.cfg.tmSchema, DBModelType.TM, opt)
1062
+
1063
+ // TM > BM
1064
+ const bm = this.cfg.hooks!.beforeTMToBM!(tm) as BM
1065
+
1066
+ // Validate/convert BM
1067
+ return this.validateAndConvert<BM>(bm, this.cfg.bmSchema, DBModelType.BM, opt)
1068
+ }
1069
+
1070
+ tmsToBM(tms: TM[], opt: CommonDaoOptions = {}): BM[] {
1071
+ // try/catch?
1072
+ return tms.map(tm => this.tmToBM(tm, opt))
1073
+ }
1074
+
982
1075
  /**
983
1076
  * Returns *converted value*.
984
1077
  * Validates (unless `skipValidation=true` passed).
@@ -987,8 +1080,8 @@ export class CommonDao<
987
1080
  */
988
1081
  validateAndConvert<IN, OUT = IN>(
989
1082
  obj: Partial<IN>,
990
- schema: ObjectSchemaTyped<IN> | AjvSchema<IN> | undefined,
991
- modelType?: DBModelType | string,
1083
+ schema: ObjectSchemaTyped<IN> | AjvSchema<IN> | ZodSchema<IN> | undefined,
1084
+ modelType: DBModelType,
992
1085
  opt: CommonDaoOptions = {},
993
1086
  ): OUT {
994
1087
  // `raw` option completely bypasses any processing
@@ -1023,10 +1116,15 @@ export class CommonDao<
1023
1116
  const table = opt.table || this.cfg.table
1024
1117
  const objectName = table + (modelType || '')
1025
1118
 
1026
- let error: JoiValidationError | AjvValidationError | undefined
1119
+ let error: JoiValidationError | AjvValidationError | ZodValidationError<IN> | undefined
1027
1120
  let convertedValue: any
1028
1121
 
1029
- if (schema instanceof AjvSchema) {
1122
+ if (schema instanceof ZodSchema) {
1123
+ // Zod schema
1124
+ const vr = zSafeValidate(obj as IN, schema)
1125
+ error = vr.error
1126
+ convertedValue = vr.data
1127
+ } else if (schema instanceof AjvSchema) {
1030
1128
  // Ajv schema
1031
1129
  convertedValue = obj // because Ajv mutates original object
1032
1130