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
package/lib/helper.js CHANGED
@@ -1,463 +1,463 @@
1
- import path from 'path'
2
- import adapterFactory from './factory/adapter.js'
3
- import connectionFactory from './factory/connection.js'
4
- import featureFactory from './factory/feature.js'
5
- import modelFactory from './factory/model.js'
6
- import actionFactory from './factory/action.js'
7
-
8
- /**
9
- * @module Helper
10
- */
11
-
12
- /**
13
- * Sanitizes a property definition for a model, ensuring it adheres to the expected structure and types.
14
- * It handles various property attributes such as type, index, required status, and more.
15
- *
16
- * If the property is virtual, it processes it accordingly; otherwise, it adds it to the model's properties
17
- * and indexes.
18
- * @param {DoboModel} model
19
- * @param {DoboModel.TProperty} prop
20
- * @param {DoboModel.TIndex[]} indexes
21
- */
22
- async function sanitizeProp (model, prop, indexes) {
23
- const { isEmpty, isString, keys, pick, isArray, isPlainObject, omit, isFunction } = this.app.lib._
24
- const allPropKeys = this.getAllPropertyKeys(model.connection.adapter)
25
- const propType = this.constructor.propertyType
26
- if (isString(prop)) {
27
- let [name, type, maxLength, idx, required] = prop.split(',').map(m => m.trim())
28
- if (isEmpty(type)) type = 'string'
29
- maxLength = isEmpty(maxLength) ? propType.string.maxLength : parseInt(maxLength)
30
- prop = { name, type, maxLength }
31
- if (!isEmpty(idx)) prop.index = idx
32
- prop.required = required === 'true'
33
- }
34
- prop.type = prop.type ?? 'string'
35
- if (isArray(prop.values)) {
36
- prop.values = prop.values.map(item => {
37
- if (isPlainObject(item)) return pick(item, ['value', 'text'])
38
- return { value: item, text: item }
39
- })
40
- } else if (!(isString(prop.values) || isFunction(prop.values))) delete prop.values
41
- if (prop.hidden) model.hidden.push(prop.name)
42
- if (prop.scanable) model.scanables.push(prop.scanable)
43
- if (prop.virtual) {
44
- const keys = Object.keys(propType)
45
- if (!keys.includes(prop.type)) this.fatal('unknownPropType%s%s', `${prop.name}:${prop.type}`, model.name)
46
- for (const key of ['required', 'rules', 'index', 'validator', 'ref', 'rulesMsg', 'immutable']) {
47
- delete prop[key]
48
- }
49
- model.properties.push(prop)
50
- } else {
51
- if (prop.index) {
52
- if (prop.index === true || prop.index === 'true') prop.index = 'index'
53
- const [idx, idxName] = prop.index.split(':')
54
- const index = { name: idxName ?? `${model.collName}_${prop.name}_${idx}`, fields: [prop.name], type: idx }
55
- indexes.push(index)
56
- }
57
- if (keys(propType).includes(prop.type)) model.properties.push(pick(prop, allPropKeys))
58
- else {
59
- const feature = this.getFeature(prop.type)
60
- if (!feature) this.fatal('unknownPropType%s%s', prop.type, model.name)
61
- const opts = omit(prop, ['name', 'type'])
62
- opts.field = prop.name
63
- await applyFeature.call(this, model, feature, opts, indexes)
64
- }
65
- }
66
- }
67
-
68
- /**
69
- * Finds and sanitizes all properties for a given model schema, ensuring that the 'id' property is included and
70
- * that all properties are properly processed.
71
- * @param {object} schema - The model schema
72
- * @param {array} inputs - The list of input properties to process
73
- * @param {array} indexes - The list of indexes to update
74
- */
75
- async function findAllProps (schema, inputs = [], indexes = []) {
76
- const { isPlainObject, cloneDeep } = this.app.lib._
77
- const isIdProp = inputs.find(p => {
78
- return isPlainObject(p) ? p.name === 'id' : p.startsWith('id,')
79
- })
80
- if (!isIdProp) {
81
- const idField = cloneDeep(schema.connection.adapter.idField)
82
- idField.name = 'id'
83
- inputs.unshift(idField)
84
- }
85
- for (const prop of inputs) {
86
- await sanitizeProp.call(this, schema, prop, indexes)
87
- }
88
- }
89
-
90
- /**
91
- * Applies a feature to a model schema, adding properties, rules, hooks, and other
92
- * configurations as defined by the feature.
93
- * @param {object} schema - The model schema
94
- * @param {object} feature - The feature to apply
95
- * @param {object} options - The options for the feature
96
- * @param {array} indexes - The list of indexes to update
97
- */
98
- async function applyFeature (schema, feature, options, indexes) {
99
- const { isArray, findIndex } = this.app.lib._
100
- if (feature.name === 'dobo:unique' && options.field === 'id') {
101
- const idx = findIndex(schema.properties, { name: 'id' })
102
- if (idx > -1) schema.properties.pullAt(idx)
103
- }
104
- const item = await feature.handler(options)
105
- if (item.rules) schema.rules.push(...item.rules)
106
- if (item.scanables) schema.scanables.push(...item.scanables)
107
- if (!isArray(item.properties)) item.properties = [item.properties]
108
- for (const prop of item.properties) {
109
- prop.feature = `${feature.plugin.ns}:${feature.name}`
110
- await sanitizeProp.call(this, schema, prop, indexes)
111
- }
112
- if (item.hooks) {
113
- item.hooks = item.hooks.map(hook => {
114
- hook.level = hook.level ?? 999
115
- return hook
116
- })
117
- schema.hooks.push(...item.hooks)
118
- }
119
- }
120
-
121
- /**
122
- * Finds and applies all features for a given model schema, ensuring that each feature is properly processed
123
- * @param {object} schema - The model schema
124
- * @param {array} inputs - The list of features to apply
125
- * @param {array} indexes - The list of indexes to update
126
- */
127
- async function findAllFeats (schema, inputs = [], indexes = []) {
128
- const { isString, omit } = this.app.lib._
129
- for (let feat of inputs) {
130
- if (isString(feat)) feat = { name: feat }
131
- const featName = feat.name.indexOf(':') === -1 ? `dobo:${feat.name}` : feat.name
132
- const feature = this.app.dobo.getFeature(featName)
133
- if (!feature) this.fatal('invalidFeature%s%s', schema.name, featName)
134
- await applyFeature.call(this, schema, feature, omit(feat, 'name'), indexes)
135
- }
136
- }
137
-
138
- /**
139
- * Finds and applies all indexes for a given model schema, ensuring that each index is properly processed
140
- * @param {object} schema - The model schema
141
- * @param {array} inputs - The list of indexes to apply
142
- * @param {array} indexes - The list of existing indexes to update
143
- */
144
- async function findAllIndexes (schema, inputs = [], indexes = []) {
145
- const items = []
146
- for (const index of inputs) {
147
- index.type = index.type ?? 'index'
148
- index.fields = index.fields ?? []
149
- if (!index.name) index.name = `${schema.name}_${index.fields.join('_')}_${index.type}`
150
- items.push(index)
151
- }
152
- schema.indexes = [...items, ...indexes]
153
- }
154
-
155
- /**
156
- * Sanitizes the references in a model schema, ensuring that all references point to valid models and properties.
157
- * It checks for duplicate reference keys and validates the existence of referenced models and properties.
158
- * @async
159
- * @param {object} schema - The model schema to sanitize
160
- * @param {object[]} schemas - The list of all model schemas
161
- * @returns {Promise<void>}
162
- */
163
- export async function sanitizeRef (schema, schemas) {
164
- const { find, isString, pullAt } = this.app.lib._
165
- const _refKeys = []
166
- for (const prop of schema.properties) {
167
- const ignored = []
168
- for (const key in prop.ref ?? {}) {
169
- _refKeys.push(key)
170
- let ref = prop.ref[key]
171
- if (isString(ref)) {
172
- ref = { field: ref }
173
- }
174
- ref.field = ref.field ?? 'id'
175
- ref.type = ref.type ?? '1:1'
176
- ref.searchField = ref.searchField ?? ref.field
177
- ref.labelField = ref.labelField ?? ref.searchField
178
- const rModel = find(schemas, { name: ref.model })
179
- if (!rModel) {
180
- ignored.push(key)
181
- this.log.warn('notFound%s%s', this.t('model'), ref.model)
182
- continue
183
- }
184
- const rProp = find(rModel.properties, { name: ref.field })
185
- if (!rProp) {
186
- ignored.push(key)
187
- this.log.warn('notFound%s%s', this.t('property'), `${ref.field}@${ref.model}`)
188
- continue
189
- }
190
- ref.fields = ref.fields ?? '*'
191
- if (['*', 'all'].includes(ref.fields)) ref.fields = rModel.properties.map(p => p.name)
192
- if (ref.fields.length > 0 && !ref.fields.includes('id')) ref.fields.unshift('id')
193
- const removed = []
194
- for (const idx in ref.fields) {
195
- const p = find(rModel.properties, { name: ref.fields[idx] })
196
- if (!p) removed.push(ref.fields[idx])
197
- }
198
- pullAt(ref.fields, removed)
199
- prop.ref[key] = ref
200
- }
201
- for (const key of ignored) {
202
- delete prop.ref[key]
203
- }
204
- }
205
- const dupes = _refKeys.filter((item, index) => _refKeys.indexOf(item) !== index)
206
- if (dupes.length > 0) throw this.error('duplicateRefKeys%s%s', schema.name, dupes.join(', '))
207
- }
208
-
209
- /**
210
- * Sanitizes all model schemas, ensuring that properties, indexes, and references are properly defined and validated.
211
- * It also determines the sortable fields based on the defined indexes.
212
- * @async
213
- * @param {object} schema - The model schema
214
- * @returns {Promise<void>}
215
- */
216
- export async function sanitizeAll (schema) {
217
- const { runHook } = this.app.bajo
218
- const { pick, keys, map, uniq, camelCase, filter } = this.app.lib._
219
- const { defaultsDeep } = this.app.lib.aneka
220
- const allPropNames = uniq(map(schema.properties, 'name'))
221
- const propType = this.constructor.propertyType
222
- const indexTypes = this.constructor.indexTypes
223
-
224
- await runHook(`dobo.${camelCase(schema.name)}:beforeSanitizeModel`, schema)
225
- // properties
226
- for (const idx in schema.properties) {
227
- let prop = schema.properties[idx]
228
- const def = propType[prop.type]
229
- prop = pick(defaultsDeep(prop, def), this.getPropertyKeysByType(prop.type))
230
- if (!keys(propType).includes(prop.type)) this.fatal('unknownPropType%s%s', `${prop.name}.${def.name}`, prop.type)
231
- if (prop.type === 'string') {
232
- prop.minLength = parseInt(prop.minLength) ?? 0
233
- prop.maxLength = parseInt(prop.maxLength) ?? 255
234
- if (prop.minLength > 0) prop.required = true
235
- if (prop.minLength === 0) delete prop.minLength
236
- }
237
- schema.properties[idx] = prop
238
- }
239
- // indexes
240
- for (const index of schema.indexes) {
241
- if (!indexTypes.includes(index.type)) this.fatal('unknownIndexType%s%s', index.type, schema.name)
242
- for (const field of index.fields) {
243
- if (!allPropNames.includes(field)) this.fatal('unknownPropNameOnIndex%s%s', field, schema.name)
244
- }
245
- }
246
- await runHook(`dobo.${camelCase(schema.name)}:afterSanitizeModel`, schema)
247
- // sorting only possible using these fields
248
- schema.sortables = []
249
- for (const index of schema.indexes) {
250
- schema.sortables.push(...index.fields)
251
- }
252
- schema.hidden = filter(uniq(schema.hidden), prop => allPropNames.includes(prop))
253
- }
254
-
255
- /**
256
- * Creates a model schema by sanitizing its properties, features, and indexes, and ensuring that all references are valid.
257
- * It also applies any buildStart and buildEnd handlers defined in the schema.
258
- * @param {object} item - The model definition object
259
- * @returns {object} - The sanitized model schema
260
- */
261
- async function createSchema (item) {
262
- const { find, orderBy, get } = this.app.lib._
263
- const { mergeObjectsByKey, defaultsDeep, parseObject } = this.app.lib.aneka
264
- const feats = item.features ?? []
265
- const props = item.properties ?? []
266
- const indexes = item.indexes ?? []
267
- item.features = []
268
- item.properties = []
269
- item.indexes = []
270
- item.hidden = item.hidden ?? []
271
- item.rules = item.rules ?? []
272
- item.buildLevel = item.buildLevel ?? 999
273
- item.hooks = item.hooks ?? []
274
- item.disabled = item.disabled ?? []
275
- item.scanables = item.scanables ?? []
276
- if (item.disabled === 'all') item.disabled = ['find', 'get', 'create', 'update', 'remove']
277
- else if (item.disabled === 'readonly') item.disabled = ['create', 'update', 'remove']
278
- const conn = item.connection ?? 'default'
279
- if (!(conn instanceof this.app.baseClass.DoboConnection)) {
280
- item.connection = null
281
- // Is there any overwritten connection?
282
- const newConn = find(this.connections, c => c.options.models.includes(item.name))
283
- if (newConn) item.connection = newConn
284
- else {
285
- item.connection = this.getConnection(conn, true)
286
- if (!item.connection && conn === 'default') item.connection = this.getConnection('memory')
287
- }
288
- if (!item.connection) this.fatal('unknownConn%s%s', conn, item.name)
289
- }
290
- // cache settings
291
- const defCache = defaultsDeep({}, item.connection.options.cache, get(this, 'app.bajoCache.config.default', this.config.default.cache))
292
- if (item.cache === false) item.cache = { ttlDur: 0 }
293
- else if (item.cache === true) item.cache = defCache
294
- else item.cache = defaultsDeep({}, item.cache, defCache)
295
- item.cache = parseObject(item.cache)
296
- if (item.connection.name === 'memory') item.cache.ttlDur = 0
297
-
298
- // let's run
299
- await findAllProps.call(this, item, props, indexes)
300
- await findAllFeats.call(this, item, feats, indexes)
301
- await findAllIndexes.call(this, item, indexes)
302
- for (const key of ['properties', 'indexes']) {
303
- item[key] = mergeObjectsByKey(item[key], 'name')
304
- }
305
- item.hooks = orderBy(item.hooks, ['name', 'level'])
306
- delete item.features
307
- await sanitizeAll.call(this, item)
308
- return item
309
- }
310
-
311
- /**
312
- * Collects all model schemas from the application, sanitizes them, and creates model instances.
313
- * It also handles any buildStart and buildEnd handlers defined in the model definitions.
314
- * @async
315
- * @method
316
- * @returns {Promise<void>}
317
- */
318
- export async function collectModels () {
319
- const { eachPlugins, callHandler } = this.app.bajo
320
- const { orderBy, has, omit } = this.app.lib._
321
- await actionFactory.call(this)
322
- await modelFactory.call(this)
323
-
324
- this.log.trace('collecting%s', this.t('model'))
325
- const me = this
326
- let schemas = []
327
- await eachPlugins(async function ({ file }) {
328
- const { readConfig, callHandler } = this.app.bajo
329
- const { pascalCase } = this.app.lib.aneka
330
- const { isPlainObject, isEmpty, isArray } = this.app.lib._
331
-
332
- let items = await readConfig(file, { ns: this.ns, baseNs: me.ns, merge: 'concat' })
333
- if (isEmpty(items)) return undefined
334
- if (isPlainObject(items)) {
335
- items.baseName = items.baseName ?? path.basename(file, path.extname(file))
336
- items.name = items.name ?? pascalCase(`${this.alias} ${items.baseName}`)
337
- }
338
- if (!isArray(items)) items = [items]
339
- for (const item of items) {
340
- if (!item.baseName) me.fatal('missing%s%s', 'baseName', file)
341
- item.name = item.name ?? pascalCase(`${this.alias} ${item.baseName}`)
342
- me.log.trace('- %s', item.name)
343
- item.collName = item.collName ?? item.name
344
- item.options = item.options ?? {}
345
- item.options.attachment = item.options.attachment ?? true
346
- item.options.persistence = item.options.persistence ?? true
347
- item.ns = this.ns
348
- if (item.buildStart) await callHandler(this, item.buildStart, item)
349
- const schema = await createSchema.call(me, item)
350
- schemas.push(schema)
351
- }
352
- }, { glob: ['model/*.*', 'model.*'], prefix: this.ns })
353
- schemas = orderBy(schemas, ['buildLevel', 'name'])
354
- for (const schema of schemas) {
355
- const idProp = schema.properties.find(p => p.name === 'id')
356
- if (!this.constructor.idTypes.includes(idProp.type)) this.fatal('invalidIdType%s%s', schema.name, this.constructor.idTypes.join(', '))
357
- if (idProp.type === 'string' && !has(idProp, 'maxLength')) idProp.maxLength = 50
358
- const plugin = this.app[schema.ns]
359
- await sanitizeRef.call(this, schema, schemas)
360
- for (const item of schema.indexes) {
361
- for (const field of item.fields) {
362
- const prop = schema.properties.find(p => p.name === field)
363
- if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'index', schema.name)
364
- }
365
- }
366
- for (const field of schema.scanables) {
367
- const prop = schema.properties.find(p => p.name === field)
368
- if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'scanable', schema.name)
369
- }
370
- const model = new this.app.baseClass.DoboModel(plugin, omit(schema, ['beforeCreate', 'afterCreate']))
371
- me.models.push(model)
372
- if (schema.buildEnd) await callHandler(plugin, schema.buildEnd, model)
373
- }
374
- schemas = []
375
- this.log.debug('collected%s%d', this.t('model'), this.models.length)
376
- }
377
-
378
- /**
379
- * Collects all model features from loaded plugins
380
- * @async
381
- * @method
382
- * @returns {Promise<void>}
383
- */
384
- export async function collectFeatures () {
385
- const { eachPlugins } = this.app.bajo
386
- await featureFactory.call(this)
387
-
388
- this.log.trace('collecting%s', this.t('feature'))
389
- const me = this
390
- await eachPlugins(async function ({ file }) {
391
- const { importModule } = this.app.bajo
392
- const { camelCase, isFunction } = this.app.lib._
393
-
394
- const name = camelCase(path.basename(file, '.js'))
395
- const handler = await importModule(file)
396
- if (!isFunction(handler)) this.fatal('invalidFeatureHandler%s%s', this.ns, name)
397
- me.features.push(new this.app.baseClass.DoboFeature(this, { name, handler }))
398
- me.log.trace('- %s:%s', this.ns, name)
399
- }, { glob: 'feature/*.js', prefix: this.ns })
400
- this.log.debug('collected%s%d', this.t('feature'), this.features.length)
401
- }
402
-
403
- /**
404
- * Collects all database connections from loaded plugins, initializes their adapters, and ensures that a default connection is available.
405
- * It also handles any memory-based connections and merges their models into a single memory connection.
406
- * @async
407
- * @method
408
- * @returns {Promise<void>}
409
- */
410
- export async function collectConnections () {
411
- const { buildCollections } = this.app.bajo
412
- const { pullAt } = this.app.lib._
413
- const { filterIndex } = this.app.lib.aneka
414
- await connectionFactory.call(this)
415
-
416
- async function handler ({ item }) {
417
- const { has } = this.app.lib._
418
- if (!has(item, 'adapter')) this.fatal('unknownDbAdapter%s')
419
- const conn = new this.app.baseClass.DoboConnection(this, item)
420
- await conn.initAdapter(item.adapter)
421
- return conn
422
- }
423
- const memIndexes = filterIndex(this.config.connections, current => current.adapter === 'dobo:memory' || current.name === 'memory')
424
- const models = memIndexes.map(idx => [...(this.config.connections[idx].models ?? [])])
425
- pullAt(this.config.connections, memIndexes)
426
- this.config.connections.push({
427
- adapter: 'dobo:memory',
428
- name: 'memory',
429
- models
430
- })
431
- this.connections = await buildCollections({ ns: this.ns, container: 'connections', handler, dupChecks: ['name'] })
432
- const defConn = this.connections.find(conn => conn.name === 'default')
433
- if (!defConn) this.log.warn('noDefaultConnection')
434
- }
435
-
436
- /**
437
- * Collects all database adapters from loaded plugins.
438
- * @async
439
- * @method
440
- * @returns {Promise<void>}
441
- */
442
- export async function collectAdapters () {
443
- const { eachPlugins, runHook } = this.app.bajo
444
- const { importModule } = this.app.bajo
445
- const { camelCase, isFunction } = this.app.lib._
446
- await adapterFactory.call(this)
447
-
448
- this.log.trace('collecting%s', this.t('adapter'))
449
- const me = this
450
- await runHook(`${this.ns}:beforeCollectAdapters`)
451
- await eachPlugins(async function ({ file }) {
452
- const name = camelCase(path.basename(file, '.js'))
453
- const factory = await importModule(file)
454
- if (!isFunction(factory)) this.fatal('invalidAdapterClassFactory%s%s', this.ns, name)
455
- const Cls = await factory.call(this)
456
- const instance = new Cls(this, name)
457
- if (!(instance instanceof this.app.baseClass.DoboAdapter)) this.fatal('invalidAdapterClass%s%s', this.ns, name)
458
- me.adapters.push(instance)
459
- me.log.trace('- %s:%s', this.ns, name)
460
- }, { glob: 'adapter/*.js', prefix: this.ns })
461
- await runHook(`${this.ns}:afterCollectAdapters`)
462
- this.log.debug('collected%s%d', this.t('adapter'), this.adapters.length)
463
- }
1
+ import path from 'path'
2
+ import adapterFactory from './factory/adapter.js'
3
+ import connectionFactory from './factory/connection.js'
4
+ import featureFactory from './factory/feature.js'
5
+ import modelFactory from './factory/model.js'
6
+ import actionFactory from './factory/action.js'
7
+
8
+ /**
9
+ * @module Helper
10
+ */
11
+
12
+ /**
13
+ * Sanitizes a property definition for a model, ensuring it adheres to the expected structure and types.
14
+ * It handles various property attributes such as type, index, required status, and more.
15
+ *
16
+ * If the property is virtual, it processes it accordingly; otherwise, it adds it to the model's properties
17
+ * and indexes.
18
+ * @param {DoboModel} model
19
+ * @param {DoboModel.TProperty} prop
20
+ * @param {DoboModel.TIndex[]} indexes
21
+ */
22
+ async function sanitizeProp (model, prop, indexes) {
23
+ const { isEmpty, isString, keys, pick, isArray, isPlainObject, omit, isFunction } = this.app.lib._
24
+ const allPropKeys = this.getAllPropertyKeys(model.connection.adapter)
25
+ const propType = this.constructor.propertyType
26
+ if (isString(prop)) {
27
+ let [name, type, maxLength, idx, required] = prop.split(',').map(m => m.trim())
28
+ if (isEmpty(type)) type = 'string'
29
+ maxLength = isEmpty(maxLength) ? propType.string.maxLength : parseInt(maxLength)
30
+ prop = { name, type, maxLength }
31
+ if (!isEmpty(idx)) prop.index = idx
32
+ prop.required = required === 'true'
33
+ }
34
+ prop.type = prop.type ?? 'string'
35
+ if (isArray(prop.values)) {
36
+ prop.values = prop.values.map(item => {
37
+ if (isPlainObject(item)) return pick(item, ['value', 'text'])
38
+ return { value: item, text: item }
39
+ })
40
+ } else if (!(isString(prop.values) || isFunction(prop.values))) delete prop.values
41
+ if (prop.hidden) model.hidden.push(prop.name)
42
+ if (prop.scanable) model.scanables.push(prop.scanable)
43
+ if (prop.virtual) {
44
+ const keys = Object.keys(propType)
45
+ if (!keys.includes(prop.type)) this.fatal('unknownPropType%s%s', `${prop.name}:${prop.type}`, model.name)
46
+ for (const key of ['required', 'rules', 'index', 'validator', 'ref', 'rulesMsg', 'immutable']) {
47
+ delete prop[key]
48
+ }
49
+ model.properties.push(prop)
50
+ } else {
51
+ if (prop.index) {
52
+ if (prop.index === true || prop.index === 'true') prop.index = 'index'
53
+ const [idx, idxName] = prop.index.split(':')
54
+ const index = { name: idxName ?? `${model.collName}_${prop.name}_${idx}`, fields: [prop.name], type: idx }
55
+ indexes.push(index)
56
+ }
57
+ if (keys(propType).includes(prop.type)) model.properties.push(pick(prop, allPropKeys))
58
+ else {
59
+ const feature = this.getFeature(prop.type)
60
+ if (!feature) this.fatal('unknownPropType%s%s', prop.type, model.name)
61
+ const opts = omit(prop, ['name', 'type'])
62
+ opts.field = prop.name
63
+ await applyFeature.call(this, model, feature, opts, indexes)
64
+ }
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Finds and sanitizes all properties for a given model schema, ensuring that the 'id' property is included and
70
+ * that all properties are properly processed.
71
+ * @param {object} schema - The model schema
72
+ * @param {array} inputs - The list of input properties to process
73
+ * @param {array} indexes - The list of indexes to update
74
+ */
75
+ async function findAllProps (schema, inputs = [], indexes = []) {
76
+ const { isPlainObject, cloneDeep } = this.app.lib._
77
+ const isIdProp = inputs.find(p => {
78
+ return isPlainObject(p) ? p.name === 'id' : p.startsWith('id,')
79
+ })
80
+ if (!isIdProp) {
81
+ const idField = cloneDeep(schema.connection.adapter.idField)
82
+ idField.name = 'id'
83
+ inputs.unshift(idField)
84
+ }
85
+ for (const prop of inputs) {
86
+ await sanitizeProp.call(this, schema, prop, indexes)
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Applies a feature to a model schema, adding properties, rules, hooks, and other
92
+ * configurations as defined by the feature.
93
+ * @param {object} schema - The model schema
94
+ * @param {object} feature - The feature to apply
95
+ * @param {object} options - The options for the feature
96
+ * @param {array} indexes - The list of indexes to update
97
+ */
98
+ async function applyFeature (schema, feature, options, indexes) {
99
+ const { isArray, findIndex } = this.app.lib._
100
+ if (feature.name === 'dobo:unique' && options.field === 'id') {
101
+ const idx = findIndex(schema.properties, { name: 'id' })
102
+ if (idx > -1) schema.properties.pullAt(idx)
103
+ }
104
+ const item = await feature.handler(options)
105
+ if (item.rules) schema.rules.push(...item.rules)
106
+ if (item.scanables) schema.scanables.push(...item.scanables)
107
+ if (!isArray(item.properties)) item.properties = [item.properties]
108
+ for (const prop of item.properties) {
109
+ prop.feature = `${feature.plugin.ns}:${feature.name}`
110
+ await sanitizeProp.call(this, schema, prop, indexes)
111
+ }
112
+ if (item.hooks) {
113
+ item.hooks = item.hooks.map(hook => {
114
+ hook.level = hook.level ?? 999
115
+ return hook
116
+ })
117
+ schema.hooks.push(...item.hooks)
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Finds and applies all features for a given model schema, ensuring that each feature is properly processed
123
+ * @param {object} schema - The model schema
124
+ * @param {array} inputs - The list of features to apply
125
+ * @param {array} indexes - The list of indexes to update
126
+ */
127
+ async function findAllFeats (schema, inputs = [], indexes = []) {
128
+ const { isString, omit } = this.app.lib._
129
+ for (let feat of inputs) {
130
+ if (isString(feat)) feat = { name: feat }
131
+ const featName = feat.name.indexOf(':') === -1 ? `dobo:${feat.name}` : feat.name
132
+ const feature = this.app.dobo.getFeature(featName)
133
+ if (!feature) this.fatal('invalidFeature%s%s', schema.name, featName)
134
+ await applyFeature.call(this, schema, feature, omit(feat, 'name'), indexes)
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Finds and applies all indexes for a given model schema, ensuring that each index is properly processed
140
+ * @param {object} schema - The model schema
141
+ * @param {array} inputs - The list of indexes to apply
142
+ * @param {array} indexes - The list of existing indexes to update
143
+ */
144
+ async function findAllIndexes (schema, inputs = [], indexes = []) {
145
+ const items = []
146
+ for (const index of inputs) {
147
+ index.type = index.type ?? 'index'
148
+ index.fields = index.fields ?? []
149
+ if (!index.name) index.name = `${schema.name}_${index.fields.join('_')}_${index.type}`
150
+ items.push(index)
151
+ }
152
+ schema.indexes = [...items, ...indexes]
153
+ }
154
+
155
+ /**
156
+ * Sanitizes the references in a model schema, ensuring that all references point to valid models and properties.
157
+ * It checks for duplicate reference keys and validates the existence of referenced models and properties.
158
+ * @async
159
+ * @param {object} schema - The model schema to sanitize
160
+ * @param {object[]} schemas - The list of all model schemas
161
+ * @returns {Promise<void>}
162
+ */
163
+ export async function sanitizeRef (schema, schemas) {
164
+ const { find, isString, pullAt } = this.app.lib._
165
+ const _refKeys = []
166
+ for (const prop of schema.properties) {
167
+ const ignored = []
168
+ for (const key in prop.ref ?? {}) {
169
+ _refKeys.push(key)
170
+ let ref = prop.ref[key]
171
+ if (isString(ref)) {
172
+ ref = { field: ref }
173
+ }
174
+ ref.field = ref.field ?? 'id'
175
+ ref.type = ref.type ?? '1:1'
176
+ ref.searchField = ref.searchField ?? ref.field
177
+ ref.labelField = ref.labelField ?? ref.searchField
178
+ const rModel = find(schemas, { name: ref.model })
179
+ if (!rModel) {
180
+ ignored.push(key)
181
+ // this.log.warn('notFound%s%s', this.t('model'), ref.model)
182
+ continue
183
+ }
184
+ const rProp = find(rModel.properties, { name: ref.field })
185
+ if (!rProp) {
186
+ ignored.push(key)
187
+ this.log.warn('notFound%s%s', this.t('property'), `${ref.field}@${ref.model}`)
188
+ continue
189
+ }
190
+ ref.fields = ref.fields ?? '*'
191
+ if (['*', 'all'].includes(ref.fields)) ref.fields = rModel.properties.map(p => p.name)
192
+ if (ref.fields.length > 0 && !ref.fields.includes('id')) ref.fields.unshift('id')
193
+ const removed = []
194
+ for (const idx in ref.fields) {
195
+ const p = find(rModel.properties, { name: ref.fields[idx] })
196
+ if (!p) removed.push(ref.fields[idx])
197
+ }
198
+ pullAt(ref.fields, removed)
199
+ prop.ref[key] = ref
200
+ }
201
+ for (const key of ignored) {
202
+ delete prop.ref[key]
203
+ }
204
+ }
205
+ const dupes = _refKeys.filter((item, index) => _refKeys.indexOf(item) !== index)
206
+ if (dupes.length > 0) throw this.error('duplicateRefKeys%s%s', schema.name, dupes.join(', '))
207
+ }
208
+
209
+ /**
210
+ * Sanitizes all model schemas, ensuring that properties, indexes, and references are properly defined and validated.
211
+ * It also determines the sortable fields based on the defined indexes.
212
+ * @async
213
+ * @param {object} schema - The model schema
214
+ * @returns {Promise<void>}
215
+ */
216
+ export async function sanitizeAll (schema) {
217
+ const { runHook } = this.app.bajo
218
+ const { pick, keys, map, uniq, camelCase, filter } = this.app.lib._
219
+ const { defaultsDeep } = this.app.lib.aneka
220
+ const allPropNames = uniq(map(schema.properties, 'name'))
221
+ const propType = this.constructor.propertyType
222
+ const indexTypes = this.constructor.indexTypes
223
+
224
+ await runHook(`dobo.${camelCase(schema.name)}:beforeSanitizeModel`, schema)
225
+ // properties
226
+ for (const idx in schema.properties) {
227
+ let prop = schema.properties[idx]
228
+ const def = propType[prop.type]
229
+ prop = pick(defaultsDeep(prop, def), this.getPropertyKeysByType(prop.type))
230
+ if (!keys(propType).includes(prop.type)) this.fatal('unknownPropType%s%s', `${prop.name}.${def.name}`, prop.type)
231
+ if (prop.type === 'string') {
232
+ prop.minLength = parseInt(prop.minLength) ?? 0
233
+ prop.maxLength = parseInt(prop.maxLength) ?? 255
234
+ if (prop.minLength > 0) prop.required = true
235
+ if (prop.minLength === 0) delete prop.minLength
236
+ }
237
+ schema.properties[idx] = prop
238
+ }
239
+ // indexes
240
+ for (const index of schema.indexes) {
241
+ if (!indexTypes.includes(index.type)) this.fatal('unknownIndexType%s%s', index.type, schema.name)
242
+ for (const field of index.fields) {
243
+ if (!allPropNames.includes(field)) this.fatal('unknownPropNameOnIndex%s%s', field, schema.name)
244
+ }
245
+ }
246
+ await runHook(`dobo.${camelCase(schema.name)}:afterSanitizeModel`, schema)
247
+ // sorting only possible using these fields
248
+ schema.sortables = []
249
+ for (const index of schema.indexes) {
250
+ schema.sortables.push(...index.fields)
251
+ }
252
+ schema.hidden = filter(uniq(schema.hidden), prop => allPropNames.includes(prop))
253
+ }
254
+
255
+ /**
256
+ * Creates a model schema by sanitizing its properties, features, and indexes, and ensuring that all references are valid.
257
+ * It also applies any buildStart and buildEnd handlers defined in the schema.
258
+ * @param {object} item - The model definition object
259
+ * @returns {object} - The sanitized model schema
260
+ */
261
+ async function createSchema (item) {
262
+ const { find, orderBy, get } = this.app.lib._
263
+ const { mergeObjectsByKey, defaultsDeep, parseObject } = this.app.lib.aneka
264
+ const feats = item.features ?? []
265
+ const props = item.properties ?? []
266
+ const indexes = item.indexes ?? []
267
+ item.features = []
268
+ item.properties = []
269
+ item.indexes = []
270
+ item.hidden = item.hidden ?? []
271
+ item.rules = item.rules ?? []
272
+ item.buildLevel = item.buildLevel ?? 999
273
+ item.hooks = item.hooks ?? []
274
+ item.disabled = item.disabled ?? []
275
+ item.scanables = item.scanables ?? []
276
+ if (item.disabled === 'all') item.disabled = ['find', 'get', 'create', 'update', 'remove']
277
+ else if (item.disabled === 'readonly') item.disabled = ['create', 'update', 'remove']
278
+ const conn = item.connection ?? 'default'
279
+ if (!(conn instanceof this.app.baseClass.DoboConnection)) {
280
+ item.connection = null
281
+ // Is there any overwritten connection?
282
+ const newConn = find(this.connections, c => c.options.models.includes(item.name))
283
+ if (newConn) item.connection = newConn
284
+ else {
285
+ item.connection = this.getConnection(conn, true)
286
+ if (!item.connection && conn === 'default') item.connection = this.getConnection('memory')
287
+ }
288
+ if (!item.connection) this.fatal('unknownConn%s%s', conn, item.name)
289
+ }
290
+ // cache settings
291
+ const defCache = defaultsDeep({}, item.connection.options.cache, get(this, 'app.bajoCache.config.default', this.config.default.cache))
292
+ if (item.cache === false) item.cache = { ttlDur: 0 }
293
+ else if (item.cache === true) item.cache = defCache
294
+ else item.cache = defaultsDeep({}, item.cache, defCache)
295
+ item.cache = parseObject(item.cache)
296
+ if (item.connection.name === 'memory') item.cache.ttlDur = 0
297
+
298
+ // let's run
299
+ await findAllProps.call(this, item, props, indexes)
300
+ await findAllFeats.call(this, item, feats, indexes)
301
+ await findAllIndexes.call(this, item, indexes)
302
+ for (const key of ['properties', 'indexes']) {
303
+ item[key] = mergeObjectsByKey(item[key], 'name')
304
+ }
305
+ item.hooks = orderBy(item.hooks, ['name', 'level'])
306
+ delete item.features
307
+ await sanitizeAll.call(this, item)
308
+ return item
309
+ }
310
+
311
+ /**
312
+ * Collects all model schemas from the application, sanitizes them, and creates model instances.
313
+ * It also handles any buildStart and buildEnd handlers defined in the model definitions.
314
+ * @async
315
+ * @method
316
+ * @returns {Promise<void>}
317
+ */
318
+ export async function collectModels () {
319
+ const { eachPlugins, callHandler } = this.app.bajo
320
+ const { orderBy, has, omit } = this.app.lib._
321
+ await actionFactory.call(this)
322
+ await modelFactory.call(this)
323
+
324
+ this.log.trace('collecting%s', this.t('model'))
325
+ const me = this
326
+ let schemas = []
327
+ await eachPlugins(async function ({ file }) {
328
+ const { readConfig, callHandler } = this.app.bajo
329
+ const { pascalCase } = this.app.lib.aneka
330
+ const { isPlainObject, isEmpty, isArray } = this.app.lib._
331
+
332
+ let items = await readConfig(file, { ns: this.ns, baseNs: me.ns, merge: 'concat' })
333
+ if (isEmpty(items)) return undefined
334
+ if (isPlainObject(items)) {
335
+ items.baseName = items.baseName ?? path.basename(file, path.extname(file))
336
+ items.name = items.name ?? pascalCase(`${this.alias} ${items.baseName}`)
337
+ }
338
+ if (!isArray(items)) items = [items]
339
+ for (const item of items) {
340
+ if (!item.baseName) me.fatal('missing%s%s', 'baseName', file)
341
+ item.name = item.name ?? pascalCase(`${this.alias} ${item.baseName}`)
342
+ me.log.trace('- %s', item.name)
343
+ item.collName = item.collName ?? item.name
344
+ item.options = item.options ?? {}
345
+ item.options.attachment = item.options.attachment ?? true
346
+ item.options.persistence = item.options.persistence ?? true
347
+ item.ns = this.ns
348
+ if (item.buildStart) await callHandler(this, item.buildStart, item)
349
+ const schema = await createSchema.call(me, item)
350
+ schemas.push(schema)
351
+ }
352
+ }, { glob: ['model/*.*', 'model.*'], prefix: this.ns })
353
+ schemas = orderBy(schemas, ['buildLevel', 'name'])
354
+ for (const schema of schemas) {
355
+ const idProp = schema.properties.find(p => p.name === 'id')
356
+ if (!this.constructor.idTypes.includes(idProp.type)) this.fatal('invalidIdType%s%s', schema.name, this.constructor.idTypes.join(', '))
357
+ if (idProp.type === 'string' && !has(idProp, 'maxLength')) idProp.maxLength = 50
358
+ const plugin = this.app[schema.ns]
359
+ await sanitizeRef.call(this, schema, schemas)
360
+ for (const item of schema.indexes) {
361
+ for (const field of item.fields) {
362
+ const prop = schema.properties.find(p => p.name === field)
363
+ if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'index', schema.name)
364
+ }
365
+ }
366
+ for (const field of schema.scanables) {
367
+ const prop = schema.properties.find(p => p.name === field)
368
+ if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'scanable', schema.name)
369
+ }
370
+ const model = new this.app.baseClass.DoboModel(plugin, omit(schema, ['beforeCreate', 'afterCreate']))
371
+ me.models.push(model)
372
+ if (schema.buildEnd) await callHandler(plugin, schema.buildEnd, model)
373
+ }
374
+ schemas = []
375
+ this.log.debug('collected%s%d', this.t('model'), this.models.length)
376
+ }
377
+
378
+ /**
379
+ * Collects all model features from loaded plugins
380
+ * @async
381
+ * @method
382
+ * @returns {Promise<void>}
383
+ */
384
+ export async function collectFeatures () {
385
+ const { eachPlugins } = this.app.bajo
386
+ await featureFactory.call(this)
387
+
388
+ this.log.trace('collecting%s', this.t('feature'))
389
+ const me = this
390
+ await eachPlugins(async function ({ file }) {
391
+ const { importModule } = this.app.bajo
392
+ const { camelCase, isFunction } = this.app.lib._
393
+
394
+ const name = camelCase(path.basename(file, '.js'))
395
+ const handler = await importModule(file)
396
+ if (!isFunction(handler)) this.fatal('invalidFeatureHandler%s%s', this.ns, name)
397
+ me.features.push(new this.app.baseClass.DoboFeature(this, { name, handler }))
398
+ me.log.trace('- %s:%s', this.ns, name)
399
+ }, { glob: 'feature/*.js', prefix: this.ns })
400
+ this.log.debug('collected%s%d', this.t('feature'), this.features.length)
401
+ }
402
+
403
+ /**
404
+ * Collects all database connections from loaded plugins, initializes their adapters, and ensures that a default connection is available.
405
+ * It also handles any memory-based connections and merges their models into a single memory connection.
406
+ * @async
407
+ * @method
408
+ * @returns {Promise<void>}
409
+ */
410
+ export async function collectConnections () {
411
+ const { buildCollections } = this.app.bajo
412
+ const { pullAt } = this.app.lib._
413
+ const { filterIndex } = this.app.lib.aneka
414
+ await connectionFactory.call(this)
415
+
416
+ async function handler ({ item }) {
417
+ const { has } = this.app.lib._
418
+ if (!has(item, 'adapter')) this.fatal('unknownDbAdapter%s')
419
+ const conn = new this.app.baseClass.DoboConnection(this, item)
420
+ await conn.initAdapter(item.adapter)
421
+ return conn
422
+ }
423
+ const memIndexes = filterIndex(this.config.connections, current => current.adapter === 'dobo:memory' || current.name === 'memory')
424
+ const models = memIndexes.map(idx => [...(this.config.connections[idx].models ?? [])])
425
+ pullAt(this.config.connections, memIndexes)
426
+ this.config.connections.push({
427
+ adapter: 'dobo:memory',
428
+ name: 'memory',
429
+ models
430
+ })
431
+ this.connections = await buildCollections({ ns: this.ns, container: 'connections', handler, dupChecks: ['name'] })
432
+ const defConn = this.connections.find(conn => conn.name === 'default')
433
+ if (!defConn) this.log.warn('noDefaultConnection')
434
+ }
435
+
436
+ /**
437
+ * Collects all database adapters from loaded plugins.
438
+ * @async
439
+ * @method
440
+ * @returns {Promise<void>}
441
+ */
442
+ export async function collectAdapters () {
443
+ const { eachPlugins, runHook } = this.app.bajo
444
+ const { importModule } = this.app.bajo
445
+ const { camelCase, isFunction } = this.app.lib._
446
+ await adapterFactory.call(this)
447
+
448
+ this.log.trace('collecting%s', this.t('adapter'))
449
+ const me = this
450
+ await runHook(`${this.ns}:beforeCollectAdapters`)
451
+ await eachPlugins(async function ({ file }) {
452
+ const name = camelCase(path.basename(file, '.js'))
453
+ const factory = await importModule(file)
454
+ if (!isFunction(factory)) this.fatal('invalidAdapterClassFactory%s%s', this.ns, name)
455
+ const Cls = await factory.call(this)
456
+ const instance = new Cls(this, name)
457
+ if (!(instance instanceof this.app.baseClass.DoboAdapter)) this.fatal('invalidAdapterClass%s%s', this.ns, name)
458
+ me.adapters.push(instance)
459
+ me.log.trace('- %s:%s', this.ns, name)
460
+ }, { glob: 'adapter/*.js', prefix: this.ns })
461
+ await runHook(`${this.ns}:afterCollectAdapters`)
462
+ this.log.debug('collected%s%d', this.t('adapter'), this.adapters.length)
463
+ }