@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.
- package/README.md +6 -0
- package/package.json +95 -0
- package/src/app/Application.js +1186 -0
- package/src/app/Validator.js +405 -0
- package/src/app/index.js +2 -0
- package/src/cli/console.js +152 -0
- package/src/cli/db/createMigration.js +241 -0
- package/src/cli/db/index.js +7 -0
- package/src/cli/db/listAssetConfig.js +10 -0
- package/src/cli/db/migrate.js +12 -0
- package/src/cli/db/reset.js +23 -0
- package/src/cli/db/rollback.js +12 -0
- package/src/cli/db/seed.js +80 -0
- package/src/cli/db/unlock.js +9 -0
- package/src/cli/index.js +72 -0
- package/src/controllers/AdminController.js +322 -0
- package/src/controllers/CollectionController.js +274 -0
- package/src/controllers/Controller.js +657 -0
- package/src/controllers/ControllerAction.js +370 -0
- package/src/controllers/MemberAction.js +27 -0
- package/src/controllers/ModelController.js +63 -0
- package/src/controllers/RelationController.js +93 -0
- package/src/controllers/UsersController.js +64 -0
- package/src/controllers/index.js +5 -0
- package/src/errors/AssetError.js +7 -0
- package/src/errors/AuthenticationError.js +7 -0
- package/src/errors/AuthorizationError.js +7 -0
- package/src/errors/ControllerError.js +14 -0
- package/src/errors/DatabaseError.js +37 -0
- package/src/errors/GraphError.js +7 -0
- package/src/errors/ModelError.js +12 -0
- package/src/errors/NotFoundError.js +7 -0
- package/src/errors/NotImplementedError.js +7 -0
- package/src/errors/QueryBuilderError.js +7 -0
- package/src/errors/RelationError.js +21 -0
- package/src/errors/ResponseError.js +56 -0
- package/src/errors/ValidationError.js +7 -0
- package/src/errors/index.js +13 -0
- package/src/graph/DitoGraphProcessor.js +213 -0
- package/src/graph/expression.js +53 -0
- package/src/graph/graph.js +258 -0
- package/src/graph/index.js +3 -0
- package/src/index.js +9 -0
- package/src/lib/EventEmitter.js +66 -0
- package/src/lib/KnexHelper.js +30 -0
- package/src/lib/index.js +2 -0
- package/src/middleware/attachLogger.js +8 -0
- package/src/middleware/createTransaction.js +33 -0
- package/src/middleware/extendContext.js +10 -0
- package/src/middleware/findRoute.js +20 -0
- package/src/middleware/handleConnectMiddleware.js +99 -0
- package/src/middleware/handleError.js +29 -0
- package/src/middleware/handleRoute.js +23 -0
- package/src/middleware/handleSession.js +77 -0
- package/src/middleware/handleUser.js +31 -0
- package/src/middleware/index.js +11 -0
- package/src/middleware/logRequests.js +125 -0
- package/src/middleware/setupRequestStorage.js +14 -0
- package/src/mixins/AssetMixin.js +78 -0
- package/src/mixins/SessionMixin.js +17 -0
- package/src/mixins/TimeStampedMixin.js +41 -0
- package/src/mixins/UserMixin.js +171 -0
- package/src/mixins/index.js +4 -0
- package/src/models/AssetModel.js +4 -0
- package/src/models/Model.js +1205 -0
- package/src/models/RelationAccessor.js +41 -0
- package/src/models/SessionModel.js +4 -0
- package/src/models/TimeStampedModel.js +4 -0
- package/src/models/UserModel.js +4 -0
- package/src/models/definitions/assets.js +5 -0
- package/src/models/definitions/filters.js +121 -0
- package/src/models/definitions/hooks.js +8 -0
- package/src/models/definitions/index.js +22 -0
- package/src/models/definitions/modifiers.js +5 -0
- package/src/models/definitions/options.js +5 -0
- package/src/models/definitions/properties.js +73 -0
- package/src/models/definitions/relations.js +5 -0
- package/src/models/definitions/schema.js +5 -0
- package/src/models/definitions/scopes.js +36 -0
- package/src/models/index.js +5 -0
- package/src/query/QueryBuilder.js +1077 -0
- package/src/query/QueryFilters.js +66 -0
- package/src/query/QueryParameters.js +79 -0
- package/src/query/Registry.js +29 -0
- package/src/query/index.js +3 -0
- package/src/schema/formats/_empty.js +4 -0
- package/src/schema/formats/_required.js +4 -0
- package/src/schema/formats/index.js +2 -0
- package/src/schema/index.js +5 -0
- package/src/schema/keywords/_computed.js +7 -0
- package/src/schema/keywords/_foreign.js +7 -0
- package/src/schema/keywords/_hidden.js +7 -0
- package/src/schema/keywords/_index.js +7 -0
- package/src/schema/keywords/_instanceof.js +45 -0
- package/src/schema/keywords/_primary.js +7 -0
- package/src/schema/keywords/_range.js +18 -0
- package/src/schema/keywords/_relate.js +13 -0
- package/src/schema/keywords/_specificType.js +7 -0
- package/src/schema/keywords/_unique.js +7 -0
- package/src/schema/keywords/_unsigned.js +7 -0
- package/src/schema/keywords/_validate.js +73 -0
- package/src/schema/keywords/index.js +12 -0
- package/src/schema/relations.js +324 -0
- package/src/schema/relations.test.js +177 -0
- package/src/schema/schema.js +289 -0
- package/src/schema/schema.test.js +720 -0
- package/src/schema/types/_asset.js +31 -0
- package/src/schema/types/_color.js +4 -0
- package/src/schema/types/index.js +2 -0
- package/src/services/Service.js +35 -0
- package/src/services/index.js +1 -0
- package/src/storage/AssetFile.js +81 -0
- package/src/storage/DiskStorage.js +114 -0
- package/src/storage/S3Storage.js +169 -0
- package/src/storage/Storage.js +231 -0
- package/src/storage/index.js +9 -0
- package/src/utils/duration.js +15 -0
- package/src/utils/emitter.js +8 -0
- package/src/utils/fs.js +10 -0
- package/src/utils/function.js +17 -0
- package/src/utils/function.test.js +77 -0
- package/src/utils/handler.js +17 -0
- package/src/utils/json.js +3 -0
- package/src/utils/model.js +35 -0
- package/src/utils/net.js +17 -0
- package/src/utils/object.js +82 -0
- package/src/utils/object.test.js +86 -0
- package/src/utils/scope.js +7 -0
- package/types/index.d.ts +3547 -0
- package/types/tests/application.test-d.ts +26 -0
- package/types/tests/controller.test-d.ts +113 -0
- package/types/tests/errors.test-d.ts +53 -0
- package/types/tests/fixtures.ts +19 -0
- package/types/tests/model.test-d.ts +193 -0
- package/types/tests/query-builder.test-d.ts +106 -0
- package/types/tests/relation.test-d.ts +83 -0
- 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
|
+
}
|