@radio-garden/ditojs-server 2.85.2-0.5067ad799

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 (137) hide show
  1. package/README.md +6 -0
  2. package/package.json +95 -0
  3. package/src/app/Application.js +1186 -0
  4. package/src/app/Validator.js +405 -0
  5. package/src/app/index.js +2 -0
  6. package/src/cli/console.js +152 -0
  7. package/src/cli/db/createMigration.js +241 -0
  8. package/src/cli/db/index.js +7 -0
  9. package/src/cli/db/listAssetConfig.js +10 -0
  10. package/src/cli/db/migrate.js +12 -0
  11. package/src/cli/db/reset.js +23 -0
  12. package/src/cli/db/rollback.js +12 -0
  13. package/src/cli/db/seed.js +80 -0
  14. package/src/cli/db/unlock.js +9 -0
  15. package/src/cli/index.js +72 -0
  16. package/src/controllers/AdminController.js +322 -0
  17. package/src/controllers/CollectionController.js +274 -0
  18. package/src/controllers/Controller.js +657 -0
  19. package/src/controllers/ControllerAction.js +370 -0
  20. package/src/controllers/MemberAction.js +27 -0
  21. package/src/controllers/ModelController.js +63 -0
  22. package/src/controllers/RelationController.js +93 -0
  23. package/src/controllers/UsersController.js +64 -0
  24. package/src/controllers/index.js +5 -0
  25. package/src/errors/AssetError.js +7 -0
  26. package/src/errors/AuthenticationError.js +7 -0
  27. package/src/errors/AuthorizationError.js +7 -0
  28. package/src/errors/ControllerError.js +14 -0
  29. package/src/errors/DatabaseError.js +37 -0
  30. package/src/errors/GraphError.js +7 -0
  31. package/src/errors/ModelError.js +12 -0
  32. package/src/errors/NotFoundError.js +7 -0
  33. package/src/errors/NotImplementedError.js +7 -0
  34. package/src/errors/QueryBuilderError.js +7 -0
  35. package/src/errors/RelationError.js +21 -0
  36. package/src/errors/ResponseError.js +56 -0
  37. package/src/errors/ValidationError.js +7 -0
  38. package/src/errors/index.js +13 -0
  39. package/src/graph/DitoGraphProcessor.js +213 -0
  40. package/src/graph/expression.js +53 -0
  41. package/src/graph/graph.js +258 -0
  42. package/src/graph/index.js +3 -0
  43. package/src/index.js +9 -0
  44. package/src/lib/EventEmitter.js +66 -0
  45. package/src/lib/KnexHelper.js +30 -0
  46. package/src/lib/index.js +2 -0
  47. package/src/middleware/attachLogger.js +8 -0
  48. package/src/middleware/createTransaction.js +33 -0
  49. package/src/middleware/extendContext.js +10 -0
  50. package/src/middleware/findRoute.js +20 -0
  51. package/src/middleware/handleConnectMiddleware.js +99 -0
  52. package/src/middleware/handleError.js +29 -0
  53. package/src/middleware/handleRoute.js +23 -0
  54. package/src/middleware/handleSession.js +77 -0
  55. package/src/middleware/handleUser.js +31 -0
  56. package/src/middleware/index.js +11 -0
  57. package/src/middleware/logRequests.js +125 -0
  58. package/src/middleware/setupRequestStorage.js +14 -0
  59. package/src/mixins/AssetMixin.js +78 -0
  60. package/src/mixins/SessionMixin.js +17 -0
  61. package/src/mixins/TimeStampedMixin.js +41 -0
  62. package/src/mixins/UserMixin.js +171 -0
  63. package/src/mixins/index.js +4 -0
  64. package/src/models/AssetModel.js +4 -0
  65. package/src/models/Model.js +1205 -0
  66. package/src/models/RelationAccessor.js +41 -0
  67. package/src/models/SessionModel.js +4 -0
  68. package/src/models/TimeStampedModel.js +4 -0
  69. package/src/models/UserModel.js +4 -0
  70. package/src/models/definitions/assets.js +5 -0
  71. package/src/models/definitions/filters.js +121 -0
  72. package/src/models/definitions/hooks.js +8 -0
  73. package/src/models/definitions/index.js +22 -0
  74. package/src/models/definitions/modifiers.js +5 -0
  75. package/src/models/definitions/options.js +5 -0
  76. package/src/models/definitions/properties.js +73 -0
  77. package/src/models/definitions/relations.js +5 -0
  78. package/src/models/definitions/schema.js +5 -0
  79. package/src/models/definitions/scopes.js +36 -0
  80. package/src/models/index.js +5 -0
  81. package/src/query/QueryBuilder.js +1077 -0
  82. package/src/query/QueryFilters.js +66 -0
  83. package/src/query/QueryParameters.js +79 -0
  84. package/src/query/Registry.js +29 -0
  85. package/src/query/index.js +3 -0
  86. package/src/schema/formats/_empty.js +4 -0
  87. package/src/schema/formats/_required.js +4 -0
  88. package/src/schema/formats/index.js +2 -0
  89. package/src/schema/index.js +5 -0
  90. package/src/schema/keywords/_computed.js +7 -0
  91. package/src/schema/keywords/_foreign.js +7 -0
  92. package/src/schema/keywords/_hidden.js +7 -0
  93. package/src/schema/keywords/_index.js +7 -0
  94. package/src/schema/keywords/_instanceof.js +45 -0
  95. package/src/schema/keywords/_primary.js +7 -0
  96. package/src/schema/keywords/_range.js +18 -0
  97. package/src/schema/keywords/_relate.js +13 -0
  98. package/src/schema/keywords/_specificType.js +7 -0
  99. package/src/schema/keywords/_unique.js +7 -0
  100. package/src/schema/keywords/_unsigned.js +7 -0
  101. package/src/schema/keywords/_validate.js +73 -0
  102. package/src/schema/keywords/index.js +12 -0
  103. package/src/schema/relations.js +324 -0
  104. package/src/schema/relations.test.js +177 -0
  105. package/src/schema/schema.js +289 -0
  106. package/src/schema/schema.test.js +720 -0
  107. package/src/schema/types/_asset.js +31 -0
  108. package/src/schema/types/_color.js +4 -0
  109. package/src/schema/types/index.js +2 -0
  110. package/src/services/Service.js +35 -0
  111. package/src/services/index.js +1 -0
  112. package/src/storage/AssetFile.js +81 -0
  113. package/src/storage/DiskStorage.js +114 -0
  114. package/src/storage/S3Storage.js +169 -0
  115. package/src/storage/Storage.js +231 -0
  116. package/src/storage/index.js +9 -0
  117. package/src/utils/duration.js +15 -0
  118. package/src/utils/emitter.js +8 -0
  119. package/src/utils/fs.js +10 -0
  120. package/src/utils/function.js +17 -0
  121. package/src/utils/function.test.js +77 -0
  122. package/src/utils/handler.js +17 -0
  123. package/src/utils/json.js +3 -0
  124. package/src/utils/model.js +35 -0
  125. package/src/utils/net.js +17 -0
  126. package/src/utils/object.js +82 -0
  127. package/src/utils/object.test.js +86 -0
  128. package/src/utils/scope.js +7 -0
  129. package/types/index.d.ts +3547 -0
  130. package/types/tests/application.test-d.ts +26 -0
  131. package/types/tests/controller.test-d.ts +113 -0
  132. package/types/tests/errors.test-d.ts +53 -0
  133. package/types/tests/fixtures.ts +19 -0
  134. package/types/tests/model.test-d.ts +193 -0
  135. package/types/tests/query-builder.test-d.ts +106 -0
  136. package/types/tests/relation.test-d.ts +83 -0
  137. package/types/tests/storage.test-d.ts +113 -0
@@ -0,0 +1,1205 @@
1
+ import objection from 'objection'
2
+ import {
3
+ isString,
4
+ isObject,
5
+ isArray,
6
+ isFunction,
7
+ isPromise,
8
+ asArray,
9
+ clone,
10
+ equals,
11
+ flatten,
12
+ parseDataPath,
13
+ normalizeDataPath,
14
+ getValueAtDataPath,
15
+ mapConcurrently,
16
+ assignDeeply,
17
+ deprecate
18
+ } from '@ditojs/utils'
19
+ import { QueryBuilder } from '../query/index.js'
20
+ import { EventEmitter, KnexHelper } from '../lib/index.js'
21
+ import {
22
+ convertSchema,
23
+ addRelationSchemas,
24
+ convertRelations
25
+ } from '../schema/index.js'
26
+ import { populateGraph, filterGraph } from '../graph/index.js'
27
+ import { formatJson } from '../utils/json.js'
28
+ import {
29
+ ResponseError,
30
+ GraphError,
31
+ ModelError,
32
+ NotFoundError,
33
+ RelationError
34
+ } from '../errors/index.js'
35
+ import RelationAccessor from './RelationAccessor.js'
36
+ import definitions from './definitions/index.js'
37
+
38
+ export class Model extends objection.Model {
39
+ static app = null // Set by `Application.addModel()`
40
+ static initialized = false
41
+ static referenceValidator = null
42
+
43
+ // Define a default constructor to allow new Model(json) as a short-cut to
44
+ // `Model.fromJson(json, { skipValidation: true })`
45
+ constructor(json) {
46
+ super()
47
+ if (json) {
48
+ this.$setJson(json)
49
+ }
50
+ }
51
+
52
+ static configure(app) {
53
+ this.app = app
54
+ this.knex(app.knex)
55
+ const { hooks, assets } = this.definition
56
+ this._configureEmitter(hooks)
57
+ if (assets) {
58
+ this._configureAssetsHooks(assets)
59
+ }
60
+ try {
61
+ for (const relation of Object.values(this.getRelations())) {
62
+ this._configureRelation(relation)
63
+ }
64
+ } catch (error) {
65
+ throw error instanceof RelationError ? error : new RelationError(error)
66
+ }
67
+ }
68
+
69
+ static _configureRelation(relation) {
70
+ // Add this relation to the related model's relatedRelations, so it can
71
+ // register all required foreign keys in its properties.
72
+ relation.relatedModelClass.getRelatedRelations().push(relation)
73
+ // TODO: Check `through` settings to make sure they're correct?
74
+
75
+ // Expose RelationAccessor instances for each relation under short-cut $name
76
+ // for access to relations and implicit calls to $relatedQuery(name).
77
+ const accessor = `$${relation.name}`
78
+ if (accessor in this.prototype) {
79
+ throw new RelationError(
80
+ `Model '${this.name}' already defines a property with name ` +
81
+ `'${accessor}' that clashes with the relation accessor.`
82
+ )
83
+ }
84
+
85
+ // Define an accessor on the class as well as on the prototype that when
86
+ // first called creates a RelationAccessor instance and then overrides the
87
+ // accessor with one that then just returns the same value afterwards.
88
+ const defineAccessor = (target, isClass) => {
89
+ Object.defineProperty(target, accessor, {
90
+ get() {
91
+ const value = new RelationAccessor(
92
+ relation,
93
+ isClass ? this : null, // modelClass
94
+ isClass ? null : this // model
95
+ )
96
+ // Override accessor with value on first call for caching.
97
+ Object.defineProperty(this, accessor, {
98
+ value,
99
+ configurable: true,
100
+ enumerable: false
101
+ })
102
+ return value
103
+ },
104
+ configurable: true,
105
+ enumerable: false
106
+ })
107
+ }
108
+
109
+ defineAccessor(this, true)
110
+ defineAccessor(this.prototype, false)
111
+ }
112
+
113
+ // @overridable
114
+ static setup() {}
115
+
116
+ // @overridable
117
+ static initialize() {}
118
+
119
+ // @overridable
120
+ $initialize() {}
121
+
122
+ get $app() {
123
+ return this.constructor.app
124
+ }
125
+
126
+ $is(model) {
127
+ return model?.constructor === this.constructor && model?.id === this.id
128
+ }
129
+
130
+ $has(...properties) {
131
+ for (const property of properties) {
132
+ if (!(property in this)) return false
133
+ }
134
+ return true
135
+ }
136
+
137
+ $update(properties, trx) {
138
+ return this.$query(trx)
139
+ .update(properties)
140
+ .runAfter((result, query) =>
141
+ // Only perform `$set()` and return `this` if the query wasn't modified
142
+ // in a way that would remove the `update()` command, e.g. toFindQuery()
143
+ query.has('update') ? this.$set(result) : result
144
+ )
145
+ }
146
+
147
+ $patch(properties, trx) {
148
+ return this.$query(trx)
149
+ .patch(properties)
150
+ .runAfter((result, query) =>
151
+ // Only perform `$set()` and return `this` if the query wasn't modified
152
+ // in a way that would remove the `patch()` command, e.g. toFindQuery()
153
+ query.has('patch') ? this.$set(result) : result
154
+ )
155
+ }
156
+
157
+ // @override
158
+ $transaction(trx, handler) {
159
+ return this.constructor.transaction(trx, handler)
160
+ }
161
+
162
+ // @override
163
+ static transaction(trx, handler) {
164
+ // Support both `transaction(trx, handler)` & `transaction(handler)`
165
+ if (!handler) {
166
+ handler = trx
167
+ trx = null
168
+ }
169
+ if (handler) {
170
+ // Use existing transaction, or create new one, to execute handler with:
171
+ return trx
172
+ ? handler(trx)
173
+ : this.knex().transaction(handler)
174
+ } else {
175
+ // No arguments, simply delegate to objection's transaction()
176
+ return super.transaction()
177
+ }
178
+ }
179
+
180
+ // @override
181
+ $validate(json, options = {}) {
182
+ if (options.skipValidation) {
183
+ return json
184
+ }
185
+ json ||= this
186
+ const inputJson = json
187
+
188
+ const shallow = !options.graph
189
+ if (shallow) {
190
+ json = clone(json, { shallow: true })
191
+ // Strip away relations.
192
+ for (const key of this.constructor.getRelationNames()) {
193
+ delete json[key]
194
+ }
195
+ // We can mutate `json` now that we took a copy of it.
196
+ options = {
197
+ ...options,
198
+ mutable: true
199
+ }
200
+ }
201
+
202
+ const validator = this.constructor.getValidator()
203
+ const args = {
204
+ options,
205
+ model: this,
206
+ json,
207
+ ctx: Object.create(null)
208
+ }
209
+
210
+ validator.beforeValidate(args)
211
+ const result = validator.validate(args)
212
+ const handleResult = result => {
213
+ validator.afterValidate(args)
214
+ // If `json` was shallow-cloned, copy over the possible default values.
215
+ return shallow ? Object.assign(inputJson, result) : result
216
+ }
217
+ // Handle both async and sync validation here:
218
+ return isPromise(result)
219
+ ? result.then(handleResult)
220
+ : handleResult(result)
221
+ }
222
+
223
+ async $validateGraph(options = {}) {
224
+ await this.$validate(null, {
225
+ ...options,
226
+ graph: true,
227
+ // Always use `async: true` option here for simplicity:
228
+ async: true
229
+ })
230
+ return this
231
+ }
232
+
233
+ // @override
234
+ static fromJson(json, options = {}) {
235
+ if (options.async && !options.skipValidation) {
236
+ // Handle async validation, as supported by Dito:
237
+ const model = new this()
238
+ return model.$validate(json, options).then(json =>
239
+ model.$setJson(json, {
240
+ ...options,
241
+ skipValidation: true
242
+ })
243
+ )
244
+ }
245
+ // Fall back to Objection's fromJson() if we don't need async handling:
246
+ return super.fromJson(json, options)
247
+ }
248
+
249
+ // @override
250
+ static query(trx) {
251
+ return super.query(trx).onError(err => {
252
+ // TODO: Shouldn't this wrapping happen on the Controller level?
253
+ err =
254
+ err instanceof ResponseError
255
+ ? err
256
+ : err instanceof objection.DBError
257
+ ? this.app.createDatabaseError(err)
258
+ : new ResponseError(err)
259
+ return Promise.reject(err)
260
+ })
261
+ }
262
+
263
+ static async count(...args) {
264
+ const { count } = (await this.query()
265
+ .count(...args)
266
+ .first()) || {}
267
+ return +count || 0
268
+ }
269
+
270
+ // @override
271
+ static get tableName() {
272
+ // If the class name ends in 'Model', remove that from the table name.
273
+ return this.name.match(/^(.*?)(?:Model|)$/)[1]
274
+ }
275
+
276
+ // @override
277
+ static get idColumn() {
278
+ // Try extracting the id column name from the raw properties definitions,
279
+ // not the resolved `definition.properties` which aren't ready at this point
280
+ // with fall-back onto default Objection.js behavior.
281
+ const { properties } = this
282
+ const ids = []
283
+ for (const [name, property] of Object.entries(properties || {})) {
284
+ if (property?.primary) {
285
+ ids.push(name)
286
+ }
287
+ }
288
+ const { length } = ids
289
+ return length > 1 ? ids : length > 0 ? ids[0] : super.idColumn
290
+ }
291
+
292
+ static getReference(modelOrId, includeProperties) {
293
+ // Creates a reference model that takes over the id / #ref properties from
294
+ // the passed model or id value/array, omitting any other properties in it,
295
+ // except for anything mentioned in the optional `includeProperties` arg.
296
+ const ref = new this()
297
+ const idProperties = this.getIdPropertyArray()
298
+ if (isObject(modelOrId)) {
299
+ const addProperty = key => {
300
+ const value = modelOrId[key]
301
+ if (value !== undefined) {
302
+ ref[key] = value
303
+ }
304
+ }
305
+ // Also support Objection's #ref type references next to id properties.
306
+ addProperty(this.uidRefProp)
307
+ idProperties.forEach(addProperty)
308
+ includeProperties?.forEach(addProperty)
309
+ } else {
310
+ // An id value/array: Map it to the properties in `getIdPropertyArray()`:
311
+ const ids = asArray(modelOrId)
312
+ if (ids.length !== idProperties.length) {
313
+ throw new ModelError(
314
+ this,
315
+ `Invalid amount of id values provided for reference: Unable to map ${
316
+ formatJson(modelOrId, false)
317
+ } to ${
318
+ formatJson(idProperties, false)
319
+ }.`
320
+ )
321
+ }
322
+ idProperties.forEach((key, index) => {
323
+ ref[key] = ids[index]
324
+ })
325
+ }
326
+ return ref
327
+ }
328
+
329
+ static isReference(obj) {
330
+ let validator = this.referenceValidator
331
+ if (!validator) {
332
+ // For `data` to be considered a reference, it needs to hold only one
333
+ // value that is either the target's id, or an Objection.js #ref value:
334
+ validator = this.referenceValidator = this.app.compileValidator(
335
+ {
336
+ oneOf: [
337
+ {
338
+ type: 'object',
339
+ // Support composite keys and add a property for each key:
340
+ properties: this.getIdPropertyArray().reduce(
341
+ (idProperties, idProperty) => {
342
+ idProperties[idProperty] = {
343
+ type: this.getProperty(idProperty).type
344
+ }
345
+ return idProperties
346
+ },
347
+ {}
348
+ ),
349
+ unevaluatedProperties: false
350
+ },
351
+ {
352
+ type: 'object',
353
+ properties: {
354
+ [this.uidRefProp]: {
355
+ type: 'string'
356
+ }
357
+ },
358
+ unevaluatedProperties: false
359
+ }
360
+ ]
361
+ },
362
+ // Receive `false` instead of thrown exceptions when validation fails:
363
+ { throw: false }
364
+ )
365
+ }
366
+ return validator(obj)
367
+ }
368
+
369
+ static getScope(name) {
370
+ return this.definition.scopes[name]
371
+ }
372
+
373
+ static hasScope(name) {
374
+ return !!this.getScope(name)
375
+ }
376
+
377
+ static getModifiers() {
378
+ return this.definition.modifiers
379
+ }
380
+
381
+ static get relationMappings() {
382
+ return this._getCached(
383
+ 'relationMappings',
384
+ () => convertRelations(this, this.definition.relations, this.app.models),
385
+ {}
386
+ )
387
+ }
388
+
389
+ static get jsonSchema() {
390
+ return this._getCached(
391
+ 'jsonSchema',
392
+ () => {
393
+ const schema = convertSchema({
394
+ type: 'object',
395
+ properties: this.definition.properties
396
+ })
397
+ addRelationSchemas(this, schema.properties)
398
+ // Merge in root-level schema additions
399
+ assignDeeply(schema, this.definition.schema)
400
+ return {
401
+ $id: this.name,
402
+ ...schema
403
+ }
404
+ },
405
+ {}
406
+ )
407
+ }
408
+
409
+ static get virtualAttributes() {
410
+ // Leverage Objection's own mechanism called `virtualAttributes` to handle
411
+ // `computedAttributes` when setting JSON data.
412
+ return this.computedAttributes
413
+ }
414
+
415
+ static get jsonAttributes() {
416
+ return this._getCached(
417
+ 'jsonSchema:jsonAttributes',
418
+ () =>
419
+ this.getAttributes(
420
+ ({ type, specificType, computed }) => (
421
+ !computed &&
422
+ !specificType &&
423
+ (type === 'object' || type === 'array')
424
+ )
425
+ ),
426
+ []
427
+ )
428
+ }
429
+
430
+ static get booleanAttributes() {
431
+ return this._getCached(
432
+ 'jsonSchema:booleanAttributes',
433
+ () =>
434
+ this.getAttributes(
435
+ ({ type, computed }) => !computed && type === 'boolean'
436
+ ),
437
+ []
438
+ )
439
+ }
440
+
441
+ static get dateAttributes() {
442
+ return this._getCached(
443
+ 'jsonSchema:dateAttributes',
444
+ () =>
445
+ this.getAttributes(
446
+ ({ type, computed }) => (
447
+ !computed && ['date', 'datetime', 'timestamp'].includes(type)
448
+ )
449
+ ),
450
+ []
451
+ )
452
+ }
453
+
454
+ static get computedAttributes() {
455
+ return this._getCached(
456
+ 'jsonSchema:computedAttributes',
457
+ () => this.getAttributes(({ computed }) => computed),
458
+ []
459
+ )
460
+ }
461
+
462
+ static get hiddenAttributes() {
463
+ return this._getCached(
464
+ 'jsonSchema:hiddenAttributes',
465
+ () => this.getAttributes(({ hidden }) => hidden),
466
+ []
467
+ )
468
+ }
469
+
470
+ static getProperty(name) {
471
+ let property = this.definition.properties[name]
472
+ // Expand $refs so we can even find properties that uses definitions:
473
+ if (property?.$ref) {
474
+ const { $ref, ...schema } = property
475
+ const definition = this.jsonSchema.definitions?.[$ref]
476
+ if (definition) {
477
+ property = {
478
+ ...schema,
479
+ ...definition
480
+ }
481
+ }
482
+ }
483
+ return property ?? null
484
+ }
485
+
486
+ static getAttributes(filter) {
487
+ return Object.keys(this.definition.properties).filter(name =>
488
+ filter(this.getProperty(name))
489
+ )
490
+ }
491
+
492
+ static _getCached(identifier, calculate, empty = {}) {
493
+ let cache = getMeta(this, 'cache', {})
494
+ // Use a simple dependency tracking mechanism with cache identifiers that
495
+ // can be children of other cached values, e.g.:
496
+ // 'jsonSchema:jsonAttributes' as a child of 'jsonSchema', so that whenever
497
+ // 'jsonSchema' changes, all cached child values are invalidated.
498
+ let entry
499
+ for (const part of identifier.split(':')) {
500
+ entry = cache[part] = (
501
+ cache[part] ||
502
+ {
503
+ cache: {},
504
+ value: undefined
505
+ }
506
+ )
507
+ cache = entry.cache
508
+ }
509
+ if (entry?.value === undefined) {
510
+ // Temporarily set cache to an empty object to prevent endless
511
+ // recursion with interdependent jsonSchema related calls...
512
+ entry.value = empty
513
+ entry.value = calculate()
514
+ // Clear child dependencies once parent value has changed:
515
+ entry.cache = {}
516
+ }
517
+ return entry?.value
518
+ }
519
+
520
+ static getRelatedRelations() {
521
+ return getMeta(this, 'relatedRelations', [])
522
+ }
523
+
524
+ // Override propertyNameToColumnName() / columnNameToPropertyName() to not
525
+ // rely on $formatDatabaseJson() / $parseDatabaseJson() do detect naming
526
+ // conventions but assume simply that they're always the same.
527
+ // This is fine since we can now change naming at Knex level.
528
+ // See knexSnakeCaseMappers()
529
+
530
+ // @override
531
+ static propertyNameToColumnName(propertyName) {
532
+ return propertyName
533
+ }
534
+
535
+ // @override
536
+ static columnNameToPropertyName(columnName) {
537
+ return columnName
538
+ }
539
+
540
+ // @override
541
+ $setJson(json, options) {
542
+ options ||= {}
543
+ const callInitialize = (
544
+ // Only call initialize when:
545
+ // 1. we're not partially patching:
546
+ !options.patch &&
547
+ // 2. $initialize() is actually doing something:
548
+ this.$initialize !== Model.prototype.$initialize &&
549
+ // 3. the data is not just a reference:
550
+ !this.constructor.isReference(json)
551
+ )
552
+ if (!callInitialize || options.skipValidation) {
553
+ super.$setJson(json, options)
554
+ if (callInitialize) {
555
+ this.$initialize()
556
+ }
557
+ } else {
558
+ // If validation isn't skipped or the model provides its own $initialize()
559
+ // method, call $setJson() with patch validation first to not complain
560
+ // about missing fields, then perform a full validation after calling
561
+ // $initialize(), to give the model a chance to configure itself.
562
+ super.$setJson(json, { ...options, patch: true })
563
+ this.$initialize()
564
+ this.$validate(this, options)
565
+ }
566
+ return this
567
+ }
568
+
569
+ // @override
570
+ $formatDatabaseJson(json) {
571
+ const { constructor } = this
572
+ for (const key of constructor.dateAttributes) {
573
+ const date = json[key]
574
+ if (date?.toISOString) {
575
+ json[key] = date.toISOString()
576
+ }
577
+ }
578
+ if (constructor.isSQLite()) {
579
+ // SQLite does not support boolean natively and needs conversion...
580
+ for (const key of constructor.booleanAttributes) {
581
+ const bool = json[key]
582
+ if (bool !== undefined) {
583
+ json[key] = bool ? 1 : 0
584
+ }
585
+ }
586
+ }
587
+ // Remove the computed properties so they don't attempt to get set.
588
+ for (const key of constructor.computedAttributes) {
589
+ delete json[key]
590
+ }
591
+ // NOTE: No need to normalize the identifiers in the JSON in case of
592
+ // normalizeDbNames, as this already happens through
593
+ // knex.config.wrapIdentifier(), see Application.js
594
+ return super.$formatDatabaseJson(json)
595
+ }
596
+
597
+ // @override
598
+ $parseDatabaseJson(json) {
599
+ const { constructor } = this
600
+ json = super.$parseDatabaseJson(json)
601
+ if (constructor.isSQLite()) {
602
+ // SQLite does not support boolean natively and needs conversion...
603
+ for (const key of constructor.booleanAttributes) {
604
+ const bool = json[key]
605
+ if (bool !== undefined) {
606
+ json[key] = !!bool
607
+ }
608
+ }
609
+ }
610
+ // Also run through normal $parseJson(), for handling of `Date` and
611
+ // `AssetFile`.
612
+ return this.$parseJson(json)
613
+ }
614
+
615
+ // @override
616
+ $parseJson(json) {
617
+ const { constructor } = this
618
+ for (const key of constructor.dateAttributes) {
619
+ const date = json[key]
620
+ if (date !== undefined) {
621
+ json[key] = isString(date) ? new Date(date) : date
622
+ }
623
+ }
624
+ // Convert plain asset files objects to AssetFile instances with references
625
+ // to the linked storage.
626
+ const { assets } = constructor.definition
627
+ if (assets) {
628
+ for (const dataPath in assets) {
629
+ const storage = constructor.app.getStorage(assets[dataPath].storage)
630
+ const data = getValueAtDataPath(json, dataPath, () => null)
631
+ if (data) {
632
+ const convertToAssetFiles = data => {
633
+ if (data) {
634
+ if (isArray(data)) {
635
+ data.forEach(convertToAssetFiles)
636
+ } else {
637
+ storage.convertAssetFile(data)
638
+ }
639
+ }
640
+ }
641
+ convertToAssetFiles(data)
642
+ }
643
+ }
644
+ }
645
+ return json
646
+ }
647
+
648
+ // @override
649
+ $formatJson(json) {
650
+ // Remove hidden attributes.
651
+ for (const key of this.constructor.hiddenAttributes) {
652
+ delete json[key]
653
+ }
654
+ return json
655
+ }
656
+
657
+ // Graph handling
658
+
659
+ $filterGraph(modelGraph, expr) {
660
+ return filterGraph(this.constructor, modelGraph, expr)
661
+ }
662
+
663
+ async $populateGraph(modelGraph, expr, trx) {
664
+ return populateGraph(this.constructor, modelGraph, expr, trx)
665
+ }
666
+
667
+ static filterGraph(modelGraph, expr) {
668
+ return filterGraph(this, modelGraph, expr)
669
+ }
670
+
671
+ static async populateGraph(modelGraph, expr, trx) {
672
+ return populateGraph(this, modelGraph, expr, trx)
673
+ }
674
+
675
+ static getPropertyOrRelationAtDataPath(dataPath) {
676
+ // Finds the property or relation at the given the dataPath of the model by
677
+ // parsing the dataPath and matching it to its relations and properties.
678
+ const parsedDataPath = parseDataPath(dataPath)
679
+ let index = 0
680
+
681
+ const getResult = ({
682
+ property = null,
683
+ relation = null,
684
+ wildcard = null
685
+ } = {}) => {
686
+ const found = !!(property || relation || wildcard)
687
+ const name = wildcard ? '*' : parsedDataPath[index]
688
+ const next = index + 1
689
+ const dataPath = found
690
+ ? normalizeDataPath(parsedDataPath.slice(0, next))
691
+ : null
692
+ const nestedDataPath = found
693
+ ? normalizeDataPath(parsedDataPath.slice(next))
694
+ : null
695
+ const expression = found
696
+ ? parsedDataPath.slice(0, relation ? next : index).join('.') +
697
+ (property ? `(#${name})` : '')
698
+ : null
699
+ return {
700
+ property,
701
+ relation,
702
+ wildcard,
703
+ dataPath,
704
+ nestedDataPath,
705
+ name,
706
+ expression
707
+ }
708
+ }
709
+
710
+ const [firstToken, ...otherTokens] = parsedDataPath
711
+ const property = this.getProperty(firstToken)
712
+ if (property) {
713
+ return getResult({ property })
714
+ } else if (firstToken === '*' || firstToken === '**') {
715
+ return getResult({ wildcard: firstToken })
716
+ } else {
717
+ let relation = this.getRelations()[firstToken]
718
+ if (relation) {
719
+ let { relatedModelClass } = relation
720
+ for (const token of otherTokens) {
721
+ index++
722
+ const property = relatedModelClass.definition.properties[token]
723
+ if (property) {
724
+ return getResult({ property, relation })
725
+ } else if (token === '*') {
726
+ if (relation.isOneToOne()) {
727
+ // Do not support wildcards on one-to-one relations:
728
+ return getResult() // Not found.
729
+ } else {
730
+ continue
731
+ }
732
+ } else if (token === '**') {
733
+ throw new ModelError(
734
+ this,
735
+ 'Deep wildcards on relations are unsupported.'
736
+ )
737
+ } else {
738
+ // Found a relation? Keep iterating.
739
+ relation = relatedModelClass.getRelations()[token]
740
+ if (relation) {
741
+ relatedModelClass = relation.relatedModelClass
742
+ } else {
743
+ return getResult() // Not found.
744
+ }
745
+ }
746
+ }
747
+ if (relation) {
748
+ // Still here? Found a relation at the end of the data-path.
749
+ return getResult({ relation })
750
+ }
751
+ }
752
+ }
753
+ return getResult()
754
+ }
755
+
756
+ // @override
757
+ static relatedQuery(relationName, trx) {
758
+ // https://github.com/Vincit/objection.js/issues/1720
759
+ return super.relatedQuery(relationName, trx).alias(relationName)
760
+ }
761
+
762
+ // @override
763
+ static modifierNotFound(query, modifier) {
764
+ if (isString(modifier)) {
765
+ if (query.modelClass().hasScope(modifier)) {
766
+ return query.applyScope(
767
+ modifier,
768
+ // Pass `false` for `checkAllowedScopes`, to always allow scopes
769
+ // that are defined on relations to be applied.
770
+ false
771
+ )
772
+ }
773
+ // Now check possible scope prefixes and handle them:
774
+
775
+ let prefix = modifier[0]
776
+ const deprecatedPrefixes = { '-': '!', '#': '@' }
777
+ if (deprecatedPrefixes[prefix]) {
778
+ prefix = deprecatedPrefixes[prefix]
779
+ deprecate(
780
+ `The '${
781
+ modifier
782
+ }' modifier is deprecated, use the '${
783
+ prefix
784
+ }${
785
+ modifier.slice(1)
786
+ }' modifier instead.`
787
+ )
788
+ }
789
+
790
+ switch (prefix) {
791
+ case '^': // Eager-applied scope:
792
+ // Always apply eager-scopes, even if the model itself doesn't know
793
+ // it. The scope may still be known in eager-loaded relations.
794
+ // Note: `applyScope()` will handle the '^' sign.
795
+ return query.applyScope(
796
+ modifier,
797
+ // Pass `false` for `checkAllowedScopes`, to always allow scopes
798
+ // that are defined on relations to be applied.
799
+ false
800
+ )
801
+ case '!': // Ignore scope:
802
+ return query.ignoreScope(modifier.slice(1))
803
+ case '@': // Select column:
804
+ return query.select(modifier.slice(1))
805
+ case '~': // omit column:
806
+ return query.omit(modifier.slice(1))
807
+ case '*': // Select all columns:
808
+ if (modifier === '*') {
809
+ return query.select('*')
810
+ }
811
+ break
812
+ }
813
+ }
814
+ super.modifierNotFound(query, modifier)
815
+ }
816
+
817
+ // @override
818
+ static createNotFoundError(ctx, error) {
819
+ return new NotFoundError(
820
+ error || (
821
+ ctx.byId
822
+ ? `'${this.name}' model with id ${ctx.byId} not found`
823
+ : `'${this.name}' model not found`
824
+ )
825
+ )
826
+ }
827
+
828
+ // @override
829
+ static createValidator() {
830
+ // Use a shared validator per app, so model schema can reference each other.
831
+ // NOTE: The Dito.js Validator class creates and manages this shared
832
+ // Objection Validator instance for us, we just need to return it here:
833
+ return this.app.validator
834
+ }
835
+
836
+ // @override
837
+ static createValidationError({ type, message, errors, options, json }) {
838
+ switch (type) {
839
+ case 'ModelValidation':
840
+ return this.app.createValidationError({
841
+ type,
842
+ message: (
843
+ message ||
844
+ `The provided data for the ${this.name} model is not valid`
845
+ ),
846
+ errors,
847
+ options,
848
+ json
849
+ })
850
+ case 'RelationExpression':
851
+ case 'UnallowedRelation':
852
+ return new RelationError({ type, message, errors })
853
+ case 'InvalidGraph':
854
+ return new GraphError({ type, message, errors })
855
+ default:
856
+ return new ResponseError({ type, message, errors })
857
+ }
858
+ }
859
+
860
+ // @override
861
+ static QueryBuilder = QueryBuilder
862
+
863
+ // https://vincit.github.io/objection.js/api/model/static-properties.html#static-cloneobjectattributes
864
+ static cloneObjectAttributes = false
865
+
866
+ // Only pick properties for database JSON that is mentioned in the schema.
867
+ static pickJsonSchemaProperties = true
868
+
869
+ // See https://gitter.im/Vincit/objection.js?at=5a81f859ce68c3bc7479d65a
870
+ static useLimitInFirst = true
871
+
872
+ static get definition() {
873
+ // Check if we already have a definition object for this class and return it
874
+ return getMeta(this, 'definition', () => {
875
+ const definition = {}
876
+
877
+ const setDefinition = (name, property) => {
878
+ Object.defineProperty(definition, name, {
879
+ ...property,
880
+ enumerable: true
881
+ })
882
+ }
883
+
884
+ const getDefinition = name => {
885
+ let modelClass = this
886
+ // Collect ancestor values for proper inheritance.
887
+ // NOTE: values are collected in sequence of inheritance, from sub-class
888
+ // to super-class. To go from super-class to sub-class when merging,
889
+ // `mergeReversed()` is used to prevent wrong overrides.
890
+ // `mergeAsReversedArrays()` can be used to keep arrays of inherited
891
+ // values per key, see `definitions.hooks`.
892
+ const values = []
893
+ while (modelClass !== objection.Model) {
894
+ // Only consider model classes that actually define `name` property.
895
+ if (name in modelClass) {
896
+ // Use reflection through getOwnPropertyDescriptor() to be able to
897
+ // call the getter on `this` rather than on `modelClass`. This can
898
+ // be used to provide abstract base-classes and have them create
899
+ // their relations for `this` inside `get relations()` accessors.
900
+ const desc = Object.getOwnPropertyDescriptor(modelClass, name)
901
+ if (desc) {
902
+ const value = desc.get?.call(this) || desc.value
903
+ if (value) {
904
+ values.push(value)
905
+ }
906
+ }
907
+ }
908
+ modelClass = Object.getPrototypeOf(modelClass)
909
+ }
910
+ // To prevent endless recursion with interdependent calls related to
911
+ // properties, override definition before calling handler():
912
+ setDefinition(name, {
913
+ configurable: true,
914
+ value: {}
915
+ })
916
+ try {
917
+ const merged = definitions[name].call(this, values)
918
+ // Once calculated, override getter with final merged value.
919
+ setDefinition(name, {
920
+ configurable: false,
921
+ value: merged
922
+ })
923
+ return merged
924
+ } catch (error) {
925
+ throw new ModelError(this, error.message)
926
+ }
927
+ }
928
+
929
+ // If no definition object was defined yet, create one with accessors for
930
+ // each entry in `definitions`. Each of these getters when called merge
931
+ // definitions up the inheritance chain and store the merged result in
932
+ // `modelClass.definition[name]` for further caching.
933
+ for (const name in definitions) {
934
+ setDefinition(name, {
935
+ configurable: true,
936
+ get: () => getDefinition(name)
937
+ })
938
+ }
939
+ return definition
940
+ })
941
+ }
942
+
943
+ // Hooks
944
+
945
+ $emit(event, ...args) {
946
+ return this.constructor.emit(event, this, ...args)
947
+ }
948
+
949
+ static beforeFind(args) {
950
+ return this._emitStaticHook('before:find', args)
951
+ }
952
+
953
+ static afterFind(args) {
954
+ return this._emitStaticHook('after:find', args)
955
+ }
956
+
957
+ static beforeInsert(args) {
958
+ return this._emitStaticHook('before:insert', args)
959
+ }
960
+
961
+ static afterInsert(args) {
962
+ return this._emitStaticHook('after:insert', args)
963
+ }
964
+
965
+ static beforeUpdate(args) {
966
+ return this._emitStaticHook('before:update', args)
967
+ }
968
+
969
+ static afterUpdate(args) {
970
+ return this._emitStaticHook('after:update', args)
971
+ }
972
+
973
+ static beforeDelete(args) {
974
+ return this._emitStaticHook('before:delete', args)
975
+ }
976
+
977
+ static afterDelete(args) {
978
+ return this._emitStaticHook('after:delete', args)
979
+ }
980
+
981
+ static async _emitStaticHook(event, originalArgs) {
982
+ const listeners = this.listeners(event)
983
+ if (listeners.length > 0) {
984
+ // Static hooks are emitted in sequence (but each event can be async), and
985
+ // results are passed through and returned in the end.
986
+ let { result } = originalArgs
987
+ // The result of any event handler will override `args.result` in the call
988
+ // of the next handler in sequence. As `StaticHookArguments` in Objection
989
+ // is private, use a JS inheritance trick here to override `args.result`:
990
+ const args = Object.create(originalArgs, {
991
+ type: {
992
+ value: event
993
+ },
994
+ result: {
995
+ get() {
996
+ return result
997
+ }
998
+ }
999
+ })
1000
+ for (const listener of listeners) {
1001
+ const res = await listener.call(this, args)
1002
+ if (res !== undefined) {
1003
+ result = res
1004
+ }
1005
+ }
1006
+ // Unfortunately `result` is always an array, even when the actual result
1007
+ // is a model object. Avoid returning it when it's not actually changed.
1008
+ // See: https://github.com/Vincit/objection.js/issues/1842
1009
+ if (result !== originalArgs.result) {
1010
+ return result
1011
+ }
1012
+ }
1013
+ }
1014
+
1015
+ // Assets handling
1016
+
1017
+ static _configureAssetsHooks(assets) {
1018
+ const assetDataPaths = Object.keys(assets)
1019
+
1020
+ this.on(
1021
+ [
1022
+ 'before:insert',
1023
+ 'before:update',
1024
+ 'before:delete'
1025
+ ],
1026
+ async ({ type, transaction, inputItems, asFindQuery }) => {
1027
+ const isInsert = type === 'before:insert'
1028
+ const isDelete = type === 'before:delete'
1029
+ // Figure out which asset data paths where actually present in the
1030
+ // submitted data, and only compare these. But when deleting, use all.
1031
+ const dataPaths = isDelete
1032
+ ? assetDataPaths
1033
+ : assetDataPaths.filter(
1034
+ dataPath => (
1035
+ // Skip check for wildcard data-paths.
1036
+ /^\*\*?/.test(dataPath) ||
1037
+ // Only keep normal data paths that match present properties.
1038
+ (parseDataPath(dataPath)[0] in inputItems[0])
1039
+ )
1040
+ )
1041
+ // `dataPaths` is empty in the case of an update/insert that does not
1042
+ // affect the assets.
1043
+ if (dataPaths.length === 0) return
1044
+
1045
+ const afterItems = isDelete
1046
+ ? []
1047
+ : inputItems
1048
+ // Load the model's asset files in their current state before the query
1049
+ // is executed. For deletes, load the data for all asset data-paths.
1050
+ // Otherwise, only load the columns present in the input data.
1051
+ const beforeItems = isInsert
1052
+ ? []
1053
+ : isDelete
1054
+ ? // When deleting, it's ok to load all columns when data-paths
1055
+ // contain wildcards unfiltered, since `afterItems` will be empty
1056
+ // anyway.
1057
+ await loadAssetDataPaths(asFindQuery(), dataPaths)
1058
+ : await asFindQuery().select(
1059
+ // Select only the properties that are present in the data,
1060
+ // and which aren't the result of computed properties.
1061
+ Object.keys(inputItems[0]).filter(key => {
1062
+ const property = this.getProperty(key)
1063
+ return property && !property.computed
1064
+ })
1065
+ )
1066
+
1067
+ const afterFilesPerDataPath = getFilesPerAssetDataPath(
1068
+ afterItems,
1069
+ dataPaths
1070
+ )
1071
+ const beforeFilesPerDataPath = getFilesPerAssetDataPath(
1072
+ beforeItems,
1073
+ dataPaths
1074
+ )
1075
+
1076
+ const importedFiles = []
1077
+ const modifiedFiles = []
1078
+
1079
+ if (transaction.rollback) {
1080
+ // Prevent wrong memory leak error messages when installing more than
1081
+ // 10 'rollback' handlers, which can happen with more complex queries.
1082
+ transaction.setMaxListeners(0)
1083
+ transaction.on('rollback', async error => {
1084
+ if (importedFiles.length > 0) {
1085
+ console.info(
1086
+ `Received '${error}', removing imported files again: ${
1087
+ importedFiles.map(file => `'${file.name}'`)
1088
+ }`
1089
+ )
1090
+ await mapConcurrently(
1091
+ importedFiles,
1092
+ file => file.storage.removeFile(file)
1093
+ )
1094
+ }
1095
+ if (modifiedFiles.length > 0) {
1096
+ // TODO: `modifiedFiles` should be restored as well, but that's
1097
+ // far from trivial since no backup is kept in
1098
+ // `handleModifiedAssets()`
1099
+ console.warn(
1100
+ `Unable to restore these already modified files: ${
1101
+ modifiedFiles.map(file => `'${file.name}'`)
1102
+ }`
1103
+ )
1104
+ }
1105
+ })
1106
+ }
1107
+
1108
+ for (const dataPath of dataPaths) {
1109
+ const storage = this.app.getStorage(assets[dataPath].storage)
1110
+ const beforeFiles = beforeFilesPerDataPath[dataPath] || []
1111
+ const afterFiles = afterFilesPerDataPath[dataPath] || []
1112
+ const beforeByKey = mapFilesByKey(beforeFiles)
1113
+ const afterByKey = mapFilesByKey(afterFiles)
1114
+ const addedFiles = afterFiles.filter(file => !beforeByKey[file.key])
1115
+ const removedFiles = beforeFiles.filter(file => !afterByKey[file.key])
1116
+ const changedFiles = afterFiles.filter(file => {
1117
+ const beforeFile = beforeByKey[file.key]
1118
+ return beforeFile && !equals(file, beforeFile)
1119
+ })
1120
+ // Also handle modified files, which are files where the data property
1121
+ // is changed before update / patch, meaning the file is changed.
1122
+ // NOTE: This will change the content for all the references to it,
1123
+ // and so should only really be used when there's only one reference.
1124
+ const modifiedFiles = afterFiles.filter(
1125
+ file => file.data && beforeByKey[file.key]
1126
+ )
1127
+ importedFiles.push(
1128
+ ...(await this.app.handleAddedAndRemovedAssets(
1129
+ storage,
1130
+ addedFiles,
1131
+ removedFiles,
1132
+ changedFiles,
1133
+ transaction
1134
+ ))
1135
+ )
1136
+ modifiedFiles.push(
1137
+ ...(await this.app.handleModifiedAssets(
1138
+ storage,
1139
+ modifiedFiles,
1140
+ transaction
1141
+ ))
1142
+ )
1143
+ }
1144
+ }
1145
+ )
1146
+ }
1147
+ }
1148
+
1149
+ EventEmitter.mixin(Model)
1150
+ KnexHelper.mixin(Model)
1151
+ // Expose a selection of QueryBuilder methods as static methods on model classes
1152
+ QueryBuilder.mixin(Model)
1153
+
1154
+ const metaMap = new WeakMap()
1155
+
1156
+ function getMeta(modelClass, key, value) {
1157
+ let meta = metaMap.get(modelClass)
1158
+ if (!meta) {
1159
+ metaMap.set(modelClass, (meta = {}))
1160
+ }
1161
+ if (!(key in meta)) {
1162
+ meta[key] = isFunction(value) ? value() : value
1163
+ }
1164
+ return meta[key]
1165
+ }
1166
+
1167
+ function loadAssetDataPaths(query, dataPaths) {
1168
+ return dataPaths.reduce(
1169
+ (query, dataPath) => query.loadDataPath(dataPath),
1170
+ query
1171
+ )
1172
+ }
1173
+
1174
+ function getValueAtAssetDataPath(item, path) {
1175
+ return getValueAtDataPath(item, path, () => undefined)
1176
+ }
1177
+
1178
+ function getFilesPerAssetDataPath(items, dataPaths) {
1179
+ return dataPaths.reduce(
1180
+ (allFiles, dataPath) => {
1181
+ allFiles[dataPath] = asArray(items).reduce(
1182
+ (files, item) => {
1183
+ const data = asArray(getValueAtAssetDataPath(item, dataPath))
1184
+ // Use flatten() as dataPath may contain wildcards, resulting in
1185
+ // nested files arrays.
1186
+ files.push(...flatten(data).filter(file => !!file))
1187
+ return files
1188
+ },
1189
+ []
1190
+ )
1191
+ return allFiles
1192
+ },
1193
+ {}
1194
+ )
1195
+ }
1196
+
1197
+ function mapFilesByKey(files) {
1198
+ return files.reduce(
1199
+ (map, file) => {
1200
+ map[file.key] = file
1201
+ return map
1202
+ },
1203
+ {}
1204
+ )
1205
+ }