dobo 2.30.7 → 2.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.jsdoc.conf.json +1 -1
  2. package/docs/Dobo.html +1 -24
  3. package/docs/DoboAction.html +13 -0
  4. package/docs/DoboAdapter.html +3 -0
  5. package/docs/DoboConnection.html +3 -0
  6. package/docs/DoboFeature.html +3 -0
  7. package/docs/DoboModel.html +51 -0
  8. package/docs/data/search.json +1 -1
  9. package/docs/extend_bajo_hook-docs.js.html +241 -0
  10. package/docs/external-Tools.html +3 -0
  11. package/docs/global.html +1 -5
  12. package/docs/index.html +1 -1
  13. package/docs/index.js.html +434 -380
  14. package/docs/lib_factory_action.js.html +290 -0
  15. package/docs/lib_factory_adapter.js.html +1057 -0
  16. package/docs/lib_factory_connection.js.html +112 -0
  17. package/docs/lib_factory_feature.js.html +55 -0
  18. package/docs/lib_factory_model.js.html +503 -0
  19. package/docs/lib_factory_model_build.js.html +28 -0
  20. package/docs/lib_factory_model_clear-record.js.html +33 -0
  21. package/docs/lib_factory_model_count-record.js.html +45 -0
  22. package/docs/lib_factory_model_create-record.js.html +52 -0
  23. package/docs/lib_factory_model_drop.js.html +29 -0
  24. package/docs/lib_factory_model_exists.js.html +29 -0
  25. package/docs/lib_factory_model_find-all-record.js.html +115 -0
  26. package/docs/lib_factory_model_find-one-record.js.html +41 -0
  27. package/docs/lib_factory_model_find-record.js.html +85 -0
  28. package/docs/lib_factory_model_get-record.js.html +55 -0
  29. package/docs/lib_factory_model_helper.js.html +579 -0
  30. package/docs/lib_factory_model_remove-record.js.html +47 -0
  31. package/docs/lib_factory_model_sanitize-body.js.html +60 -0
  32. package/docs/lib_factory_model_sanitize-fixture.js.html +59 -0
  33. package/docs/lib_factory_model_sanitize-record.js.html +73 -0
  34. package/docs/lib_factory_model_update-record.js.html +61 -0
  35. package/docs/lib_factory_model_upsert-record.js.html +74 -0
  36. package/docs/{method_validate.js.html → lib_factory_model_validate.js.html} +109 -73
  37. package/docs/lib_helper.js.html +466 -0
  38. package/docs/module-Helper.html +3 -0
  39. package/docs/module-Helper_Model.html +3 -0
  40. package/docs/module-Hook.html +15 -0
  41. package/docs/scripts/core.js +477 -476
  42. package/docs/scripts/resize.js +36 -36
  43. package/docs/scripts/search.js +105 -105
  44. package/docs/scripts/third-party/fuse.js +1 -1
  45. package/docs/scripts/third-party/hljs-line-num-original.js +285 -282
  46. package/docs/scripts/third-party/hljs-line-num.js +1 -1
  47. package/docs/scripts/third-party/hljs-original.js +1202 -1195
  48. package/docs/scripts/third-party/hljs.js +1 -1
  49. package/docs/scripts/third-party/popper.js +1 -1
  50. package/docs/scripts/third-party/tippy.js +1 -1
  51. package/docs/scripts/third-party/tocbot.js +509 -508
  52. package/docs/static/home.md +3 -1
  53. package/extend/bajo/hook-docs.js +238 -0
  54. package/extend/bajo/intl/en-US.json +9 -9
  55. package/extend/bajo/intl/id.json +9 -9
  56. package/extend/bajoCli/applet/rebuild-model.js +2 -2
  57. package/extend/dobo/{driver → adapter}/memory.js +6 -6
  58. package/extend/dobo/feature/immutable.js +2 -2
  59. package/extend/dobo/feature/removed-at.js +1 -1
  60. package/index.js +33 -30
  61. package/lib/factory/action.js +145 -18
  62. package/lib/factory/adapter.js +1054 -0
  63. package/lib/factory/connection.js +46 -22
  64. package/lib/factory/feature.js +23 -3
  65. package/lib/factory/model/build.js +11 -2
  66. package/lib/factory/model/bulk-create-record.js +2 -2
  67. package/lib/factory/model/clear-record.js +13 -3
  68. package/lib/factory/model/count-record.js +13 -2
  69. package/lib/factory/model/create-aggregate.js +2 -2
  70. package/lib/factory/model/create-attachment.js +1 -1
  71. package/lib/factory/model/create-histogram.js +2 -2
  72. package/lib/factory/model/create-record.js +15 -2
  73. package/lib/factory/model/drop.js +12 -2
  74. package/lib/factory/model/exists.js +10 -6
  75. package/lib/factory/model/find-all-record.js +14 -3
  76. package/lib/factory/model/find-attachment.js +1 -1
  77. package/lib/factory/model/find-one-record.js +13 -1
  78. package/lib/factory/model/find-record.js +17 -53
  79. package/lib/factory/model/get-record.js +11 -39
  80. package/lib/factory/model/{_util.js → helper.js} +176 -16
  81. package/lib/factory/model/remove-attachment.js +1 -1
  82. package/lib/factory/model/remove-record.js +11 -28
  83. package/lib/factory/model/sanitize-body.js +14 -12
  84. package/lib/factory/model/sanitize-fixture.js +17 -4
  85. package/lib/factory/model/sanitize-record.js +9 -6
  86. package/lib/factory/model/transaction.js +2 -2
  87. package/lib/factory/model/update-record.js +12 -40
  88. package/lib/factory/model/upsert-record.js +13 -42
  89. package/lib/factory/model/validate.js +47 -14
  90. package/lib/factory/model.js +301 -23
  91. package/lib/{collect-models.js → helper.js} +463 -364
  92. package/package.json +1 -1
  93. package/wiki/CHANGES.md +17 -7
  94. package/wiki/ECOSYSTEM.md +7 -7
  95. package/wiki/GETTING-STARTED.md +3 -3
  96. package/wiki/QUERY-LANGUAGE.md +1 -1
  97. package/docs/lib_collect-connections.js.html +0 -39
  98. package/docs/lib_collect-drivers.js.html +0 -52
  99. package/docs/lib_collect-features.js.html +0 -36
  100. package/docs/lib_collect-schemas.js.html +0 -94
  101. package/docs/lib_index.js.html +0 -6
  102. package/docs/method_model_create.js.html +0 -35
  103. package/docs/method_model_drop.js.html +0 -34
  104. package/docs/method_model_exists.js.html +0 -40
  105. package/docs/method_record_count.js.html +0 -69
  106. package/docs/method_record_create.js.html +0 -114
  107. package/docs/method_record_find-all.js.html +0 -44
  108. package/docs/method_record_find-one.js.html +0 -73
  109. package/docs/method_record_find.js.html +0 -118
  110. package/docs/method_record_get.js.html +0 -92
  111. package/docs/method_record_remove.js.html +0 -75
  112. package/docs/method_record_update.js.html +0 -107
  113. package/docs/method_record_upsert.js.html +0 -54
  114. package/docs/method_sanitize_body.js.html +0 -88
  115. package/docs/method_sanitize_date.js.html +0 -30
  116. package/docs/method_sanitize_id.js.html +0 -20
  117. package/docs/module-Lib.html +0 -3
  118. package/lib/collect-connections.js +0 -56
  119. package/lib/collect-drivers.js +0 -35
  120. package/lib/collect-features.js +0 -40
  121. package/lib/factory/driver.js +0 -574
  122. package/lib/factory/model/sanitize-id.js +0 -7
  123. package/lib/index.js +0 -3
@@ -1,364 +1,463 @@
1
- import path from 'path'
2
- import modelFactory from './factory/model.js'
3
- import actionFactory from './factory/action.js'
4
-
5
- /**
6
- * Sanitize one single property of a model
7
- *
8
- * @param {Object} model - Loaded model
9
- * @param {Object} prop - Property to check
10
- * @param {Array} [indexes] - Container array to fill up found index
11
- */
12
- async function sanitizeProp (model, prop, indexes) {
13
- const { isEmpty, isString, keys, pick, isArray, isPlainObject, omit, isFunction } = this.app.lib._
14
- const allPropKeys = this.getAllPropertyKeys(model.connection.driver)
15
- const propType = this.constructor.propertyType
16
- if (isString(prop)) {
17
- let [name, type, maxLength, idx, required] = prop.split(',').map(m => m.trim())
18
- if (isEmpty(type)) type = 'string'
19
- maxLength = isEmpty(maxLength) ? propType.string.maxLength : parseInt(maxLength)
20
- prop = { name, type, maxLength }
21
- if (!isEmpty(idx)) prop.index = idx
22
- prop.required = required === 'true'
23
- }
24
- prop.type = prop.type ?? 'string'
25
- if (isArray(prop.values)) {
26
- prop.values = prop.values.map(item => {
27
- if (isPlainObject(item)) return pick(item, ['value', 'text'])
28
- return { value: item, text: item }
29
- })
30
- } else if (!(isString(prop.values) || isFunction(prop.values))) delete prop.values
31
- if (prop.hidden) model.hidden.push(prop.name)
32
- if (prop.scanable) model.scanables.push(prop.scanable)
33
- if (prop.virtual) {
34
- const keys = Object.keys(propType)
35
- if (!keys.includes(prop.type)) this.fatal('unknownPropType%s%s', `${prop.name}:${prop.type}`, model.name)
36
- for (const key of ['required', 'rules', 'index', 'validator', 'ref', 'rulesMsg', 'immutable']) {
37
- delete prop[key]
38
- }
39
- model.properties.push(prop)
40
- } else {
41
- if (prop.index) {
42
- if (prop.index === true || prop.index === 'true') prop.index = 'index'
43
- const [idx, idxName] = prop.index.split(':')
44
- const index = { name: idxName ?? `${model.collName}_${prop.name}_${idx}`, fields: [prop.name], type: idx }
45
- indexes.push(index)
46
- }
47
- if (keys(propType).includes(prop.type)) model.properties.push(pick(prop, allPropKeys))
48
- else {
49
- const feature = this.getFeature(prop.type)
50
- if (!feature) this.fatal('unknownPropType%s%s', prop.type, model.name)
51
- const opts = omit(prop, ['name', 'type'])
52
- opts.field = prop.name
53
- await applyFeature.call(this, model, feature, opts, indexes)
54
- }
55
- }
56
- }
57
-
58
- /**
59
- * Collect all properties it can be found on model
60
- *
61
- * @param {Object} model - Model
62
- * @param {Array} [inputs] - Array of properties
63
- * @param {Array} [indexes] - Container array to fill up found index
64
- */
65
- async function findAllProps (model, inputs = [], indexes = []) {
66
- const { isPlainObject, cloneDeep } = this.app.lib._
67
- const isIdProp = inputs.find(p => {
68
- return isPlainObject(p) ? p.name === 'id' : p.startsWith('id,')
69
- })
70
- if (!isIdProp) {
71
- const idField = cloneDeep(model.connection.driver.idField)
72
- idField.name = 'id'
73
- inputs.unshift(idField)
74
- }
75
- for (const prop of inputs) {
76
- await sanitizeProp.call(this, model, prop, indexes)
77
- }
78
- }
79
-
80
- /**
81
- * Apply each feature found
82
- * @param {Object} model - Model
83
- * @param {Object} feature - Feature to apply
84
- * @param {Object} options - Options to the feature
85
- * @returns {Array} New properties found in feature
86
- */
87
- async function applyFeature (model, feature, options, indexes) {
88
- const { isArray, findIndex } = this.app.lib._
89
- if (feature.name === 'dobo:unique' && options.field === 'id') {
90
- const idx = findIndex(model.properties, { name: 'id' })
91
- if (idx > -1) model.properties.pullAt(idx)
92
- }
93
- const item = await feature.handler(options)
94
- if (item.rules) model.rules.push(...item.rules)
95
- if (item.scanables) model.scanables.push(...item.scanables)
96
- if (!isArray(item.properties)) item.properties = [item.properties]
97
- for (const prop of item.properties) {
98
- prop.feature = `${feature.plugin.ns}:${feature.name}`
99
- await sanitizeProp.call(this, model, prop, indexes)
100
- }
101
- if (item.hooks) {
102
- item.hooks = item.hooks.map(hook => {
103
- hook.level = hook.level ?? 999
104
- return hook
105
- })
106
- model.hooks.push(...item.hooks)
107
- }
108
- }
109
-
110
- /**
111
- * Collect all features it can be found on model
112
- *
113
- * @param {Object} model - Model
114
- * @param {Array} [inputs] - Array of properties
115
- */
116
- async function findAllFeats (model, inputs = [], indexes = []) {
117
- const { isString, omit } = this.app.lib._
118
- for (let feat of inputs) {
119
- if (isString(feat)) feat = { name: feat }
120
- const featName = feat.name.indexOf(':') === -1 ? `dobo:${feat.name}` : feat.name
121
- const feature = this.app.dobo.getFeature(featName)
122
- if (!feature) this.fatal('invalidFeature%s%s', model.name, featName)
123
- await applyFeature.call(this, model, feature, omit(feat, 'name'), indexes)
124
- }
125
- }
126
-
127
- /**
128
- * Collect all indexes it can be found on model
129
- *
130
- * @param {Object} model - Model
131
- * @param {Array} [inputs] - Array of properties
132
- */
133
- async function findAllIndexes (model, inputs = [], indexes = []) {
134
- const items = []
135
- for (const index of inputs) {
136
- index.type = index.type ?? 'index'
137
- index.fields = index.fields ?? []
138
- if (!index.name) index.name = `${model.name}_${index.fields.join('_')}_${index.type}`
139
- items.push(index)
140
- }
141
- model.indexes = [...items, ...indexes]
142
- }
143
-
144
- /**
145
- * Sanitize any reference/relationship found in properties
146
- *
147
- * @param {Object} model - Model
148
- * @param {Array} [models] - All model match agaist. Defaults to ```dobo.models```
149
- */
150
- export async function sanitizeRef (model, models) {
151
- const { find, isString, pullAt } = this.app.lib._
152
- const _refKeys = []
153
- for (const prop of model.properties) {
154
- const ignored = []
155
- for (const key in prop.ref ?? {}) {
156
- _refKeys.push(key)
157
- let ref = prop.ref[key]
158
- if (isString(ref)) {
159
- ref = { field: ref }
160
- }
161
- ref.field = ref.field ?? 'id'
162
- ref.type = ref.type ?? '1:1'
163
- ref.searchField = ref.searchField ?? ref.field
164
- ref.labelField = ref.labelField ?? ref.searchField
165
- const rModel = find(models, { name: ref.model })
166
- if (!rModel) {
167
- ignored.push(key)
168
- this.log.warn('notFound%s%s', this.t('model'), ref.model)
169
- continue
170
- }
171
- const rProp = find(rModel.properties, { name: ref.field })
172
- if (!rProp) {
173
- ignored.push(key)
174
- this.log.warn('notFound%s%s', this.t('property'), `${ref.field}@${ref.model}`)
175
- continue
176
- }
177
- ref.fields = ref.fields ?? '*'
178
- if (['*', 'all'].includes(ref.fields)) ref.fields = rModel.properties.map(p => p.name)
179
- if (ref.fields.length > 0 && !ref.fields.includes('id')) ref.fields.unshift('id')
180
- const removed = []
181
- for (const idx in ref.fields) {
182
- const p = find(rModel.properties, { name: ref.fields[idx] })
183
- if (!p) removed.push(ref.fields[idx])
184
- }
185
- pullAt(ref.fields, removed)
186
- prop.ref[key] = ref
187
- }
188
- for (const key of ignored) {
189
- delete prop.ref[key]
190
- }
191
- }
192
- const dupes = _refKeys.filter((item, index) => _refKeys.indexOf(item) !== index)
193
- if (dupes.length > 0) throw this.error('duplicateRefKeys%s%s', model.name, dupes.join(', '))
194
- }
195
-
196
- /**
197
- * Sanitize all but reference, because a reference needs all models to be available first
198
- *
199
- * @param {Object} model - Model
200
- */
201
- export async function sanitizeAll (model) {
202
- const { runHook } = this.app.bajo
203
- const { pick, keys, map, uniq, camelCase, filter } = this.app.lib._
204
- const { defaultsDeep } = this.app.lib.aneka
205
- const allPropNames = uniq(map(model.properties, 'name'))
206
- const propType = this.constructor.propertyType
207
- const indexTypes = this.constructor.indexTypes
208
-
209
- await runHook(`dobo.${camelCase(model.name)}:beforeSanitizeModel`, model)
210
- // properties
211
- for (const idx in model.properties) {
212
- let prop = model.properties[idx]
213
- const def = propType[prop.type]
214
- prop = pick(defaultsDeep(prop, def), this.getPropertyKeysByType(prop.type))
215
- if (!keys(propType).includes(prop.type)) this.fatal('unknownPropType%s%s', `${prop.name}.${def.name}`, prop.type)
216
- if (prop.type === 'string') {
217
- prop.minLength = parseInt(prop.minLength) ?? 0
218
- prop.maxLength = parseInt(prop.maxLength) ?? 255
219
- if (prop.minLength > 0) prop.required = true
220
- if (prop.minLength === 0) delete prop.minLength
221
- }
222
- model.properties[idx] = prop
223
- }
224
- // indexes
225
- for (const index of model.indexes) {
226
- if (!indexTypes.includes(index.type)) this.fatal('unknownIndexType%s%s', index.type, model.name)
227
- for (const field of index.fields) {
228
- if (!allPropNames.includes(field)) this.fatal('unknownPropNameOnIndex%s%s', field, model.name)
229
- }
230
- }
231
- await runHook(`dobo.${camelCase(model.name)}:afterSanitizeModel`, model)
232
- // sorting only possible using these fields
233
- model.sortables = []
234
- for (const index of model.indexes) {
235
- model.sortables.push(...index.fields)
236
- }
237
- model.hidden = filter(uniq(model.hidden), prop => allPropNames.includes(prop))
238
- }
239
-
240
- /**
241
- * Create schema for model
242
- *
243
- * @param {Object} item - Source item
244
- * @returns {Object} Sanitized model
245
- */
246
- async function createSchema (item) {
247
- const { find, orderBy, get } = this.app.lib._
248
- const { mergeObjectsByKey, defaultsDeep, parseObject } = this.app.lib.aneka
249
- const feats = item.features ?? []
250
- const props = item.properties ?? []
251
- const indexes = item.indexes ?? []
252
- item.features = []
253
- item.properties = []
254
- item.indexes = []
255
- item.hidden = item.hidden ?? []
256
- item.rules = item.rules ?? []
257
- item.buildLevel = item.buildLevel ?? 999
258
- item.hooks = item.hooks ?? []
259
- item.disabled = item.disabled ?? []
260
- item.scanables = item.scanables ?? []
261
- if (item.disabled === 'all') item.disabled = ['find', 'get', 'create', 'update', 'remove']
262
- else if (item.disabled === 'readonly') item.disabled = ['create', 'update', 'remove']
263
- const conn = item.connection ?? 'default'
264
- if (!(conn instanceof this.app.baseClass.DoboConnection)) {
265
- item.connection = null
266
- // Is there any overwritten connection?
267
- const newConn = find(this.connections, c => c.options.models.includes(item.name))
268
- if (newConn) item.connection = newConn
269
- else {
270
- item.connection = this.getConnection(conn, true)
271
- if (!item.connection && conn === 'default') item.connection = this.getConnection('memory')
272
- }
273
- if (!item.connection) this.fatal('unknownConn%s%s', conn, item.name)
274
- }
275
- // cache settings
276
- const defCache = defaultsDeep({}, item.connection.options.cache, get(this, 'app.bajoCache.config.default', this.config.default.cache))
277
- if (item.cache === false) item.cache = { ttlDur: 0 }
278
- else if (item.cache === true) item.cache = defCache
279
- else item.cache = defaultsDeep({}, item.cache, defCache)
280
- item.cache = parseObject(item.cache)
281
- if (item.connection.name === 'memory') item.cache.ttlDur = 0
282
-
283
- // let's run
284
- await findAllProps.call(this, item, props, indexes)
285
- await findAllFeats.call(this, item, feats, indexes)
286
- await findAllIndexes.call(this, item, indexes)
287
- for (const key of ['properties', 'indexes']) {
288
- item[key] = mergeObjectsByKey(item[key], 'name')
289
- }
290
- item.hooks = orderBy(item.hooks, ['name', 'level'])
291
- delete item.features
292
- await sanitizeAll.call(this, item)
293
- return item
294
- }
295
-
296
- /**
297
- * Collect all models from loaded plugins and create the models
298
- *
299
- * @name collectModels
300
- * @memberof module:lib
301
- * @async
302
- * @see Dobo#init
303
- */
304
- async function collectModels () {
305
- const { eachPlugins, callHandler } = this.app.bajo
306
- const { orderBy, has, omit } = this.app.lib._
307
- await actionFactory.call(this)
308
- await modelFactory.call(this)
309
-
310
- this.log.trace('collecting%s', this.t('model'))
311
- const me = this
312
- let schemas = []
313
- await eachPlugins(async function ({ file }) {
314
- const { readConfig, callHandler } = this.app.bajo
315
- const { pascalCase } = this.app.lib.aneka
316
- const { isPlainObject, isEmpty, isArray } = this.app.lib._
317
-
318
- let items = await readConfig(file, { ns: this.ns, baseNs: me.ns, merge: true })
319
- if (isEmpty(items)) return undefined
320
- if (isPlainObject(items)) {
321
- items.baseName = items.baseName ?? path.basename(file, path.extname(file))
322
- items.name = items.name ?? pascalCase(`${this.alias} ${items.baseName}`)
323
- }
324
- if (!isArray(items)) items = [items]
325
- for (const item of items) {
326
- if (!item.baseName) me.fatal('missing%s%s', 'baseName', file)
327
- item.name = item.name ?? pascalCase(`${this.alias} ${item.baseName}`)
328
- me.log.trace('- %s', item.name)
329
- item.collName = item.collName ?? item.name
330
- item.options = item.options ?? {}
331
- item.options.attachment = item.options.attachment ?? true
332
- item.options.persistence = item.options.persistence ?? true
333
- item.ns = this.ns
334
- if (item.buildStart) await callHandler(this, item.buildStart, item)
335
- const schema = await createSchema.call(me, item)
336
- schemas.push(schema)
337
- }
338
- }, { glob: ['model/*.*', 'model.*'], prefix: this.ns })
339
- schemas = orderBy(schemas, ['buildLevel', 'name'])
340
- for (const schema of schemas) {
341
- const idProp = schema.properties.find(p => p.name === 'id')
342
- if (!this.constructor.idTypes.includes(idProp.type)) this.fatal('invalidIdType%s%s', schema.name, this.constructor.idTypes.join(', '))
343
- if (idProp.type === 'string' && !has(idProp, 'maxLength')) idProp.maxLength = 50
344
- const plugin = this.app[schema.ns]
345
- await sanitizeRef.call(this, schema, schemas)
346
- for (const item of schema.indexes) {
347
- for (const field of item.fields) {
348
- const prop = schema.properties.find(p => p.name === field)
349
- if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'index', schema.name)
350
- }
351
- }
352
- for (const field of schema.scanables) {
353
- const prop = schema.properties.find(p => p.name === field)
354
- if (!prop || (prop && prop.virtual)) throw this.error('virtualFieldIn%s%s%s', field, 'scanable', schema.name)
355
- }
356
- const model = new this.app.baseClass.DoboModel(plugin, omit(schema, ['beforeCreate', 'afterCreate']))
357
- me.models.push(model)
358
- if (schema.buildEnd) await callHandler(plugin, schema.buildEnd, model)
359
- }
360
- schemas = []
361
- this.log.debug('collected%s%d', this.t('model'), this.models.length)
362
- }
363
-
364
- export default collectModels
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: true })
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
+ }