dobo 2.38.0 → 2.39.1

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 (160) hide show
  1. package/.bootorder +0 -0
  2. package/.github/FUNDING.yml +0 -0
  3. package/.github/workflows/repo-lockdown.yml +0 -0
  4. package/.jsdoc.conf.json +0 -0
  5. package/LICENSE +0 -0
  6. package/README.md +0 -0
  7. package/docs/Dobo.html +0 -0
  8. package/docs/DoboAction.html +0 -0
  9. package/docs/DoboAdapter.html +0 -0
  10. package/docs/DoboConnection.html +0 -0
  11. package/docs/DoboFeature.html +0 -0
  12. package/docs/DoboModel.html +0 -0
  13. package/docs/data/search.json +0 -0
  14. package/docs/extend_bajo_hook-docs.js.html +0 -0
  15. package/docs/external-Tools.html +0 -0
  16. package/docs/fonts/Inconsolata-Regular.ttf +0 -0
  17. package/docs/fonts/OpenSans-Regular.ttf +0 -0
  18. package/docs/fonts/WorkSans-Bold.ttf +0 -0
  19. package/docs/global.html +0 -0
  20. package/docs/index.html +0 -0
  21. package/docs/index.js.html +0 -0
  22. package/docs/lib_factory_action.js.html +0 -0
  23. package/docs/lib_factory_adapter.js.html +0 -0
  24. package/docs/lib_factory_connection.js.html +0 -0
  25. package/docs/lib_factory_feature.js.html +0 -0
  26. package/docs/lib_factory_model.js.html +0 -0
  27. package/docs/lib_factory_model_build.js.html +0 -0
  28. package/docs/lib_factory_model_clear-record.js.html +0 -0
  29. package/docs/lib_factory_model_count-record.js.html +0 -0
  30. package/docs/lib_factory_model_create-record.js.html +0 -0
  31. package/docs/lib_factory_model_drop.js.html +0 -0
  32. package/docs/lib_factory_model_exists.js.html +0 -0
  33. package/docs/lib_factory_model_find-all-record.js.html +0 -0
  34. package/docs/lib_factory_model_find-one-record.js.html +0 -0
  35. package/docs/lib_factory_model_find-record.js.html +0 -0
  36. package/docs/lib_factory_model_get-record.js.html +0 -0
  37. package/docs/lib_factory_model_helper.js.html +0 -0
  38. package/docs/lib_factory_model_remove-record.js.html +0 -0
  39. package/docs/lib_factory_model_sanitize-body.js.html +0 -0
  40. package/docs/lib_factory_model_sanitize-fixture.js.html +0 -0
  41. package/docs/lib_factory_model_sanitize-record.js.html +0 -0
  42. package/docs/lib_factory_model_update-record.js.html +0 -0
  43. package/docs/lib_factory_model_upsert-record.js.html +0 -0
  44. package/docs/lib_factory_model_validate.js.html +0 -0
  45. package/docs/lib_helper.js.html +0 -0
  46. package/docs/module-Helper.html +0 -0
  47. package/docs/module-Helper_Model.html +0 -0
  48. package/docs/module-Hook.html +0 -0
  49. package/docs/scripts/core.js +0 -0
  50. package/docs/scripts/core.min.js +0 -0
  51. package/docs/scripts/resize.js +0 -0
  52. package/docs/scripts/search.js +0 -0
  53. package/docs/scripts/search.min.js +5 -5
  54. package/docs/scripts/third-party/Apache-License-2.0.txt +0 -0
  55. package/docs/scripts/third-party/fuse.js +0 -0
  56. package/docs/scripts/third-party/hljs-line-num-original.js +0 -0
  57. package/docs/scripts/third-party/hljs-line-num.js +0 -0
  58. package/docs/scripts/third-party/hljs-original.js +0 -0
  59. package/docs/scripts/third-party/hljs.js +0 -0
  60. package/docs/scripts/third-party/popper.js +0 -0
  61. package/docs/scripts/third-party/tippy.js +0 -0
  62. package/docs/scripts/third-party/tocbot.js +0 -0
  63. package/docs/scripts/third-party/tocbot.min.js +0 -0
  64. package/docs/static/bitcoin.jpeg +0 -0
  65. package/docs/static/home.md +0 -0
  66. package/docs/static/logo-ecosystem.png +0 -0
  67. package/docs/static/logo.png +0 -0
  68. package/docs/styles/clean-jsdoc-theme-base.css +0 -0
  69. package/docs/styles/clean-jsdoc-theme-dark.css +0 -0
  70. package/docs/styles/clean-jsdoc-theme-light.css +0 -0
  71. package/docs/styles/clean-jsdoc-theme-scrollbar.css +0 -0
  72. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +0 -0
  73. package/docs/styles/clean-jsdoc-theme.min.css +0 -0
  74. package/extend/bajo/hook.js +236 -236
  75. package/extend/bajo/intl/en-US.json +0 -0
  76. package/extend/bajo/intl/id.json +0 -0
  77. package/extend/bajoCli/applet/aggregate.js +0 -0
  78. package/extend/bajoCli/applet/clear-record.js +0 -0
  79. package/extend/bajoCli/applet/connection.js +0 -0
  80. package/extend/bajoCli/applet/count-record.js +0 -0
  81. package/extend/bajoCli/applet/create-record.js +0 -0
  82. package/extend/bajoCli/applet/find-record.js +0 -0
  83. package/extend/bajoCli/applet/get-record.js +0 -0
  84. package/extend/bajoCli/applet/histogram.js +0 -0
  85. package/extend/bajoCli/applet/lib/post-process.js +0 -0
  86. package/extend/bajoCli/applet/model.js +0 -0
  87. package/extend/bajoCli/applet/rebuild-model.js +0 -0
  88. package/extend/bajoCli/applet/remove-record.js +0 -0
  89. package/extend/bajoCli/applet/update-record.js +0 -0
  90. package/extend/bajoCli/applet.js +0 -0
  91. package/extend/dobo/adapter/memory.js +175 -175
  92. package/extend/dobo/feature/created-at.js +0 -0
  93. package/extend/dobo/feature/dt.js +0 -0
  94. package/extend/dobo/feature/image.js +0 -0
  95. package/extend/dobo/feature/immutable.js +0 -0
  96. package/extend/dobo/feature/removed-at.js +0 -0
  97. package/extend/dobo/feature/unique.js +0 -0
  98. package/extend/dobo/feature/updated-at.js +0 -0
  99. package/extend/waibuMpa/route/attachment/@model/@id/@field/@file.js +0 -0
  100. package/extend/waibuStatic/virtual.json +0 -0
  101. package/index.js +0 -0
  102. package/lib/factory/action.js +287 -287
  103. package/lib/factory/adapter.js +1055 -1055
  104. package/lib/factory/connection.js +109 -109
  105. package/lib/factory/feature.js +52 -52
  106. package/lib/factory/model/aggregate.js +23 -23
  107. package/lib/factory/model/build.js +25 -25
  108. package/lib/factory/model/bulk-create-record.js +36 -36
  109. package/lib/factory/model/clear-record.js +30 -30
  110. package/lib/factory/model/count-record.js +42 -42
  111. package/lib/factory/model/create-attachment.js +36 -36
  112. package/lib/factory/model/create-record.js +49 -49
  113. package/lib/factory/model/drop.js +26 -26
  114. package/lib/factory/model/exists.js +26 -26
  115. package/lib/factory/model/find-all-record.js +112 -112
  116. package/lib/factory/model/find-attachment.js +28 -28
  117. package/lib/factory/model/find-one-record.js +38 -38
  118. package/lib/factory/model/find-record.js +82 -82
  119. package/lib/factory/model/get-attachment.js +15 -15
  120. package/lib/factory/model/get-record.js +52 -52
  121. package/lib/factory/model/helper.js +575 -576
  122. package/lib/factory/model/histogram.js +23 -23
  123. package/lib/factory/model/list-attachment.js +40 -40
  124. package/lib/factory/model/load-fixtures.js +63 -63
  125. package/lib/factory/model/remove-attachment.js +24 -24
  126. package/lib/factory/model/remove-record.js +44 -44
  127. package/lib/factory/model/sanitize-body.js +57 -57
  128. package/lib/factory/model/sanitize-fixture.js +56 -56
  129. package/lib/factory/model/sanitize-record.js +80 -80
  130. package/lib/factory/model/transaction.js +15 -15
  131. package/lib/factory/model/update-attachment.js +9 -9
  132. package/lib/factory/model/update-record.js +58 -58
  133. package/lib/factory/model/upsert-record.js +71 -71
  134. package/lib/factory/model/validate.js +0 -0
  135. package/lib/factory/model.js +503 -503
  136. package/lib/helper.js +463 -463
  137. package/package.json +1 -1
  138. package/test/e2e/_run.js +12 -12
  139. package/test/e2e/e2e-factory-process.test.js +25 -25
  140. package/test/e2e/e2e-model-adapter-process.test.js +52 -52
  141. package/test/integration/aspect-01-start-connections.test.js +33 -33
  142. package/test/integration/aspect-02-query-regex.test.js +34 -34
  143. package/test/integration/aspect-03-model-adapter-flow.test.js +88 -88
  144. package/test/unit/_stub.js +244 -244
  145. package/test/unit/dobo-action.test.js +58 -58
  146. package/test/unit/dobo-connection.test.js +37 -37
  147. package/test/unit/dobo-core.test.js +136 -136
  148. package/test/unit/dobo-feature.test.js +25 -25
  149. package/test/unit/dobo-model.test.js +115 -115
  150. package/test/unit/helper-sanitize-all.test.js +40 -40
  151. package/test/unit/helper-sanitize-ref.test.js +64 -64
  152. package/wiki/APPLETS.md +0 -0
  153. package/wiki/CHANGES.md +477 -469
  154. package/wiki/CONFIG.md +0 -0
  155. package/wiki/CONTRIBUTING.md +0 -0
  156. package/wiki/DEV-GUIDE.md +0 -0
  157. package/wiki/ECOSYSTEM.md +0 -0
  158. package/wiki/GETTING-STARTED.md +0 -0
  159. package/wiki/QUERY-LANGUAGE.md +0 -0
  160. package/wiki/USER-GUIDE.md +0 -0
@@ -1,1055 +1,1055 @@
1
- import { ulid } from 'ulid'
2
- import { v4 as uuidv4, v7 as uuidv7 } from 'uuid'
3
- import crypto from 'crypto'
4
-
5
- const defIdField = {
6
- name: '_id',
7
- type: 'string',
8
- maxLength: 50,
9
- required: true,
10
- index: 'primary'
11
- }
12
-
13
- /**
14
- * @external Tools
15
- * @see {@link https://ardhi.github.io/bajo/Tools.html|Bajo Tools}
16
- */
17
-
18
- /**
19
- * @typedef TIdField
20
- * @type {object}
21
- * @memberof DoboAdapter
22
- * @property {string} [name='_id'] - The name of the ID field.
23
- * @property {string} [type='string'] - The data type of the ID field.
24
- * @property {number} [maxLength=50] - The maximum length of the ID field.
25
- * @property {boolean} [required=true] - Indicates if the ID field is required.
26
- * @property {string} [index='primary'] - The index type of the ID field.
27
- */
28
-
29
- /**
30
- * @typedef TSupport
31
- * @memberof DoboAdapter
32
- * @type {object}
33
- * @property {object} [propType={}] - An object indicating support for various property types.
34
- * @property {boolean} [propType.object=false] - Indicates if object property type is supported.
35
- * @property {boolean} [propType.array=false] - Indicates if array property type is supported.
36
- * @property {boolean} [propType.datetime=true] - Indicates if datetime property type is supported.
37
- * @property {boolean} [search=false] - Indicates if search functionality is supported.
38
- * @property {boolean} [uniqueIndex=false] - Indicates if unique index functionality is supported.
39
- * @property {boolean} [nullableField=true] - Indicates if nullable fields are supported.
40
- * @property {boolean} [transaction=false] - Indicates if transaction functionality is supported.
41
- */
42
-
43
- /**
44
- * Adapter factory function.
45
- *
46
- * @async
47
- * @returns {Promise<DoboAdapter>}
48
- */
49
- async function adapterFactory () {
50
- const { Tools } = this.app.baseClass
51
- const { pick, cloneDeep, has, uniq, without, isEmpty, omit, isFunction, camelCase, last } = this.app.lib._
52
- const { isSet } = this.app.lib.aneka
53
- const { runHook } = this.app.bajo
54
-
55
- /**
56
- * DoboAdapter class serves as a base class for all database adapters in the Dobo framework. It provides common functionality for managing models, records, and database operations.
57
- * Child classes should implement the abstract methods to provide specific database functionality.
58
- *
59
- * @class
60
- * @extends external:Tools
61
- */
62
- class DoboAdapter extends Tools {
63
- /**
64
- * Constructor.
65
- */
66
- constructor (plugin, name, options = {}) {
67
- super(plugin)
68
-
69
- /**
70
- * Adapter name
71
- * @type {string}
72
- */
73
- this.name = name
74
-
75
- /**
76
- * ID field configuration
77
- * @type {DoboAdapter.TIdField}
78
- */
79
- this.idField = cloneDeep(defIdField)
80
- this.propertyType = {}
81
-
82
- /**
83
- * Support configuration for the adapter
84
- * @type {DoboAdapter.TSupport}
85
- */
86
- this.support = {
87
- propType: {
88
- object: false,
89
- array: false,
90
- datetime: true
91
- },
92
- search: false,
93
- uniqueIndex: false,
94
- nullableField: true,
95
- transaction: false
96
- }
97
-
98
- /**
99
- * Indicates whether to use UTC for datetime fields
100
- * @type {boolean}
101
- */
102
- this.useUtc = false
103
-
104
- /**
105
- * Maximum chunk size for bulk operations
106
- * @type {number}
107
- */
108
- this.maxChunkSize = 500
109
-
110
- /**
111
- * Indicates whether the adapter uses in-memory storage
112
- * @type {boolean}
113
- */
114
- this.memory = false
115
-
116
- /**
117
- * Adapter options
118
- * @type {object}
119
- */
120
- this.options = options
121
- }
122
-
123
- /**
124
- * Sanitize connection object
125
- * @async
126
- * @method
127
- * @param {Object} conn - Connection object
128
- * @returns {Promise<void>}
129
- */
130
- async sanitizeConnection (conn) {
131
- conn.proto = conn.proto ?? 'http' // used by adapter that use url based connection
132
- conn.memory = false
133
- }
134
-
135
- /**
136
- * Sanitizes the body of a record before creating or updating it. It ensures that all required fields
137
- * are present and have valid values, and converts data types as necessary.
138
- * @param {DoboModel} model - The model instance for which the body is being sanitized
139
- * @param {object} body - The body of the record to be sanitized
140
- * @param {boolean} [partial=false] - Indicates whether to perform a partial update
141
- * @returns {object} - Sanitized body
142
- */
143
- sanitizeBody (model, body = {}, partial) {
144
- const { keys, pick } = this.app.lib._
145
- const item = cloneDeep(body)
146
- let newId = false
147
- if (has(item, 'id') && this.idField.name !== 'id') {
148
- item[this.idField.name] = item.id
149
- newId = true
150
- }
151
- for (const prop of model.getNonVirtualProperties()) {
152
- if (item[prop.name] === 'null') item[prop.name] = null
153
- if (!isSet(item[prop.name]) && !this.support.nullableField) {
154
- switch (prop.type) {
155
- case 'datetime': item[prop.name] = new Date(0); break
156
- case 'float':
157
- case 'double': item[prop.name] = 0; break
158
- case 'string':
159
- case 'text': item[prop.name] = ''; break
160
- case 'object': item[prop.name] = {}; break
161
- case 'array': item[prop.name] = []; break
162
- }
163
- }
164
- if (isSet(item[prop.name]) && !this.support.propType[prop.type]) {
165
- if (prop.type === 'datetime') item[prop.name] = item[prop.name].toISOString()
166
- else if (['object', 'array'].includes(prop.type)) item[prop.name] = JSON.stringify(item[prop.name])
167
- }
168
- }
169
- const result = partial ? pick(item, keys(body)) : item
170
- if (newId) delete result.id
171
- return result
172
- }
173
-
174
- /**
175
- * Sanitizes a record retrieved from the database, converting data types as necessary
176
- * and ensuring that the record conforms to the model's schema.
177
- * @param {DoboModel} model - The model instance for which the record is being sanitized
178
- * @param {object} [record={}] - The record retrieved from the database
179
- * @param {object} [options={}] - Additional options for sanitization
180
- * @returns {object} - Sanitized record
181
- */
182
- sanitizeRecord (model, record = {}, options = {}) {
183
- const { dayjs } = this.app.lib
184
- const { isString } = this.app.lib._
185
- const item = { ...record }
186
- if (has(item, this.idField.name) && this.idField.name !== 'id') {
187
- item.id = item[this.idField.name]
188
- delete item[this.idField.name]
189
- }
190
- for (const prop of model.properties) {
191
- if (isSet(item[prop.name])) {
192
- if (!this.support.propType[prop.type]) {
193
- try {
194
- if (prop.type === 'datetime') {
195
- const dt = this.useUtc ? dayjs.utc(item[prop.name]) : dayjs(item[prop.name])
196
- item[prop.name] = dt.toDate()
197
- } else if (isString(item[prop.name]) && ['object', 'array'].includes(prop.type)) item[prop.name] = JSON.parse(item[prop.name])
198
- } catch (err) {
199
- item[prop.name] = null
200
- }
201
- }
202
- if (prop.type === 'datetime' && isString(item[prop.name])) {
203
- const dt = this.useUtc ? dayjs.utc(item[prop.name]) : dayjs(item[prop.name])
204
- item[prop.name] = dt.toDate()
205
- }
206
- if (prop.type === 'boolean' && isSet(item[prop.name])) item[prop.name] = Boolean(item[prop.name])
207
- }
208
- }
209
- return item
210
- }
211
-
212
- /**
213
- * Utility method to get the real fields of a model, excluding virtual fields.
214
- * This is useful for operations that require only the actual stored properties of a model.
215
- * @param {*} model
216
- * @returns {string[]} - Array of real field names
217
- */
218
- getRealFields (model) {
219
- return model.getProperties({ noVirtual: true, namesOnly: true })
220
- }
221
-
222
- /**
223
- * Utility method to get the virtual fields of a model.
224
- * This is useful for operations that need to work with computed or derived properties.
225
- * @param {DoboModel} model - The model instance
226
- * @returns {string[]} - Array of virtual field names
227
- */
228
- getVirtualFields (model) {
229
- return model.getVirtualProperties({ namesOnly: true })
230
- }
231
-
232
- /**
233
- * Get returning fields for a model based on the provided options. If the adapter supports returning fields,
234
- * it will return the specified fields or all model properties. It ensures that the ID field is always
235
- * included in the returned fields.
236
- * @param {DoboModel} model - The model instance for which to get the returning fields
237
- * @param {object} options - Options that may include the fields to return
238
- * @returns {string[]} - Array of field names to be returned
239
- */
240
- _getReturningFields (model, options = {}) {
241
- const { fields = [] } = options
242
- if (!this.support.returning) return []
243
- let items = fields.length > 0 ? [...fields] : model.properties.map(prop => prop.name)
244
- if (!items.includes(this.idField.name)) items.unshift(this.idField.name)
245
- if (this.idField.name !== 'id') items = without(items, ['id'])
246
- return uniq(items)
247
- }
248
-
249
- /**
250
- * Attaches hooks to the model for various operations. It runs the appropriate hooks
251
- * before and after the specified operation, allowing for custom behavior to be injected
252
- * into the model's lifecycle.
253
- * @internal
254
- * @async
255
- * @method
256
- * @param {string} name - The name of the hook
257
- * @param {DoboModel} model - The model instance to which the hook is being attached
258
- * @param {...any} args - Additional arguments to be passed to the hook
259
- */
260
- async _attachHook (name, model, ...args) {
261
- const { ns } = this.app.dobo
262
- const { kebabCase } = this.app.lib._
263
- const options = last(args)
264
- if (!options.noAdapterHook) {
265
- const prefix = kebabCase(name).split('-')[0]
266
- await runHook(`${ns}.adapter:${prefix}Any`, model, options)
267
- await runHook(`${ns}.adapter:${name}`, model, ...args)
268
- await runHook(`${ns}.adapter.${camelCase(model.name)}:${name}`, ...args)
269
- }
270
- }
271
-
272
- /**
273
- * Checks the uniqueness of fields with a unique index.
274
- * @async
275
- * @method
276
- * @internal
277
- * @param {DoboModel} model - The model instance to check
278
- * @param {object} body - The data to be checked for uniqueness
279
- * @param {object} options - Additional options, including the action being performed
280
- * @returns {Promise<void>} - Resolves if unique, throws an error if not
281
- */
282
- _checkUnique = async (model, body = {}, options = {}) => {
283
- const { isSet } = this.app.lib.aneka
284
- const { filter, map, isEmpty, forOwn } = this.app.lib._
285
- const indexes = filter(model.indexes ?? [], idx => idx.type === 'unique')
286
- for (const index of indexes) {
287
- const query = {}
288
- for (const field of index.fields) {
289
- if (isSet(body[field])) query[field] = body[field]
290
- }
291
- if (isEmpty(query)) continue
292
- const { data } = await model.findOneRecord({ query }, options)
293
- if (!isEmpty(data)) {
294
- if (['updateRecord', 'upsertRecord'].includes(options.action)) {
295
- let eq = true
296
- forOwn(query, (v, k) => {
297
- if (data[k] !== v) eq = false
298
- })
299
- if (!eq) continue
300
- }
301
- const error = this.app.dobo.t('uniqueConstraintError')
302
- const details = map(index.fields, field => {
303
- return { field, error }
304
- })
305
- throw this.app.dobo.error(error, { details, body })
306
- }
307
- }
308
- }
309
-
310
- // Internal calls that will be called by model
311
-
312
- /**
313
- * Wrapper for the `modelExists` method, called internally by `model` to make sure
314
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
315
- *
316
- * @internal
317
- * @async
318
- * @method
319
- * @param {DoboModel} model
320
- * @param {object} options
321
- * @returns {Promise<boolean>}
322
- */
323
- async _modelExists (model, options = {}) {
324
- return await this.modelExists(model, options)
325
- }
326
-
327
- /**
328
- * Wrapper for the `buildModel` method, called internally by `model` to make sure
329
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
330
- * @internal
331
- * @async
332
- * @method
333
- * @param {DoboModel} model
334
- * @param {object} options
335
- * @returns {Promise<object>}
336
- */
337
- async _buildModel (model, options = {}) {
338
- return await this.buildModel(model, options)
339
- }
340
-
341
- /**
342
- * Wrapper for the `dropModel` method, called internally by `model` to make sure
343
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
344
- * @internal
345
- * @async
346
- * @method
347
- * @param {DoboModel} model
348
- * @param {object} options
349
- * @returns {Promise<object>}
350
- */
351
- async _dropModel (model, options = {}) {
352
- return await this.dropModel(model, options)
353
- }
354
-
355
- /**
356
- * Prepares the body of a record for creation by populating default values for properties
357
- * that are not set. It handles various types of default values, including functions,
358
- * special strings (like 'now', 'uuid', etc.), and static values.
359
- * @internal
360
- * @async
361
- * @method
362
- * @param {DoboModel} model - The model instance for which the body is being prepared
363
- * @param {object} body - The data to be prepared for creation
364
- * @param {object} options - Additional options that may affect the preparation
365
- * @returns {Promise<object>} - The prepared body with default values populated
366
- */
367
- async _prepBodyForCreate (model, body = {}, options = {}) {
368
- const { callHandler } = this.app.bajo
369
- const { isSet, generateId } = this.app.lib.aneka
370
- for (const prop of model.getProperties({ noVirtual: true })) {
371
- if (isSet(prop.default) && (!options.noDefault) && (!isSet(body[prop.name]) || body[prop.name] === prop.default)) {
372
- if (isFunction(prop.default)) body[prop.name] = await prop.default.call(model)
373
- else if (typeof prop.default !== 'string') body[prop.name] = prop.default
374
- else {
375
- if (['now'].includes(prop.default) && prop.type === 'datetime') {
376
- body[prop.name] = new Date()
377
- } else if (['uuid', 'uuidv4'].includes(prop.default) && prop.type === 'string') {
378
- body[prop.name] = uuidv4().slice(0, prop.maxLength)
379
- } else if (prop.default === 'uuidv7' && prop.type === 'string') {
380
- body[prop.name] = uuidv7().slice(0, prop.maxLength)
381
- } else if (prop.default === 'ulid' && prop.type === 'string') {
382
- body[prop.name] = ulid().slice(0, prop.maxLength)
383
- } else if (prop.default === 'generateid' && prop.type === 'string') {
384
- body[prop.name] = generateId()
385
- } else if (prop.default.startsWith('handler:')) {
386
- const [, ...args] = prop.default.split(':')
387
- if (args.length > 0) body[prop.name] = await callHandler(args.join(':'))
388
- } else if (prop.default.startsWith('md5:') && prop.type === 'string') {
389
- const [, field] = prop.default.split(':')
390
- const fields = field.split(',')
391
- if (model.properties.filter(item => fields.includes(item.name)).length === fields.length) {
392
- const values = fields.map(f => body[f])
393
- body[prop.name] = crypto.createHash('md5').update(values.join(':')).digest('hex')
394
- }
395
- } else {
396
- body[prop.name] = prop.default
397
- }
398
- }
399
- }
400
- }
401
- return pick(body, this.getRealFields(model))
402
- }
403
-
404
- /**
405
- * Prepares the ID for a record before creation. It generates an ID if it is not set in the body.
406
- *
407
- * @internal
408
- * @async
409
- * @method
410
- * @param {DoboModel} model - The model instance for which the ID is being prepared
411
- * @param {object} body - The data containing the ID
412
- * @param {object} options - Additional options that may affect ID generation
413
- * @returns {Promise<void>} - Resolves when the ID has been prepared
414
- */
415
- async _prepIdForCreate (model, body = {}, options = {}) {
416
- const { isSet, generateId } = this.app.lib.aneka
417
- const { isFunction } = this.app.lib._
418
- const prop = model.properties.find(p => p.name === 'id')
419
- if (!isSet(body.id) && prop.type === 'string') {
420
- if (this.idGenerator) {
421
- if (['uuid', 'uuidv4'].includes(this.idGenerator)) body.id = uuidv4()
422
- else if (['uuidv7'].includes(this.idGenerator)) body.id = uuidv7()
423
- else if (this.idGenerator === 'generateId') body.id = generateId()
424
- else if (isFunction(this.idGenerator)) body.id = await this.idGenerator(model, body, options)
425
- }
426
- if (!body.id) body.id = ulid()
427
- body.id = body.id.slice(0, prop.maxLength)
428
- }
429
- }
430
-
431
- _injectMeta (result = {}, options = {}) {
432
- result.warnings = result.warnings ?? []
433
- result.warnings.push(...(options.warnings ?? []))
434
- }
435
-
436
- /**
437
- * Wrapper for the {@link DoboAdapter#createRecord} method, called internally by `model` to make sure
438
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
439
- * @internal
440
- * @async
441
- * @method
442
- * @param {DoboModel} model
443
- * @param {object} input
444
- * @param {object} options
445
- * @returns {Promise<object>}
446
- */
447
- async _createRecord (model, input = {}, options = {}) {
448
- const { isSet } = this.app.lib.aneka
449
- let body = await this._prepBodyForCreate(model, input, options)
450
- await this._prepIdForCreate(model, body, options)
451
- if (!options.noUniqueCheck) {
452
- if (!this.support.uniqueIndex) await this._checkUnique(model, body, options)
453
- }
454
- if (!options.noIdCheck && isSet(body.id)) {
455
- const resp = await this.getRecord(model, body.id, { noMagic: true })
456
- if (!isEmpty(resp.data)) throw this.plugin.error('recordExists%s%s', body.id, model.name)
457
- }
458
- body = this.sanitizeBody(model, body)
459
-
460
- await this._attachHook('beforeCreateRecord', model, body, options)
461
- const result = await this.createRecord(model, body, options)
462
- await this._attachHook('afterCreateRecord', model, body, result, options)
463
-
464
- if (options.noResult) return
465
- result.data = this.sanitizeRecord(model, result.data, options)
466
- this._injectMeta(result, options)
467
- return result
468
- }
469
-
470
- /**
471
- * Wrapper for the {@link DoboAdapter#bulkCreateRecord} method, called internally by `model` to make sure
472
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
473
- * @internal
474
- * @async
475
- * @method
476
- * @param {DoboModel} model
477
- * @param {Array<object>} bodies
478
- * @param {object} options
479
- * @returns {Promise<void>}
480
- */
481
- async _bulkCreateRecord (model, bodies = [], options = {}) {
482
- const { chunk } = this.app.lib._
483
- let { chunkSize = this.maxChunkSize } = options
484
- if (chunkSize > this.maxChunkSize) chunkSize = this.maxChunkSize
485
- for (const idx in bodies) {
486
- const body = await this._prepBodyForCreate(model, bodies[idx], options)
487
- await this._prepIdForCreate(model, body, options)
488
- bodies[idx] = this.sanitizeBody(model, body)
489
- }
490
-
491
- await this._attachHook('beforeBulkCreateRecord', model, bodies, options)
492
- const items = chunk(bodies, chunkSize)
493
- for (const item of items) {
494
- await this.bulkCreateRecord(model, item, options)
495
- }
496
- await this._attachHook('afterBulkCreateRecord', model, bodies, [], options)
497
- }
498
-
499
- /**
500
- * Wrapper for the {@link DoboAdapter#getRecord} method, called internally by `model` to make sure
501
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
502
- * @internal
503
- * @async
504
- * @method
505
- * @param {DoboModel} model
506
- * @param {string|number} id
507
- * @param {object} options
508
- * @returns {Promise<object>}
509
- */
510
- async _getRecord (model, id, options = {}) {
511
- await this._attachHook('beforeGetRecord', model, id, options)
512
- const result = await this.getRecord(model, id, options)
513
- await this._attachHook('afterGetRecord', model, id, result, options)
514
-
515
- if (isEmpty(result.data) && options.throwNotFound) throw this.plugin.error('recordNotFound%s%s', id, model.name)
516
- result.data = this.sanitizeRecord(model, result.data, options)
517
- this._injectMeta(result, options)
518
- return result
519
- }
520
-
521
- /**
522
- * Wrapper for the {@link DoboAdapter#updateRecord} method, called internally by `model` to make sure
523
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
524
- * @internal
525
- * @async
526
- * @method
527
- * @param {DoboModel} model
528
- * @param {string|number} id
529
- * @param {object} input
530
- * @param {object} options
531
- * @returns {Promise<object>}
532
- */
533
- async _updateRecord (model, id, input = {}, options = {}) {
534
- let body = omit(input, this.getVirtualFields(model))
535
- if (!options.noUniqueCheck) {
536
- if (!this.support.uniqueIndex) await this._checkUnique(model, body, options)
537
- }
538
- if (!options._data) {
539
- const resp = await this.getRecord(model, id, { noMagic: true })
540
- if (!resp.data) throw this.plugin.error('recordNotFound%s%s', id, model.name)
541
- options._data = resp.data
542
- }
543
- body = this.sanitizeBody(model, body, true)
544
- delete body.id
545
-
546
- await this._attachHook('beforeUpdateRecord', model, id, body, options)
547
- const result = await this.updateRecord(model, id, body, options)
548
- await this._attachHook('afterUpdateRecord', model, id, body, result, options)
549
-
550
- if (options.noResult) return
551
- result.oldData = this.sanitizeRecord(model, result.oldData, options)
552
- result.data = this.sanitizeRecord(model, result.data, options)
553
- this._injectMeta(result, options)
554
- return result
555
- }
556
-
557
- /**
558
- * Upserts a record for the given model.
559
- * This method will only run if child adapter does not implement {@link DoboAdapter#upsertRecord}.
560
- *
561
- * @internal
562
- * @async
563
- * @method
564
- * @param {DoboModel} model - The model instance for which the record is being upserted
565
- * @param {object} input - The input data for the upsert
566
- * @param {object} options - Additional options that may affect record upserting
567
- * @returns {Promise<object>} - The result of the record upsert
568
- */
569
- async _upsertRecord (model, input = {}, options = {}) {
570
- let body = omit(input, this.getVirtualFields(model))
571
- if (!options.noUniqueCheck) {
572
- if (!this.support.uniqueIndex) await this._checkUnique(model, body, options)
573
- }
574
- if (isSet(body.id)) {
575
- if (!options._data) {
576
- const resp = await this.getRecord(model, body.id, { noMagic: true })
577
- if (!resp.data) throw this.plugin.error('recordNotFound%s%s', body.id, model.name)
578
- options._data = resp.data
579
- }
580
- }
581
- body = this.sanitizeBody(model, body)
582
-
583
- await this._attachHook('beforeUpsertRecord', model, body, options)
584
- const result = await this.upsertRecord(model, body, options)
585
- await this._attachHook('afterUpsertRecord', model, body, result, options)
586
-
587
- if (options.noResult) return
588
- if (result.oldData) result.oldData = this.sanitizeRecord(model, result.oldData, options)
589
- result.data = this.sanitizeRecord(model, result.data, options)
590
- this._injectMeta(result, options)
591
- return result
592
- }
593
-
594
- /**
595
- * Wrapper for the {@link DoboAdapter#removeRecord} method, called internally by `model` to make sure
596
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
597
- * @internal
598
- * @async
599
- * @method
600
- * @param {DoboModel} model
601
- * @param {string|number} id
602
- * @param {object} options
603
- * @returns {Promise<object>}
604
- */
605
- async _removeRecord (model, id, options = {}) {
606
- if (!options._data) {
607
- const resp = await this.getRecord(model, id, { noMagic: true })
608
- if (!resp.data) throw this.plugin.error('recordNotFound%s%s', id, model.name)
609
- options._data = resp.data
610
- }
611
-
612
- await this._attachHook('beforeRemoveRecord', model, id, options)
613
- const result = await this.removeRecord(model, id, options)
614
- await this._attachHook('afterRemoveRecord', model, id, result, options)
615
-
616
- if (options.noResult) return
617
- result.oldData = this.sanitizeRecord(model, result.oldData, options)
618
- this._injectMeta(result, options)
619
- return result
620
- }
621
-
622
- /**
623
- * Wrapper for the {@link DoboAdapter#clearRecord} method, called internally by `model` to make sure
624
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
625
- * @internal
626
- * @async
627
- * @method
628
- * @param {DoboModel} model
629
- * @param {object} options
630
- * @returns {Promise<object>}
631
- */
632
- async _clearRecord (model, options = {}) {
633
- await this._attachHook('beforeClearRecord', model, options)
634
- const result = await this.clearRecord(model, options)
635
- await this._attachHook('afterClearRecord', model, result, options)
636
-
637
- this._injectMeta(result, options)
638
- return result
639
- }
640
-
641
- /**
642
- * Wrapper for the {@link DoboAdapter#findRecord} method, called internally by `model` to make sure
643
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
644
- * @internal
645
- * @async
646
- * @method
647
- * @param {DoboModel} model
648
- * @param {object} filter
649
- * @param {object} options
650
- * @returns {Promise<object>}
651
- */
652
- async _findRecord (model, filter = {}, options = {}) {
653
- let result
654
- try {
655
- await this._attachHook('beforeFindRecord', model, filter, options)
656
- result = await this.findRecord(model, filter, options)
657
- await this._attachHook('afterFindRecord', model, filter, result, options)
658
- } catch (err) {
659
- if (!['_emptyColumnQuery', '_abortAction'].includes(err.message)) throw err
660
- result = {
661
- data: [],
662
- count: 0
663
- // warnings: [] // TODO: should generate warnings?
664
- }
665
- }
666
-
667
- for (const idx in result.data) {
668
- result.data[idx] = this.sanitizeRecord(model, result.data[idx], options)
669
- }
670
- this._injectMeta(result, options)
671
- return result
672
- }
673
-
674
- /**
675
- * Finds all records for the given model based on the provided filter.
676
- * This method will only run if child adapter does not implement {@link DoboAdapter#findAllRecord}.
677
- *
678
- * @internal
679
- * @async
680
- * @method
681
- * @param {DoboModel} model - The model instance for which the records are being found
682
- * @param {object} filter - The filter criteria for finding records
683
- * @param {object} options - Additional options that may affect record finding
684
- * @returns {Promise<object>} - The result of the record finding
685
- */
686
- async _findAllRecord (model, filter = {}, options = {}) {
687
- let result
688
- try {
689
- await this._attachHook('beforeFindAllRecord', model, filter, options)
690
- result = await this.findAllRecord(model, filter, options)
691
- await this._attachHook('afterFindAllRecord', model, filter, result, options)
692
- } catch (err) {
693
- if (err.message !== '_emptyColumnQuery') throw err
694
- result = {
695
- data: [],
696
- count: 0
697
- // warnings: [] // TODO: should generate warnings?
698
- }
699
- }
700
-
701
- for (const idx in result.data) {
702
- result.data[idx] = this.sanitizeRecord(model, result.data[idx], options)
703
- }
704
- this._injectMeta(result, options)
705
- return result
706
- }
707
-
708
- /**
709
- * Wrapper for the {@link DoboAdapter#countRecord} method, called internally by `model` to make sure
710
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
711
- * @internal
712
- * @async
713
- * @method
714
- * @param {DoboModel} model
715
- * @param {object} filter
716
- * @param {object} options
717
- * @returns {Promise<object>}
718
- */
719
- async _countRecord (model, filter = {}, options = {}) {
720
- let result
721
- try {
722
- await this._attachHook('beforeCountRecord', model, filter, options)
723
- result = await this.countRecord(model, filter, options)
724
- await this._attachHook('afterCountRecord', model, filter, result, options)
725
- } catch (err) {
726
- if (err.message !== '_emptyColumnQuery') throw err
727
- result = { data: 0 }
728
- }
729
-
730
- return result
731
- }
732
-
733
- /**
734
- * Wrapper for the {@link DoboAdapter#aggregate} method, called internally by `model` to make sure
735
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
736
- * @internal
737
- * @async
738
- * @method
739
- * @param {DoboModel} model
740
- * @param {object} filter
741
- * @param {object} params
742
- * @param {object} options
743
- * @returns {Promise<object>}
744
- */
745
- async _aggregate (model, filter = {}, params = {}, options = {}) {
746
- const fieldPropTypes = ['integer', 'smallint', 'float', 'double']
747
- const groupPropTypes = ['string', ...fieldPropTypes]
748
- this.app.dobo.checkAggregateParams(params)
749
- const { group, field } = params
750
-
751
- let prop = model.properties.find(p => p.name === group)
752
- if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), group)
753
- if (!groupPropTypes.includes(prop.type)) throw this.plugin.error('allowedPropType%s%s', group, groupPropTypes.join(', '))
754
-
755
- prop = model.properties.find(p => p.name === field)
756
- if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), field)
757
- // if (!fieldPropTypes.includes(prop.type)) throw this.plugin.error('allowedPropType%s%s', field, fieldPropTypes.join(', '))
758
-
759
- let result
760
- try {
761
- await this._attachHook('beforeAggregate', model, filter, params, options)
762
- result = await this.aggregate(model, filter, params, options)
763
- await this._attachHook('afterAggregate', model, filter, params, result, options)
764
- } catch (err) {
765
- if (err.message !== '_emptyColumnQuery') throw err
766
- result = { data: [] }
767
- }
768
-
769
- for (const idx in result.data) {
770
- result.data[idx] = this.sanitizeRecord(model, result.data[idx])
771
- }
772
- this._injectMeta(result, options)
773
- return result
774
- }
775
-
776
- /**
777
- * Wrapper for the {@link DoboAdapter#histogram} method, called internally by `model` to make sure
778
- * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
779
- * @internal
780
- * @async
781
- * @method
782
- * @param {DoboModel} model
783
- * @param {object} filter
784
- * @param {object} params
785
- * @param {object} options
786
- * @returns {Promise<object>}
787
- */
788
- async _histogram (model, filter = {}, params, options = {}) {
789
- // const fieldPropTypes = ['integer', 'smallint', 'float', 'double']
790
- const groupPropTypes = ['datetime', 'date']
791
- this.app.dobo.checkHistogramParams(params)
792
- const { group, field } = params
793
-
794
- let prop = model.properties.find(p => p.name === group)
795
- if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), group)
796
- if (!groupPropTypes.includes(prop.type)) throw this.plugin.error('allowedPropType%s%s', group, groupPropTypes.join(', '))
797
-
798
- prop = model.properties.find(p => p.name === field)
799
- if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), field)
800
-
801
- let result
802
- try {
803
- await this._attachHook('beforeHistogram', model, filter, params, options)
804
- result = await this.histogram(model, filter, params, options)
805
- await this._attachHook('afterHistogram', model, filter, params, result, options)
806
- } catch (err) {
807
- if (err.message !== '_emptyColumnQuery') throw err
808
- result = { data: [] }
809
- }
810
-
811
- for (const idx in result.data) {
812
- result.data[idx] = this.sanitizeRecord(model, result.data[idx])
813
- }
814
- this._injectMeta(result, options)
815
- return result
816
- }
817
-
818
- // Public calls that need to be implemented by child adapters
819
-
820
- /**
821
- * Connects to the database using the provided connection parameters.
822
- * @param {*} connection - The connection parameters for the database
823
- * @param {*} noRebuild - Flag indicating whether to skip rebuilding the database schema
824
- * @returns {Promise<void>} - Resolves when the connection is established
825
- */
826
- async connect (connection, noRebuild) {
827
- }
828
-
829
- /**
830
- * Checks if the model exists in the database.
831
- *
832
- * Must be implemented by child classes to provide specific database functionality for model existence checking,
833
- * or throw an error if the operation is not supported by the adapter.
834
- *
835
- * @param {DoboModel} model - The model instance to check for existence
836
- * @param {object} options - Additional options that may affect the existence check
837
- */
838
- async modelExists (model, options = {}) {
839
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'modelExists', this.name)
840
- }
841
-
842
- /**
843
- * Builds the model in the database.
844
- *
845
- * Must be implemented by child classes to provide specific database functionality for model building,
846
- * or throw an error if the operation is not supported by the adapter.
847
- *
848
- * @param {DoboModel} model - The model instance for which the record is being built
849
- * @param {object} options - Additional options that may affect model building
850
- */
851
- async buildModel (model, options = {}) {
852
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'buildModel', this.name)
853
- }
854
-
855
- /**
856
- * Drops the model from the database.
857
- *
858
- * Must be implemented by child classes to provide specific database functionality for dropping a model,
859
- * or throw an error if the operation is not supported by the adapter.
860
- *
861
- * @param {DoboModel} model - The model instance for which the record is being dropped
862
- * @param {object} options - Additional options that may affect record dropping
863
- */
864
- async dropModel (model, options = {}) {
865
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'dropModel', this.name)
866
- }
867
-
868
- /**
869
- * Creates a new record for the given model.
870
- *
871
- * Must be implemented by child classes to provide specific database functionality for record creation,
872
- * or throw an error if the operation is not supported by the adapter.
873
- *
874
- * @param {DoboModel} model - The model instance for which the record is being created
875
- * @param {object} input - The input data for the new record
876
- * @param {object} options - Additional options that may affect record creation
877
- * @returns {Promise<object>} - The result of the record creation
878
- */
879
- async createRecord (model, body = {}, options = {}) {
880
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'createRecord', this.name)
881
- }
882
-
883
- /**
884
- * Retrieves a record for the given model by its ID.
885
- *
886
- * Must be implemented by child classes to provide specific database functionality for record retrieval,
887
- * or throw an error if the operation is not supported by the adapter.
888
- *
889
- * @param {DoboModel} model - The model instance for which the record is being retrieved
890
- * @param {string|number} id - The ID of the record to retrieve
891
- * @param {object} options - Additional options that may affect record retrieval
892
- * @returns {Promise<object>} - The result of the record retrieval
893
- */
894
- async getRecord (model, id, options = {}) {
895
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'getRecord', this.name)
896
- }
897
-
898
- /**
899
- * Updates a record for the given model by its ID.
900
- *
901
- * Must be implemented by child classes to provide specific database functionality for record updating,
902
- * or throw an error if the operation is not supported by the adapter.
903
- *
904
- * @param {DoboModel} model - The model instance for which the record is being updated
905
- * @param {string|number} id - The ID of the record to update
906
- * @param {object} input - The input data for the update
907
- * @param {object} options - Additional options that may affect record updating
908
- * @returns {Promise<object>} - The result of the record update
909
- */
910
- async updateRecord (model, id, body = {}, options = {}) {
911
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'updateRecord', this.name)
912
- }
913
-
914
- /**
915
- * Removes a record for the given model by its ID.
916
- *
917
- * Must be implemented by child classes to provide specific database functionality for record removal,
918
- * or throw an error if the operation is not supported by the adapter.
919
- *
920
- * @param {DoboModel} model - The model instance for which the record is being removed
921
- * @param {string|number} id - The ID of the record to remove
922
- * @param {object} options - Additional options that may affect record removal
923
- * @returns {Promise<object>} - The result of the record removal
924
- */
925
- async removeRecord (model, id, options = {}) {
926
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'removeRecord', this.name)
927
- }
928
-
929
- /**
930
- * Clears all records for the given model.
931
- *
932
- * Must be implemented by child classes to provide specific database functionality for record clearing,
933
- * or throw an error if the operation is not supported by the adapter.
934
- *
935
- * @param {DoboModel} model - The model instance for which the records are being cleared
936
- * @param {object} options - Additional options that may affect record clearing
937
- * @returns {Promise<object>} - The result of the record clearing
938
- */
939
- async clearRecord (model, options = {}) {
940
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'clearRecord', this.name)
941
- }
942
-
943
- /**
944
- * Finds records for the given model based on the provided filter.
945
- *
946
- * Must be implemented by child classes to provide specific database functionality for record finding,
947
- * or throw an error if the operation is not supported by the adapter.
948
- *
949
- * @param {DoboModel} model - The model instance for which the records are being found
950
- * @param {object} filter - The filter criteria for finding records
951
- * @param {object} options - Additional options that may affect record finding
952
- * @returns {Promise<object>} - The result of the record finding
953
- */
954
- async findRecord (model, filter = {}, options = {}) {
955
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'findRecord', this.name)
956
- }
957
-
958
- /**
959
- * Bulk creates records for the given model.
960
- *
961
- * Must be implemented by child classes to provide specific database functionality for bulk record creation,
962
- * or throw an error if the operation is not supported by the adapter.
963
- *
964
- * @param {DoboModel} model - The model instance for which the records are being created
965
- * @param {Array<object>} bodies - The array of input data for the new records
966
- * @param {object} options - Additional options that may affect record creation
967
- * @returns {Promise<void>} - Resolves when the records have been created
968
- */
969
- async bulkCreateRecord (model, bodies = [], options = {}) {
970
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'bulkCreateRecord', this.name)
971
- }
972
-
973
- /**
974
- * Counts the records for the given model based on the provided filter.
975
- *
976
- * Must be implemented by child classes to provide specific database functionality for record counting,
977
- * or throw an error if the operation is not supported by the adapter.
978
- *
979
- * @param {DoboModel} model - The model instance for which the records are being counted
980
- * @param {object} filter - The filter criteria for counting records
981
- * @param {object} options - Additional options that may affect record counting
982
- * @returns {Promise<object>} - The result of the record counting
983
- */
984
- async countRecord (model, filter = {}, options = {}) {
985
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'countRecord', this.name)
986
- }
987
-
988
- /**
989
- * Creates an aggregate for the given model based on the provided filter and parameters.
990
- *
991
- * Must be implemented by child classes to provide specific database functionality for aggregate creation,
992
- * or throw an error if the operation is not supported by the adapter.
993
- *
994
- * @param {DoboModel} model - The model instance for which the aggregate is being created
995
- * @param {object} filter - The filter criteria for creating the aggregate
996
- * @param {object} params - The parameters for the aggregate creation
997
- * @param {object} options - Additional options that may affect aggregate creation
998
- * @returns {Promise<object>} - The result of the aggregate creation
999
- */
1000
- async aggregate (model, filter = {}, params = {}, options = {}) {
1001
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'aggregate', this.name)
1002
- }
1003
-
1004
- /**
1005
- * Creates a histogram for the given model based on the provided filter and parameters.
1006
- *
1007
- * Must be implemented by child classes to provide specific database functionality for histogram creation,
1008
- * or throw an error if the operation is not supported by the adapter.
1009
- *
1010
- * @param {DoboModel} model - The model instance for which the histogram is being created
1011
- * @param {object} filter - The filter criteria for creating the histogram
1012
- * @param {object} params - The parameters for the histogram creation
1013
- * @param {object} options - Additional options that may affect histogram creation
1014
- * @returns {Promise<object>} - The result of the histogram creation
1015
- */
1016
- async histogram (model, filter = {}, params = {}, options = {}) {
1017
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'histogram', this.name)
1018
- }
1019
-
1020
- /**
1021
- * Executes a transaction for the given model.
1022
- *
1023
- * Must be implemented by child classes to provide specific database functionality for transactions,
1024
- * or throw an error if the operation is not supported by the adapter.
1025
- *
1026
- * @param {DoboModel} model - The model instance for which the transaction is being executed
1027
- * @param {Function} handler - The transaction handler function
1028
- * @param {...any} args - Additional arguments for the transaction handler
1029
- */
1030
- async transaction (model, handler, ...args) {
1031
- throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'transaction', this.name)
1032
- }
1033
-
1034
- /**
1035
- * Disposes of the adapter, performing any necessary cleanup operations.
1036
- *
1037
- * @returns {Promise<void>} - Resolves when the adapter has been disposed
1038
- */
1039
- async dispose () {
1040
- await super.dispose()
1041
- }
1042
- }
1043
-
1044
- class DoboNullAdapter extends DoboAdapter {
1045
- constructor (plugin, name = 'null', options = {}) {
1046
- super(plugin, name, options)
1047
- this.memory = true
1048
- }
1049
- }
1050
-
1051
- this.app.baseClass.DoboAdapter = DoboAdapter
1052
- this.app.baseClass.DoboNullAdapter = DoboNullAdapter
1053
- }
1054
-
1055
- export default adapterFactory
1
+ import { ulid } from 'ulid'
2
+ import { v4 as uuidv4, v7 as uuidv7 } from 'uuid'
3
+ import crypto from 'crypto'
4
+
5
+ const defIdField = {
6
+ name: '_id',
7
+ type: 'string',
8
+ maxLength: 50,
9
+ required: true,
10
+ index: 'primary'
11
+ }
12
+
13
+ /**
14
+ * @external Tools
15
+ * @see {@link https://ardhi.github.io/bajo/Tools.html|Bajo Tools}
16
+ */
17
+
18
+ /**
19
+ * @typedef TIdField
20
+ * @type {object}
21
+ * @memberof DoboAdapter
22
+ * @property {string} [name='_id'] - The name of the ID field.
23
+ * @property {string} [type='string'] - The data type of the ID field.
24
+ * @property {number} [maxLength=50] - The maximum length of the ID field.
25
+ * @property {boolean} [required=true] - Indicates if the ID field is required.
26
+ * @property {string} [index='primary'] - The index type of the ID field.
27
+ */
28
+
29
+ /**
30
+ * @typedef TSupport
31
+ * @memberof DoboAdapter
32
+ * @type {object}
33
+ * @property {object} [propType={}] - An object indicating support for various property types.
34
+ * @property {boolean} [propType.object=false] - Indicates if object property type is supported.
35
+ * @property {boolean} [propType.array=false] - Indicates if array property type is supported.
36
+ * @property {boolean} [propType.datetime=true] - Indicates if datetime property type is supported.
37
+ * @property {boolean} [search=false] - Indicates if search functionality is supported.
38
+ * @property {boolean} [uniqueIndex=false] - Indicates if unique index functionality is supported.
39
+ * @property {boolean} [nullableField=true] - Indicates if nullable fields are supported.
40
+ * @property {boolean} [transaction=false] - Indicates if transaction functionality is supported.
41
+ */
42
+
43
+ /**
44
+ * Adapter factory function.
45
+ *
46
+ * @async
47
+ * @returns {Promise<DoboAdapter>}
48
+ */
49
+ async function adapterFactory () {
50
+ const { Tools } = this.app.baseClass
51
+ const { pick, cloneDeep, has, uniq, without, isEmpty, omit, isFunction, camelCase, last } = this.app.lib._
52
+ const { isSet } = this.app.lib.aneka
53
+ const { runHook } = this.app.bajo
54
+
55
+ /**
56
+ * DoboAdapter class serves as a base class for all database adapters in the Dobo framework. It provides common functionality for managing models, records, and database operations.
57
+ * Child classes should implement the abstract methods to provide specific database functionality.
58
+ *
59
+ * @class
60
+ * @extends external:Tools
61
+ */
62
+ class DoboAdapter extends Tools {
63
+ /**
64
+ * Constructor.
65
+ */
66
+ constructor (plugin, name, options = {}) {
67
+ super(plugin)
68
+
69
+ /**
70
+ * Adapter name
71
+ * @type {string}
72
+ */
73
+ this.name = name
74
+
75
+ /**
76
+ * ID field configuration
77
+ * @type {DoboAdapter.TIdField}
78
+ */
79
+ this.idField = cloneDeep(defIdField)
80
+ this.propertyType = {}
81
+
82
+ /**
83
+ * Support configuration for the adapter
84
+ * @type {DoboAdapter.TSupport}
85
+ */
86
+ this.support = {
87
+ propType: {
88
+ object: false,
89
+ array: false,
90
+ datetime: true
91
+ },
92
+ search: false,
93
+ uniqueIndex: false,
94
+ nullableField: true,
95
+ transaction: false
96
+ }
97
+
98
+ /**
99
+ * Indicates whether to use UTC for datetime fields
100
+ * @type {boolean}
101
+ */
102
+ this.useUtc = false
103
+
104
+ /**
105
+ * Maximum chunk size for bulk operations
106
+ * @type {number}
107
+ */
108
+ this.maxChunkSize = 500
109
+
110
+ /**
111
+ * Indicates whether the adapter uses in-memory storage
112
+ * @type {boolean}
113
+ */
114
+ this.memory = false
115
+
116
+ /**
117
+ * Adapter options
118
+ * @type {object}
119
+ */
120
+ this.options = options
121
+ }
122
+
123
+ /**
124
+ * Sanitize connection object
125
+ * @async
126
+ * @method
127
+ * @param {Object} conn - Connection object
128
+ * @returns {Promise<void>}
129
+ */
130
+ async sanitizeConnection (conn) {
131
+ conn.proto = conn.proto ?? 'http' // used by adapter that use url based connection
132
+ conn.memory = false
133
+ }
134
+
135
+ /**
136
+ * Sanitizes the body of a record before creating or updating it. It ensures that all required fields
137
+ * are present and have valid values, and converts data types as necessary.
138
+ * @param {DoboModel} model - The model instance for which the body is being sanitized
139
+ * @param {object} body - The body of the record to be sanitized
140
+ * @param {boolean} [partial=false] - Indicates whether to perform a partial update
141
+ * @returns {object} - Sanitized body
142
+ */
143
+ sanitizeBody (model, body = {}, partial) {
144
+ const { keys, pick } = this.app.lib._
145
+ const item = cloneDeep(body)
146
+ let newId = false
147
+ if (has(item, 'id') && this.idField.name !== 'id') {
148
+ item[this.idField.name] = item.id
149
+ newId = true
150
+ }
151
+ for (const prop of model.getNonVirtualProperties()) {
152
+ if (item[prop.name] === 'null') item[prop.name] = null
153
+ if (!isSet(item[prop.name]) && !this.support.nullableField) {
154
+ switch (prop.type) {
155
+ case 'datetime': item[prop.name] = new Date(0); break
156
+ case 'float':
157
+ case 'double': item[prop.name] = 0; break
158
+ case 'string':
159
+ case 'text': item[prop.name] = ''; break
160
+ case 'object': item[prop.name] = {}; break
161
+ case 'array': item[prop.name] = []; break
162
+ }
163
+ }
164
+ if (isSet(item[prop.name]) && !this.support.propType[prop.type]) {
165
+ if (prop.type === 'datetime') item[prop.name] = item[prop.name].toISOString()
166
+ else if (['object', 'array'].includes(prop.type)) item[prop.name] = JSON.stringify(item[prop.name])
167
+ }
168
+ }
169
+ const result = partial ? pick(item, keys(body)) : item
170
+ if (newId) delete result.id
171
+ return result
172
+ }
173
+
174
+ /**
175
+ * Sanitizes a record retrieved from the database, converting data types as necessary
176
+ * and ensuring that the record conforms to the model's schema.
177
+ * @param {DoboModel} model - The model instance for which the record is being sanitized
178
+ * @param {object} [record={}] - The record retrieved from the database
179
+ * @param {object} [options={}] - Additional options for sanitization
180
+ * @returns {object} - Sanitized record
181
+ */
182
+ sanitizeRecord (model, record = {}, options = {}) {
183
+ const { dayjs } = this.app.lib
184
+ const { isString } = this.app.lib._
185
+ const item = { ...record }
186
+ if (has(item, this.idField.name) && this.idField.name !== 'id') {
187
+ item.id = item[this.idField.name]
188
+ delete item[this.idField.name]
189
+ }
190
+ for (const prop of model.properties) {
191
+ if (isSet(item[prop.name])) {
192
+ if (!this.support.propType[prop.type]) {
193
+ try {
194
+ if (prop.type === 'datetime') {
195
+ const dt = this.useUtc ? dayjs.utc(item[prop.name]) : dayjs(item[prop.name])
196
+ item[prop.name] = dt.toDate()
197
+ } else if (isString(item[prop.name]) && ['object', 'array'].includes(prop.type)) item[prop.name] = JSON.parse(item[prop.name])
198
+ } catch (err) {
199
+ item[prop.name] = null
200
+ }
201
+ }
202
+ if (prop.type === 'datetime' && isString(item[prop.name])) {
203
+ const dt = this.useUtc ? dayjs.utc(item[prop.name]) : dayjs(item[prop.name])
204
+ item[prop.name] = dt.toDate()
205
+ }
206
+ if (prop.type === 'boolean' && isSet(item[prop.name])) item[prop.name] = Boolean(item[prop.name])
207
+ }
208
+ }
209
+ return item
210
+ }
211
+
212
+ /**
213
+ * Utility method to get the real fields of a model, excluding virtual fields.
214
+ * This is useful for operations that require only the actual stored properties of a model.
215
+ * @param {*} model
216
+ * @returns {string[]} - Array of real field names
217
+ */
218
+ getRealFields (model) {
219
+ return model.getProperties({ noVirtual: true, namesOnly: true })
220
+ }
221
+
222
+ /**
223
+ * Utility method to get the virtual fields of a model.
224
+ * This is useful for operations that need to work with computed or derived properties.
225
+ * @param {DoboModel} model - The model instance
226
+ * @returns {string[]} - Array of virtual field names
227
+ */
228
+ getVirtualFields (model) {
229
+ return model.getVirtualProperties({ namesOnly: true })
230
+ }
231
+
232
+ /**
233
+ * Get returning fields for a model based on the provided options. If the adapter supports returning fields,
234
+ * it will return the specified fields or all model properties. It ensures that the ID field is always
235
+ * included in the returned fields.
236
+ * @param {DoboModel} model - The model instance for which to get the returning fields
237
+ * @param {object} options - Options that may include the fields to return
238
+ * @returns {string[]} - Array of field names to be returned
239
+ */
240
+ _getReturningFields (model, options = {}) {
241
+ const { fields = [] } = options
242
+ if (!this.support.returning) return []
243
+ let items = fields.length > 0 ? [...fields] : model.properties.map(prop => prop.name)
244
+ if (!items.includes(this.idField.name)) items.unshift(this.idField.name)
245
+ if (this.idField.name !== 'id') items = without(items, ['id'])
246
+ return uniq(items)
247
+ }
248
+
249
+ /**
250
+ * Attaches hooks to the model for various operations. It runs the appropriate hooks
251
+ * before and after the specified operation, allowing for custom behavior to be injected
252
+ * into the model's lifecycle.
253
+ * @internal
254
+ * @async
255
+ * @method
256
+ * @param {string} name - The name of the hook
257
+ * @param {DoboModel} model - The model instance to which the hook is being attached
258
+ * @param {...any} args - Additional arguments to be passed to the hook
259
+ */
260
+ async _attachHook (name, model, ...args) {
261
+ const { ns } = this.app.dobo
262
+ const { kebabCase } = this.app.lib._
263
+ const options = last(args)
264
+ if (!options.noAdapterHook) {
265
+ const prefix = kebabCase(name).split('-')[0]
266
+ await runHook(`${ns}.adapter:${prefix}Any`, model, options)
267
+ await runHook(`${ns}.adapter:${name}`, model, ...args)
268
+ await runHook(`${ns}.adapter.${camelCase(model.name)}:${name}`, ...args)
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Checks the uniqueness of fields with a unique index.
274
+ * @async
275
+ * @method
276
+ * @internal
277
+ * @param {DoboModel} model - The model instance to check
278
+ * @param {object} body - The data to be checked for uniqueness
279
+ * @param {object} options - Additional options, including the action being performed
280
+ * @returns {Promise<void>} - Resolves if unique, throws an error if not
281
+ */
282
+ _checkUnique = async (model, body = {}, options = {}) => {
283
+ const { isSet } = this.app.lib.aneka
284
+ const { filter, map, isEmpty, forOwn } = this.app.lib._
285
+ const indexes = filter(model.indexes ?? [], idx => idx.type === 'unique')
286
+ for (const index of indexes) {
287
+ const query = {}
288
+ for (const field of index.fields) {
289
+ if (isSet(body[field])) query[field] = body[field]
290
+ }
291
+ if (isEmpty(query)) continue
292
+ const { data } = await model.findOneRecord({ query }, options)
293
+ if (!isEmpty(data)) {
294
+ if (['updateRecord', 'upsertRecord'].includes(options.action)) {
295
+ let eq = true
296
+ forOwn(query, (v, k) => {
297
+ if (data[k] !== v) eq = false
298
+ })
299
+ if (!eq) continue
300
+ }
301
+ const error = this.app.dobo.t('uniqueConstraintError')
302
+ const details = map(index.fields, field => {
303
+ return { field, error }
304
+ })
305
+ throw this.app.dobo.error(error, { details, body })
306
+ }
307
+ }
308
+ }
309
+
310
+ // Internal calls that will be called by model
311
+
312
+ /**
313
+ * Wrapper for the `modelExists` method, called internally by `model` to make sure
314
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
315
+ *
316
+ * @internal
317
+ * @async
318
+ * @method
319
+ * @param {DoboModel} model
320
+ * @param {object} options
321
+ * @returns {Promise<boolean>}
322
+ */
323
+ async _modelExists (model, options = {}) {
324
+ return await this.modelExists(model, options)
325
+ }
326
+
327
+ /**
328
+ * Wrapper for the `buildModel` method, called internally by `model` to make sure
329
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
330
+ * @internal
331
+ * @async
332
+ * @method
333
+ * @param {DoboModel} model
334
+ * @param {object} options
335
+ * @returns {Promise<object>}
336
+ */
337
+ async _buildModel (model, options = {}) {
338
+ return await this.buildModel(model, options)
339
+ }
340
+
341
+ /**
342
+ * Wrapper for the `dropModel` method, called internally by `model` to make sure
343
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
344
+ * @internal
345
+ * @async
346
+ * @method
347
+ * @param {DoboModel} model
348
+ * @param {object} options
349
+ * @returns {Promise<object>}
350
+ */
351
+ async _dropModel (model, options = {}) {
352
+ return await this.dropModel(model, options)
353
+ }
354
+
355
+ /**
356
+ * Prepares the body of a record for creation by populating default values for properties
357
+ * that are not set. It handles various types of default values, including functions,
358
+ * special strings (like 'now', 'uuid', etc.), and static values.
359
+ * @internal
360
+ * @async
361
+ * @method
362
+ * @param {DoboModel} model - The model instance for which the body is being prepared
363
+ * @param {object} body - The data to be prepared for creation
364
+ * @param {object} options - Additional options that may affect the preparation
365
+ * @returns {Promise<object>} - The prepared body with default values populated
366
+ */
367
+ async _prepBodyForCreate (model, body = {}, options = {}) {
368
+ const { callHandler } = this.app.bajo
369
+ const { isSet, generateId } = this.app.lib.aneka
370
+ for (const prop of model.getProperties({ noVirtual: true })) {
371
+ if (isSet(prop.default) && (!options.noDefault) && (!isSet(body[prop.name]) || body[prop.name] === prop.default)) {
372
+ if (isFunction(prop.default)) body[prop.name] = await prop.default.call(model)
373
+ else if (typeof prop.default !== 'string') body[prop.name] = prop.default
374
+ else {
375
+ if (['now'].includes(prop.default) && prop.type === 'datetime') {
376
+ body[prop.name] = new Date()
377
+ } else if (['uuid', 'uuidv4'].includes(prop.default) && prop.type === 'string') {
378
+ body[prop.name] = uuidv4().slice(0, prop.maxLength)
379
+ } else if (prop.default === 'uuidv7' && prop.type === 'string') {
380
+ body[prop.name] = uuidv7().slice(0, prop.maxLength)
381
+ } else if (prop.default === 'ulid' && prop.type === 'string') {
382
+ body[prop.name] = ulid().slice(0, prop.maxLength)
383
+ } else if (prop.default === 'generateid' && prop.type === 'string') {
384
+ body[prop.name] = generateId()
385
+ } else if (prop.default.startsWith('handler:')) {
386
+ const [, ...args] = prop.default.split(':')
387
+ if (args.length > 0) body[prop.name] = await callHandler(args.join(':'))
388
+ } else if (prop.default.startsWith('md5:') && prop.type === 'string') {
389
+ const [, field] = prop.default.split(':')
390
+ const fields = field.split(',')
391
+ if (model.properties.filter(item => fields.includes(item.name)).length === fields.length) {
392
+ const values = fields.map(f => body[f])
393
+ body[prop.name] = crypto.createHash('md5').update(values.join(':')).digest('hex')
394
+ }
395
+ } else {
396
+ body[prop.name] = prop.default
397
+ }
398
+ }
399
+ }
400
+ }
401
+ return pick(body, this.getRealFields(model))
402
+ }
403
+
404
+ /**
405
+ * Prepares the ID for a record before creation. It generates an ID if it is not set in the body.
406
+ *
407
+ * @internal
408
+ * @async
409
+ * @method
410
+ * @param {DoboModel} model - The model instance for which the ID is being prepared
411
+ * @param {object} body - The data containing the ID
412
+ * @param {object} options - Additional options that may affect ID generation
413
+ * @returns {Promise<void>} - Resolves when the ID has been prepared
414
+ */
415
+ async _prepIdForCreate (model, body = {}, options = {}) {
416
+ const { isSet, generateId } = this.app.lib.aneka
417
+ const { isFunction } = this.app.lib._
418
+ const prop = model.properties.find(p => p.name === 'id')
419
+ if (!isSet(body.id) && prop.type === 'string') {
420
+ if (this.idGenerator) {
421
+ if (['uuid', 'uuidv4'].includes(this.idGenerator)) body.id = uuidv4()
422
+ else if (['uuidv7'].includes(this.idGenerator)) body.id = uuidv7()
423
+ else if (this.idGenerator === 'generateId') body.id = generateId()
424
+ else if (isFunction(this.idGenerator)) body.id = await this.idGenerator(model, body, options)
425
+ }
426
+ if (!body.id) body.id = ulid()
427
+ body.id = body.id.slice(0, prop.maxLength)
428
+ }
429
+ }
430
+
431
+ _injectMeta (result = {}, options = {}) {
432
+ result.warnings = result.warnings ?? []
433
+ result.warnings.push(...(options.warnings ?? []))
434
+ }
435
+
436
+ /**
437
+ * Wrapper for the {@link DoboAdapter#createRecord} method, called internally by `model` to make sure
438
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
439
+ * @internal
440
+ * @async
441
+ * @method
442
+ * @param {DoboModel} model
443
+ * @param {object} input
444
+ * @param {object} options
445
+ * @returns {Promise<object>}
446
+ */
447
+ async _createRecord (model, input = {}, options = {}) {
448
+ const { isSet } = this.app.lib.aneka
449
+ let body = await this._prepBodyForCreate(model, input, options)
450
+ await this._prepIdForCreate(model, body, options)
451
+ if (!options.noUniqueCheck) {
452
+ if (!this.support.uniqueIndex) await this._checkUnique(model, body, options)
453
+ }
454
+ if (!options.noIdCheck && isSet(body.id)) {
455
+ const resp = await this.getRecord(model, body.id, { noMagic: true })
456
+ if (!isEmpty(resp.data)) throw this.plugin.error('recordExists%s%s', body.id, model.name)
457
+ }
458
+ body = this.sanitizeBody(model, body)
459
+
460
+ await this._attachHook('beforeCreateRecord', model, body, options)
461
+ const result = await this.createRecord(model, body, options)
462
+ await this._attachHook('afterCreateRecord', model, body, result, options)
463
+
464
+ if (options.noResult) return
465
+ result.data = this.sanitizeRecord(model, result.data, options)
466
+ this._injectMeta(result, options)
467
+ return result
468
+ }
469
+
470
+ /**
471
+ * Wrapper for the {@link DoboAdapter#bulkCreateRecord} method, called internally by `model` to make sure
472
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
473
+ * @internal
474
+ * @async
475
+ * @method
476
+ * @param {DoboModel} model
477
+ * @param {Array<object>} bodies
478
+ * @param {object} options
479
+ * @returns {Promise<void>}
480
+ */
481
+ async _bulkCreateRecord (model, bodies = [], options = {}) {
482
+ const { chunk } = this.app.lib._
483
+ let { chunkSize = this.maxChunkSize } = options
484
+ if (chunkSize > this.maxChunkSize) chunkSize = this.maxChunkSize
485
+ for (const idx in bodies) {
486
+ const body = await this._prepBodyForCreate(model, bodies[idx], options)
487
+ await this._prepIdForCreate(model, body, options)
488
+ bodies[idx] = this.sanitizeBody(model, body)
489
+ }
490
+
491
+ await this._attachHook('beforeBulkCreateRecord', model, bodies, options)
492
+ const items = chunk(bodies, chunkSize)
493
+ for (const item of items) {
494
+ await this.bulkCreateRecord(model, item, options)
495
+ }
496
+ await this._attachHook('afterBulkCreateRecord', model, bodies, [], options)
497
+ }
498
+
499
+ /**
500
+ * Wrapper for the {@link DoboAdapter#getRecord} method, called internally by `model` to make sure
501
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
502
+ * @internal
503
+ * @async
504
+ * @method
505
+ * @param {DoboModel} model
506
+ * @param {string|number} id
507
+ * @param {object} options
508
+ * @returns {Promise<object>}
509
+ */
510
+ async _getRecord (model, id, options = {}) {
511
+ await this._attachHook('beforeGetRecord', model, id, options)
512
+ const result = await this.getRecord(model, id, options)
513
+ await this._attachHook('afterGetRecord', model, id, result, options)
514
+
515
+ if (isEmpty(result.data) && options.throwNotFound) throw this.plugin.error('recordNotFound%s%s', id, model.name)
516
+ result.data = this.sanitizeRecord(model, result.data, options)
517
+ this._injectMeta(result, options)
518
+ return result
519
+ }
520
+
521
+ /**
522
+ * Wrapper for the {@link DoboAdapter#updateRecord} method, called internally by `model` to make sure
523
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
524
+ * @internal
525
+ * @async
526
+ * @method
527
+ * @param {DoboModel} model
528
+ * @param {string|number} id
529
+ * @param {object} input
530
+ * @param {object} options
531
+ * @returns {Promise<object>}
532
+ */
533
+ async _updateRecord (model, id, input = {}, options = {}) {
534
+ let body = omit(input, this.getVirtualFields(model))
535
+ if (!options.noUniqueCheck) {
536
+ if (!this.support.uniqueIndex) await this._checkUnique(model, body, options)
537
+ }
538
+ if (!options._data) {
539
+ const resp = await this.getRecord(model, id, { noMagic: true })
540
+ if (!resp.data) throw this.plugin.error('recordNotFound%s%s', id, model.name)
541
+ options._data = resp.data
542
+ }
543
+ body = this.sanitizeBody(model, body, true)
544
+ delete body.id
545
+
546
+ await this._attachHook('beforeUpdateRecord', model, id, body, options)
547
+ const result = await this.updateRecord(model, id, body, options)
548
+ await this._attachHook('afterUpdateRecord', model, id, body, result, options)
549
+
550
+ if (options.noResult) return
551
+ result.oldData = this.sanitizeRecord(model, result.oldData, options)
552
+ result.data = this.sanitizeRecord(model, result.data, options)
553
+ this._injectMeta(result, options)
554
+ return result
555
+ }
556
+
557
+ /**
558
+ * Upserts a record for the given model.
559
+ * This method will only run if child adapter does not implement {@link DoboAdapter#upsertRecord}.
560
+ *
561
+ * @internal
562
+ * @async
563
+ * @method
564
+ * @param {DoboModel} model - The model instance for which the record is being upserted
565
+ * @param {object} input - The input data for the upsert
566
+ * @param {object} options - Additional options that may affect record upserting
567
+ * @returns {Promise<object>} - The result of the record upsert
568
+ */
569
+ async _upsertRecord (model, input = {}, options = {}) {
570
+ let body = omit(input, this.getVirtualFields(model))
571
+ if (!options.noUniqueCheck) {
572
+ if (!this.support.uniqueIndex) await this._checkUnique(model, body, options)
573
+ }
574
+ if (isSet(body.id)) {
575
+ if (!options._data) {
576
+ const resp = await this.getRecord(model, body.id, { noMagic: true })
577
+ if (!resp.data) throw this.plugin.error('recordNotFound%s%s', body.id, model.name)
578
+ options._data = resp.data
579
+ }
580
+ }
581
+ body = this.sanitizeBody(model, body)
582
+
583
+ await this._attachHook('beforeUpsertRecord', model, body, options)
584
+ const result = await this.upsertRecord(model, body, options)
585
+ await this._attachHook('afterUpsertRecord', model, body, result, options)
586
+
587
+ if (options.noResult) return
588
+ if (result.oldData) result.oldData = this.sanitizeRecord(model, result.oldData, options)
589
+ result.data = this.sanitizeRecord(model, result.data, options)
590
+ this._injectMeta(result, options)
591
+ return result
592
+ }
593
+
594
+ /**
595
+ * Wrapper for the {@link DoboAdapter#removeRecord} method, called internally by `model` to make sure
596
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
597
+ * @internal
598
+ * @async
599
+ * @method
600
+ * @param {DoboModel} model
601
+ * @param {string|number} id
602
+ * @param {object} options
603
+ * @returns {Promise<object>}
604
+ */
605
+ async _removeRecord (model, id, options = {}) {
606
+ if (!options._data) {
607
+ const resp = await this.getRecord(model, id, { noMagic: true })
608
+ if (!resp.data) throw this.plugin.error('recordNotFound%s%s', id, model.name)
609
+ options._data = resp.data
610
+ }
611
+
612
+ await this._attachHook('beforeRemoveRecord', model, id, options)
613
+ const result = await this.removeRecord(model, id, options)
614
+ await this._attachHook('afterRemoveRecord', model, id, result, options)
615
+
616
+ if (options.noResult) return
617
+ result.oldData = this.sanitizeRecord(model, result.oldData, options)
618
+ this._injectMeta(result, options)
619
+ return result
620
+ }
621
+
622
+ /**
623
+ * Wrapper for the {@link DoboAdapter#clearRecord} method, called internally by `model` to make sure
624
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
625
+ * @internal
626
+ * @async
627
+ * @method
628
+ * @param {DoboModel} model
629
+ * @param {object} options
630
+ * @returns {Promise<object>}
631
+ */
632
+ async _clearRecord (model, options = {}) {
633
+ await this._attachHook('beforeClearRecord', model, options)
634
+ const result = await this.clearRecord(model, options)
635
+ await this._attachHook('afterClearRecord', model, result, options)
636
+
637
+ this._injectMeta(result, options)
638
+ return result
639
+ }
640
+
641
+ /**
642
+ * Wrapper for the {@link DoboAdapter#findRecord} method, called internally by `model` to make sure
643
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
644
+ * @internal
645
+ * @async
646
+ * @method
647
+ * @param {DoboModel} model
648
+ * @param {object} filter
649
+ * @param {object} options
650
+ * @returns {Promise<object>}
651
+ */
652
+ async _findRecord (model, filter = {}, options = {}) {
653
+ let result
654
+ try {
655
+ await this._attachHook('beforeFindRecord', model, filter, options)
656
+ result = await this.findRecord(model, filter, options)
657
+ await this._attachHook('afterFindRecord', model, filter, result, options)
658
+ } catch (err) {
659
+ if (!['_emptyColumnQuery', '_abortAction'].includes(err.message)) throw err
660
+ result = {
661
+ data: [],
662
+ count: 0
663
+ // warnings: [] // TODO: should generate warnings?
664
+ }
665
+ }
666
+
667
+ for (const idx in result.data) {
668
+ result.data[idx] = this.sanitizeRecord(model, result.data[idx], options)
669
+ }
670
+ this._injectMeta(result, options)
671
+ return result
672
+ }
673
+
674
+ /**
675
+ * Finds all records for the given model based on the provided filter.
676
+ * This method will only run if child adapter does not implement {@link DoboAdapter#findAllRecord}.
677
+ *
678
+ * @internal
679
+ * @async
680
+ * @method
681
+ * @param {DoboModel} model - The model instance for which the records are being found
682
+ * @param {object} filter - The filter criteria for finding records
683
+ * @param {object} options - Additional options that may affect record finding
684
+ * @returns {Promise<object>} - The result of the record finding
685
+ */
686
+ async _findAllRecord (model, filter = {}, options = {}) {
687
+ let result
688
+ try {
689
+ await this._attachHook('beforeFindAllRecord', model, filter, options)
690
+ result = await this.findAllRecord(model, filter, options)
691
+ await this._attachHook('afterFindAllRecord', model, filter, result, options)
692
+ } catch (err) {
693
+ if (err.message !== '_emptyColumnQuery') throw err
694
+ result = {
695
+ data: [],
696
+ count: 0
697
+ // warnings: [] // TODO: should generate warnings?
698
+ }
699
+ }
700
+
701
+ for (const idx in result.data) {
702
+ result.data[idx] = this.sanitizeRecord(model, result.data[idx], options)
703
+ }
704
+ this._injectMeta(result, options)
705
+ return result
706
+ }
707
+
708
+ /**
709
+ * Wrapper for the {@link DoboAdapter#countRecord} method, called internally by `model` to make sure
710
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
711
+ * @internal
712
+ * @async
713
+ * @method
714
+ * @param {DoboModel} model
715
+ * @param {object} filter
716
+ * @param {object} options
717
+ * @returns {Promise<object>}
718
+ */
719
+ async _countRecord (model, filter = {}, options = {}) {
720
+ let result
721
+ try {
722
+ await this._attachHook('beforeCountRecord', model, filter, options)
723
+ result = await this.countRecord(model, filter, options)
724
+ await this._attachHook('afterCountRecord', model, filter, result, options)
725
+ } catch (err) {
726
+ if (err.message !== '_emptyColumnQuery') throw err
727
+ result = { data: 0 }
728
+ }
729
+
730
+ return result
731
+ }
732
+
733
+ /**
734
+ * Wrapper for the {@link DoboAdapter#aggregate} method, called internally by `model` to make sure
735
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
736
+ * @internal
737
+ * @async
738
+ * @method
739
+ * @param {DoboModel} model
740
+ * @param {object} filter
741
+ * @param {object} params
742
+ * @param {object} options
743
+ * @returns {Promise<object>}
744
+ */
745
+ async _aggregate (model, filter = {}, params = {}, options = {}) {
746
+ const fieldPropTypes = ['integer', 'smallint', 'float', 'double']
747
+ const groupPropTypes = ['string', ...fieldPropTypes]
748
+ this.app.dobo.checkAggregateParams(params)
749
+ const { group, field } = params
750
+
751
+ let prop = model.properties.find(p => p.name === group)
752
+ if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), group)
753
+ if (!groupPropTypes.includes(prop.type)) throw this.plugin.error('allowedPropType%s%s', group, groupPropTypes.join(', '))
754
+
755
+ prop = model.properties.find(p => p.name === field)
756
+ if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), field)
757
+ // if (!fieldPropTypes.includes(prop.type)) throw this.plugin.error('allowedPropType%s%s', field, fieldPropTypes.join(', '))
758
+
759
+ let result
760
+ try {
761
+ await this._attachHook('beforeAggregate', model, filter, params, options)
762
+ result = await this.aggregate(model, filter, params, options)
763
+ await this._attachHook('afterAggregate', model, filter, params, result, options)
764
+ } catch (err) {
765
+ if (err.message !== '_emptyColumnQuery') throw err
766
+ result = { data: [] }
767
+ }
768
+
769
+ for (const idx in result.data) {
770
+ result.data[idx] = this.sanitizeRecord(model, result.data[idx])
771
+ }
772
+ this._injectMeta(result, options)
773
+ return result
774
+ }
775
+
776
+ /**
777
+ * Wrapper for the {@link DoboAdapter#histogram} method, called internally by `model` to make sure
778
+ * all adapter hooks are executed accordingly, and all inputs and outputs are sanitized.
779
+ * @internal
780
+ * @async
781
+ * @method
782
+ * @param {DoboModel} model
783
+ * @param {object} filter
784
+ * @param {object} params
785
+ * @param {object} options
786
+ * @returns {Promise<object>}
787
+ */
788
+ async _histogram (model, filter = {}, params, options = {}) {
789
+ // const fieldPropTypes = ['integer', 'smallint', 'float', 'double']
790
+ const groupPropTypes = ['datetime', 'date']
791
+ this.app.dobo.checkHistogramParams(params)
792
+ const { group, field } = params
793
+
794
+ let prop = model.properties.find(p => p.name === group)
795
+ if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), group)
796
+ if (!groupPropTypes.includes(prop.type)) throw this.plugin.error('allowedPropType%s%s', group, groupPropTypes.join(', '))
797
+
798
+ prop = model.properties.find(p => p.name === field)
799
+ if (!prop) throw this.plugin.error('unknown%s%s', this.plugin.t('field.field'), field)
800
+
801
+ let result
802
+ try {
803
+ await this._attachHook('beforeHistogram', model, filter, params, options)
804
+ result = await this.histogram(model, filter, params, options)
805
+ await this._attachHook('afterHistogram', model, filter, params, result, options)
806
+ } catch (err) {
807
+ if (err.message !== '_emptyColumnQuery') throw err
808
+ result = { data: [] }
809
+ }
810
+
811
+ for (const idx in result.data) {
812
+ result.data[idx] = this.sanitizeRecord(model, result.data[idx])
813
+ }
814
+ this._injectMeta(result, options)
815
+ return result
816
+ }
817
+
818
+ // Public calls that need to be implemented by child adapters
819
+
820
+ /**
821
+ * Connects to the database using the provided connection parameters.
822
+ * @param {*} connection - The connection parameters for the database
823
+ * @param {*} noRebuild - Flag indicating whether to skip rebuilding the database schema
824
+ * @returns {Promise<void>} - Resolves when the connection is established
825
+ */
826
+ async connect (connection, noRebuild) {
827
+ }
828
+
829
+ /**
830
+ * Checks if the model exists in the database.
831
+ *
832
+ * Must be implemented by child classes to provide specific database functionality for model existence checking,
833
+ * or throw an error if the operation is not supported by the adapter.
834
+ *
835
+ * @param {DoboModel} model - The model instance to check for existence
836
+ * @param {object} options - Additional options that may affect the existence check
837
+ */
838
+ async modelExists (model, options = {}) {
839
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'modelExists', this.name)
840
+ }
841
+
842
+ /**
843
+ * Builds the model in the database.
844
+ *
845
+ * Must be implemented by child classes to provide specific database functionality for model building,
846
+ * or throw an error if the operation is not supported by the adapter.
847
+ *
848
+ * @param {DoboModel} model - The model instance for which the record is being built
849
+ * @param {object} options - Additional options that may affect model building
850
+ */
851
+ async buildModel (model, options = {}) {
852
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'buildModel', this.name)
853
+ }
854
+
855
+ /**
856
+ * Drops the model from the database.
857
+ *
858
+ * Must be implemented by child classes to provide specific database functionality for dropping a model,
859
+ * or throw an error if the operation is not supported by the adapter.
860
+ *
861
+ * @param {DoboModel} model - The model instance for which the record is being dropped
862
+ * @param {object} options - Additional options that may affect record dropping
863
+ */
864
+ async dropModel (model, options = {}) {
865
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'dropModel', this.name)
866
+ }
867
+
868
+ /**
869
+ * Creates a new record for the given model.
870
+ *
871
+ * Must be implemented by child classes to provide specific database functionality for record creation,
872
+ * or throw an error if the operation is not supported by the adapter.
873
+ *
874
+ * @param {DoboModel} model - The model instance for which the record is being created
875
+ * @param {object} input - The input data for the new record
876
+ * @param {object} options - Additional options that may affect record creation
877
+ * @returns {Promise<object>} - The result of the record creation
878
+ */
879
+ async createRecord (model, body = {}, options = {}) {
880
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'createRecord', this.name)
881
+ }
882
+
883
+ /**
884
+ * Retrieves a record for the given model by its ID.
885
+ *
886
+ * Must be implemented by child classes to provide specific database functionality for record retrieval,
887
+ * or throw an error if the operation is not supported by the adapter.
888
+ *
889
+ * @param {DoboModel} model - The model instance for which the record is being retrieved
890
+ * @param {string|number} id - The ID of the record to retrieve
891
+ * @param {object} options - Additional options that may affect record retrieval
892
+ * @returns {Promise<object>} - The result of the record retrieval
893
+ */
894
+ async getRecord (model, id, options = {}) {
895
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'getRecord', this.name)
896
+ }
897
+
898
+ /**
899
+ * Updates a record for the given model by its ID.
900
+ *
901
+ * Must be implemented by child classes to provide specific database functionality for record updating,
902
+ * or throw an error if the operation is not supported by the adapter.
903
+ *
904
+ * @param {DoboModel} model - The model instance for which the record is being updated
905
+ * @param {string|number} id - The ID of the record to update
906
+ * @param {object} input - The input data for the update
907
+ * @param {object} options - Additional options that may affect record updating
908
+ * @returns {Promise<object>} - The result of the record update
909
+ */
910
+ async updateRecord (model, id, body = {}, options = {}) {
911
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'updateRecord', this.name)
912
+ }
913
+
914
+ /**
915
+ * Removes a record for the given model by its ID.
916
+ *
917
+ * Must be implemented by child classes to provide specific database functionality for record removal,
918
+ * or throw an error if the operation is not supported by the adapter.
919
+ *
920
+ * @param {DoboModel} model - The model instance for which the record is being removed
921
+ * @param {string|number} id - The ID of the record to remove
922
+ * @param {object} options - Additional options that may affect record removal
923
+ * @returns {Promise<object>} - The result of the record removal
924
+ */
925
+ async removeRecord (model, id, options = {}) {
926
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'removeRecord', this.name)
927
+ }
928
+
929
+ /**
930
+ * Clears all records for the given model.
931
+ *
932
+ * Must be implemented by child classes to provide specific database functionality for record clearing,
933
+ * or throw an error if the operation is not supported by the adapter.
934
+ *
935
+ * @param {DoboModel} model - The model instance for which the records are being cleared
936
+ * @param {object} options - Additional options that may affect record clearing
937
+ * @returns {Promise<object>} - The result of the record clearing
938
+ */
939
+ async clearRecord (model, options = {}) {
940
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'clearRecord', this.name)
941
+ }
942
+
943
+ /**
944
+ * Finds records for the given model based on the provided filter.
945
+ *
946
+ * Must be implemented by child classes to provide specific database functionality for record finding,
947
+ * or throw an error if the operation is not supported by the adapter.
948
+ *
949
+ * @param {DoboModel} model - The model instance for which the records are being found
950
+ * @param {object} filter - The filter criteria for finding records
951
+ * @param {object} options - Additional options that may affect record finding
952
+ * @returns {Promise<object>} - The result of the record finding
953
+ */
954
+ async findRecord (model, filter = {}, options = {}) {
955
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'findRecord', this.name)
956
+ }
957
+
958
+ /**
959
+ * Bulk creates records for the given model.
960
+ *
961
+ * Must be implemented by child classes to provide specific database functionality for bulk record creation,
962
+ * or throw an error if the operation is not supported by the adapter.
963
+ *
964
+ * @param {DoboModel} model - The model instance for which the records are being created
965
+ * @param {Array<object>} bodies - The array of input data for the new records
966
+ * @param {object} options - Additional options that may affect record creation
967
+ * @returns {Promise<void>} - Resolves when the records have been created
968
+ */
969
+ async bulkCreateRecord (model, bodies = [], options = {}) {
970
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'bulkCreateRecord', this.name)
971
+ }
972
+
973
+ /**
974
+ * Counts the records for the given model based on the provided filter.
975
+ *
976
+ * Must be implemented by child classes to provide specific database functionality for record counting,
977
+ * or throw an error if the operation is not supported by the adapter.
978
+ *
979
+ * @param {DoboModel} model - The model instance for which the records are being counted
980
+ * @param {object} filter - The filter criteria for counting records
981
+ * @param {object} options - Additional options that may affect record counting
982
+ * @returns {Promise<object>} - The result of the record counting
983
+ */
984
+ async countRecord (model, filter = {}, options = {}) {
985
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'countRecord', this.name)
986
+ }
987
+
988
+ /**
989
+ * Creates an aggregate for the given model based on the provided filter and parameters.
990
+ *
991
+ * Must be implemented by child classes to provide specific database functionality for aggregate creation,
992
+ * or throw an error if the operation is not supported by the adapter.
993
+ *
994
+ * @param {DoboModel} model - The model instance for which the aggregate is being created
995
+ * @param {object} filter - The filter criteria for creating the aggregate
996
+ * @param {object} params - The parameters for the aggregate creation
997
+ * @param {object} options - Additional options that may affect aggregate creation
998
+ * @returns {Promise<object>} - The result of the aggregate creation
999
+ */
1000
+ async aggregate (model, filter = {}, params = {}, options = {}) {
1001
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'aggregate', this.name)
1002
+ }
1003
+
1004
+ /**
1005
+ * Creates a histogram for the given model based on the provided filter and parameters.
1006
+ *
1007
+ * Must be implemented by child classes to provide specific database functionality for histogram creation,
1008
+ * or throw an error if the operation is not supported by the adapter.
1009
+ *
1010
+ * @param {DoboModel} model - The model instance for which the histogram is being created
1011
+ * @param {object} filter - The filter criteria for creating the histogram
1012
+ * @param {object} params - The parameters for the histogram creation
1013
+ * @param {object} options - Additional options that may affect histogram creation
1014
+ * @returns {Promise<object>} - The result of the histogram creation
1015
+ */
1016
+ async histogram (model, filter = {}, params = {}, options = {}) {
1017
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'histogram', this.name)
1018
+ }
1019
+
1020
+ /**
1021
+ * Executes a transaction for the given model.
1022
+ *
1023
+ * Must be implemented by child classes to provide specific database functionality for transactions,
1024
+ * or throw an error if the operation is not supported by the adapter.
1025
+ *
1026
+ * @param {DoboModel} model - The model instance for which the transaction is being executed
1027
+ * @param {Function} handler - The transaction handler function
1028
+ * @param {...any} args - Additional arguments for the transaction handler
1029
+ */
1030
+ async transaction (model, handler, ...args) {
1031
+ throw this.plugin.error('notSupportedAdapter%s%s%s', this.app.t('method'), 'transaction', this.name)
1032
+ }
1033
+
1034
+ /**
1035
+ * Disposes of the adapter, performing any necessary cleanup operations.
1036
+ *
1037
+ * @returns {Promise<void>} - Resolves when the adapter has been disposed
1038
+ */
1039
+ async dispose () {
1040
+ await super.dispose()
1041
+ }
1042
+ }
1043
+
1044
+ class DoboNullAdapter extends DoboAdapter {
1045
+ constructor (plugin, name = 'null', options = {}) {
1046
+ super(plugin, name, options)
1047
+ this.memory = true
1048
+ }
1049
+ }
1050
+
1051
+ this.app.baseClass.DoboAdapter = DoboAdapter
1052
+ this.app.baseClass.DoboNullAdapter = DoboNullAdapter
1053
+ }
1054
+
1055
+ export default adapterFactory