@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,1077 @@
1
+ import objection from 'objection'
2
+ import {
3
+ isArray,
4
+ isString,
5
+ isBoolean,
6
+ isObject,
7
+ isPlainObject,
8
+ asArray,
9
+ clone,
10
+ mapKeys,
11
+ getValueAtDataPath,
12
+ setValueAtDataPath,
13
+ normalizeDataPath,
14
+ parseDataPath
15
+ } from '@ditojs/utils'
16
+ import { QueryParameters } from './QueryParameters.js'
17
+ import { KnexHelper } from '../lib/index.js'
18
+ import { DitoGraphProcessor, walkGraph } from '../graph/index.js'
19
+ import { QueryBuilderError, RelationError } from '../errors/index.js'
20
+ import { createLookup } from '../utils/object.js'
21
+ import { getScope } from '../utils/scope.js'
22
+
23
+ const SYMBOL_ALL = Symbol('all')
24
+
25
+ export class QueryBuilder extends objection.QueryBuilder {
26
+ #ignoreGraph = false
27
+ #graphAlgorithm = 'fetch'
28
+ #isJoinChildQuery = false
29
+ #scopes = { default: true } // Eager-apply the default scope
30
+ #allowScopes = null
31
+ #ignoreScopes = {}
32
+ #appliedScopes = {}
33
+ #allowFilters = null
34
+ #executeFirst = null // Part of a work-around for cyclic graphs
35
+ #omits = []
36
+
37
+ // @override
38
+ clone() {
39
+ const copy = super.clone()
40
+ copy.#ignoreGraph = this.#ignoreGraph
41
+ copy.#graphAlgorithm = this.#graphAlgorithm
42
+ copy.#appliedScopes = { ...this.#appliedScopes }
43
+ copy.#allowFilters = this.#allowFilters ? { ...this.#allowFilters } : null
44
+ copy.#copyScopes(this)
45
+ return copy
46
+ }
47
+
48
+ // @override
49
+ async execute() {
50
+ if (this.#omits.length > 0) {
51
+ this.runAfter(result => {
52
+ for (const model of asArray(result)) {
53
+ model.$omitFromJson(...this.#omits)
54
+ }
55
+ return result
56
+ })
57
+ }
58
+ this.#applyScopes()
59
+ // In case of cyclic graphs, run `_executeFirst()` now:
60
+ await this.#executeFirst?.()
61
+ return super.execute()
62
+ }
63
+
64
+ hasNormalSelects() {
65
+ // Returns true if the query defines normal selects:
66
+ // select(), column(), columns()
67
+ return this.has(/^(select|columns?)$/)
68
+ }
69
+
70
+ hasSpecialSelects() {
71
+ // Returns true if the query defines special selects:
72
+ // distinct(), count(), countDistinct(), min(), max(),
73
+ // sum(), sumDistinct(), avg(), avgDistinct()
74
+ return this.hasSelects() && !this.hasNormalSelects()
75
+ }
76
+
77
+ // @override
78
+ childQueryOf(query, options) {
79
+ super.childQueryOf(query, options)
80
+ this.#isJoinChildQuery = query.#graphAlgorithm === 'join'
81
+ if (this.isInternal()) {
82
+ // Internal queries shouldn't apply or inherit any scopes, not even the
83
+ // default scope.
84
+ this.#clearScopes(false)
85
+ } else {
86
+ // Inherit the graph scopes from the parent query.
87
+ this.#ignoreGraph = query.#ignoreGraph
88
+ this.#graphAlgorithm = query.#graphAlgorithm
89
+ this.#copyScopes(query, true)
90
+ }
91
+ return this
92
+ }
93
+
94
+ // @override
95
+ toFindQuery() {
96
+ // Temporary workaround to fix this issue until it is resolved in Objection:
97
+ // https://github.com/Vincit/objection.js/issues/2093
98
+ return super.toFindQuery().clear('runAfter')
99
+ }
100
+
101
+ omit(...properties) {
102
+ this.#omits.push(...properties)
103
+ }
104
+
105
+ #withScope(...args) {
106
+ const { checkAllowedScopes, scopes } = getScopes(args)
107
+ for (const expr of scopes) {
108
+ if (expr) {
109
+ const { scope, graph } = getScope(expr)
110
+ if (
111
+ checkAllowedScopes &&
112
+ this.#allowScopes &&
113
+ !this.#allowScopes[scope]
114
+ ) {
115
+ throw new QueryBuilderError(
116
+ `Query scope '${scope}' is not allowed.`
117
+ )
118
+ }
119
+ this.#scopes[scope] ||= graph
120
+ }
121
+ }
122
+ return scopes
123
+ }
124
+
125
+ withScope(...args) {
126
+ this.#withScope(...args)
127
+ return this
128
+ }
129
+
130
+ // Clear all scopes defined with `withScope()` statements, preserving the
131
+ // default scope.
132
+ clearWithScope() {
133
+ return this.#clearScopes(true)
134
+ }
135
+
136
+ ignoreScope(...scopes) {
137
+ if (!this.#ignoreScopes[SYMBOL_ALL]) {
138
+ this.#ignoreScopes =
139
+ scopes.length > 0
140
+ ? {
141
+ ...this.#ignoreScopes,
142
+ ...createLookup(scopes)
143
+ }
144
+ : // Empty arguments = ignore all scopes
145
+ {
146
+ [SYMBOL_ALL]: true
147
+ }
148
+ }
149
+ return this
150
+ }
151
+
152
+ applyScope(...args) {
153
+ // When directly applying a scope, still use `#withScope() to merge it into
154
+ // `this.#scopes`, so it can still be passed on to forked child queries.
155
+ // This also handles the checks against `#allowScopes`.
156
+ for (const expr of this.#withScope(...args)) {
157
+ if (expr) {
158
+ const { scope, graph } = getScope(expr)
159
+ this.#applyScope(scope, graph)
160
+ }
161
+ }
162
+ return this
163
+ }
164
+
165
+ allowScope(...scopes) {
166
+ this.#allowScopes ||= {
167
+ default: true // The default scope is always allowed.
168
+ }
169
+ for (const expr of scopes) {
170
+ if (expr) {
171
+ const { scope } = getScope(expr)
172
+ this.#allowScopes[scope] = true
173
+ }
174
+ }
175
+ }
176
+
177
+ clearAllowScope() {
178
+ this.#allowScopes = null
179
+ }
180
+
181
+ #clearScopes(addDefault) {
182
+ // `_scopes` is an object where the keys are the scopes and the values
183
+ // indicate if the scope should be eager-applied or not:
184
+ this.#scopes = addDefault
185
+ ? { default: true } // Eager-apply the default scope
186
+ : {}
187
+ return this
188
+ }
189
+
190
+ #copyScopes(query, isChildQuery = false) {
191
+ const isSameModelClass = this.modelClass() === query.modelClass()
192
+ // Only copy `#allowScopes` and `_ignoreScopes` if it's for the same model.
193
+ if (isSameModelClass) {
194
+ this.#allowScopes = query.#allowScopes ? { ...query.#allowScopes } : null
195
+ this.#ignoreScopes = { ...query.#ignoreScopes }
196
+ }
197
+ const scopes = isChildQuery
198
+ ? // When copying scopes for child-queries, we also need to take the
199
+ // already applied scopes into account and copy those too.
200
+ { ...query.#appliedScopes, ...query.#scopes }
201
+ : { ...query.#scopes }
202
+ // If the target is a child query of a graph query, copy all scopes, graph
203
+ // and non-graph. If it is a child query of a related or eager query,
204
+ // copy only the graph scopes.
205
+ const copyAllScopes = (
206
+ isSameModelClass &&
207
+ isChildQuery &&
208
+ query.has(/GraphAndFetch$/)
209
+ )
210
+ this.#scopes = copyAllScopes
211
+ ? scopes
212
+ : filterScopes(scopes, (scope, graph) => graph) // copy graph-scopes only.
213
+ }
214
+
215
+ #applyScopes() {
216
+ if (!this.#ignoreScopes[SYMBOL_ALL]) {
217
+ // Only apply default scopes if this is a normal find query, meaning it
218
+ // does not define any write operations or special selects, e.g. `count`:
219
+ const isNormalFind = (
220
+ this.isFind() &&
221
+ !this.hasSpecialSelects()
222
+ )
223
+ // If this isn't a normal find query, ignore all graph operations,
224
+ // to not mess with special selects such as `count`, etc:
225
+ this.#ignoreGraph = !isNormalFind
226
+ // All scopes in `_scopes` were already checked against `#allowScopes`.
227
+ // They themselves are allowed to apply / request other scopes that
228
+ // aren't listed, so clear `#allowScopes` and restore again after:
229
+ const allowScopes = this.#allowScopes
230
+ this.#allowScopes = null
231
+ const collectedScopes = {}
232
+ // Scopes can themselves request more scopes by calling `withScope()`
233
+ // In order to prevent that from causing problems while looping over
234
+ // `_scopes`, create a local copy of the entries, set `_scopes` to an
235
+ // empty object during iteration and check if there are new entries after
236
+ // one full loop. Keep doing this until there's nothing left, but keep
237
+ // track of all the applied scopes, so `_scopes` can be set to the the
238
+ // that in the end. This is needed for child queries, see `childQueryOf()`
239
+ let scopes = Object.entries(this.#scopes)
240
+ while (scopes.length > 0) {
241
+ this.#scopes = {}
242
+ for (const [scope, graph] of scopes) {
243
+ // Don't apply `default` scopes on anything else than a normal find
244
+ // query:
245
+ if (isNormalFind || scope !== 'default') {
246
+ this.#applyScope(scope, graph)
247
+ collectedScopes[scope] ||= graph
248
+ }
249
+ }
250
+ scopes = Object.entries(this.#scopes)
251
+ }
252
+ this.#scopes = collectedScopes
253
+ this.#allowScopes = allowScopes
254
+ this.#ignoreGraph = false
255
+ }
256
+ }
257
+
258
+ #applyScope(scope, graph) {
259
+ if (!this.#ignoreScopes[SYMBOL_ALL] && !this.#ignoreScopes[scope]) {
260
+ // Prevent multiple application of scopes. This can easily occur
261
+ // with the nesting and eager-application of graph-scopes, see below.
262
+ // NOTE: The boolean values indicate the `graph` settings, not whether the
263
+ // scopes were applied or not.
264
+ if (!(scope in this.#appliedScopes)) {
265
+ // Only apply graph-scopes that are actually defined on the model:
266
+ const func = this.modelClass().getScope(scope)
267
+ if (func) {
268
+ func.call(this, this)
269
+ }
270
+ this.#appliedScopes[scope] = graph
271
+ }
272
+ if (graph) {
273
+ // Also bake the scope into any graph expression that may have been
274
+ // set already using `withGraph()` & co.
275
+ const expr = this.graphExpressionObject()
276
+ if (expr) {
277
+ // Add a new modifier to the existing graph expression that
278
+ // recursively adds the graph-scope to the resulting queries. This
279
+ // even works if nested scopes expand the graph expression, because it
280
+ // re-applies itself to the result.
281
+ const name = `^${scope}`
282
+ const modifiers = {
283
+ [name]: query => {
284
+ query.withScope(
285
+ name,
286
+ // Pass `false` for `checkAllowedScopes` as when `#applyScope()`
287
+ // was called, no further checks are required.
288
+ false
289
+ )
290
+ if (query.#isJoinChildQuery) {
291
+ // Join child queries are never executed, and need to apply
292
+ // their scopes manually. Note that it's OK to call this
293
+ // repeatedly, because `_appliedScopes` prevents multiple
294
+ // application of the same scope.
295
+ query.#applyScopes()
296
+ }
297
+ }
298
+ }
299
+ this.withGraph(
300
+ addGraphScope(this.modelClass(), expr, [name], modifiers, true)
301
+ ).modifiers(modifiers)
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ applyFilter(name, ...args) {
308
+ if (isObject(name)) {
309
+ // Multiple filters: applyFilter({ active: [], recent: [30] })
310
+ for (const [key, value] of Object.entries(name)) {
311
+ this.applyFilter(key, ...asArray(value))
312
+ }
313
+ return this
314
+ }
315
+
316
+ if (this.#allowFilters && !this.#allowFilters[name]) {
317
+ throw new QueryBuilderError(`Query filter '${name}' is not allowed.`)
318
+ }
319
+ const filter = this.modelClass().definition.filters[name]
320
+ if (!filter) {
321
+ throw new QueryBuilderError(`Query filter '${name}' is not defined.`)
322
+ }
323
+ // NOTE: Filters are automatically combine with and operations!
324
+ return this.andWhere(query => filter(query, ...args))
325
+ }
326
+
327
+ allowFilter(...filters) {
328
+ this.#allowFilters ||= {}
329
+ for (const filter of filters) {
330
+ this.#allowFilters[filter] = true
331
+ }
332
+ }
333
+
334
+ // A algorithm-agnostic version of `withGraphFetched()` / `withGraphJoined()`,
335
+ // with the algorithm specifiable in the options. Additionally, it handles
336
+ // `_ignoreGraph` and `_graphAlgorithm`:
337
+ withGraph(expr, options = {}) {
338
+ // To make merging easier, keep the current algorithm if none is specified:
339
+ const { algorithm = this.#graphAlgorithm } = options
340
+ const method = {
341
+ fetch: 'withGraphFetched',
342
+ join: 'withGraphJoined'
343
+ }[algorithm]
344
+ if (!method) {
345
+ throw new QueryBuilderError(
346
+ `Graph algorithm '${algorithm}' is unsupported.`
347
+ )
348
+ }
349
+ if (!this.#ignoreGraph) {
350
+ this.#graphAlgorithm = algorithm
351
+ super[method](expr, options)
352
+ }
353
+ return this
354
+ }
355
+
356
+ // @override
357
+ withGraphFetched(expr, options) {
358
+ return this.withGraph(expr, { ...options, algorithm: 'fetch' })
359
+ }
360
+
361
+ // @override
362
+ withGraphJoined(expr, options) {
363
+ return this.withGraph(expr, { ...options, algorithm: 'join' })
364
+ }
365
+
366
+ toSQL() {
367
+ return this.toKnexQuery().toSQL()
368
+ }
369
+
370
+ raw(...args) {
371
+ // TODO: Figure out a way to support `object.raw()` syntax and return a knex
372
+ // raw expression without accessing the private `RawBuilder.toKnexRaw()`:
373
+ return objection.raw(...args).toKnexRaw(this)
374
+ }
375
+
376
+ selectRaw(...args) {
377
+ return this.select(objection.raw(...args))
378
+ }
379
+
380
+ // Non-deprecated version of Objection's `pluck()`
381
+ pluck(key) {
382
+ return this.runAfter(result =>
383
+ isArray(result)
384
+ ? result.map(it => it?.[key])
385
+ : isObject(result)
386
+ ? result[key]
387
+ : result
388
+ )
389
+ }
390
+
391
+ loadDataPath(dataPath, options) {
392
+ const parsedDataPath = parseDataPath(dataPath)
393
+
394
+ const {
395
+ property,
396
+ relation,
397
+ wildcard,
398
+ expression,
399
+ nestedDataPath,
400
+ name
401
+ } = this.modelClass().getPropertyOrRelationAtDataPath(parsedDataPath)
402
+
403
+ if (nestedDataPath) {
404
+ // Once a JSON data type is reached, even if it's not at the end of the
405
+ // provided path, load it and assume we're done with the loading part.
406
+ if (
407
+ !(
408
+ wildcard ||
409
+ property && ['object', 'array'].includes(property.type)
410
+ )
411
+ ) {
412
+ throw new QueryBuilderError(
413
+ `Unable to load full data-path '${
414
+ dataPath
415
+ }' (Unmatched: '${
416
+ nestedDataPath
417
+ }').`
418
+ )
419
+ }
420
+ }
421
+
422
+ // Handle the special case of root-level property separately,
423
+ // because `withGraph('(#propertyName)')` is not supported.
424
+ if (name && !relation) {
425
+ this.select(name)
426
+ } else {
427
+ this.withGraph(expression, options)
428
+ }
429
+ return this
430
+ }
431
+
432
+ // @override
433
+ truncate({ restart = true, cascade = false } = {}) {
434
+ if (this.isPostgreSQL()) {
435
+ // Support `restart` and `cascade` in PostgreSQL truncate queries.
436
+ return this.raw(
437
+ `truncate table ??${
438
+ restart ? ' restart identity' : ''
439
+ }${
440
+ cascade ? ' cascade' : ''
441
+ }`,
442
+ this.modelClass().tableName
443
+ )
444
+ }
445
+ return super.truncate()
446
+ }
447
+
448
+ // @override
449
+ insert(data) {
450
+ // Only PostgreSQL is able to insert multiple entries at once it seems,
451
+ // all others have to fall back on insertGraph() to do so for now:
452
+ return !this.isPostgreSQL() && isArray(data) && data.length > 1
453
+ ? this.insertGraph(data)
454
+ : super.insert(data)
455
+ }
456
+
457
+ // https://github.com/Vincit/objection.js/issues/101#issuecomment-200363667
458
+ upsert(data, options = {}) {
459
+ let mainQuery
460
+ return this.runBefore((result, builder) => {
461
+ if (!builder.context().isMainQuery) {
462
+ // At this point the builder should only contain a bunch of `where*`
463
+ // operations. Store this query for later use in runAfter(). Also mark
464
+ // the query with `isMainQuery: true` so we can skip all this when
465
+ // this function is called for the `mainQuery`.
466
+ mainQuery = builder.clone().context({ isMainQuery: true })
467
+ // Call update() on the original query, turning it into an update.
468
+ builder[options.update ? 'update' : 'patch'](data)
469
+ }
470
+ return result
471
+ }).runAfter((result, builder) => {
472
+ if (!builder.context().isMainQuery) {
473
+ return result === 0
474
+ ? mainQuery[options.fetch ? 'insertAndFetch' : 'insert'](data)
475
+ : // We can use the `mainQuery` we saved in runBefore() to fetch the
476
+ // inserted results. It is noteworthy that this query will return
477
+ // the wrong results if the update changed any of the columns the
478
+ // where operates with. This also returns all updated models.
479
+ mainQuery.first()
480
+ }
481
+ return result
482
+ })
483
+ }
484
+
485
+ find(query, allowParam) {
486
+ if (!query) return this
487
+ const allowed = !allowParam
488
+ ? QueryParameters.getAllowed()
489
+ : // If it's already a lookup object just use it, otherwise convert it:
490
+ isPlainObject(allowParam)
491
+ ? allowParam
492
+ : createLookup(allowParam)
493
+ for (const [key, value] of Object.entries(query)) {
494
+ // Support array notation for multiple parameters, as sent by axios:
495
+ const param = key.endsWith('[]') ? key.slice(0, -2) : key
496
+ if (!allowed[param]) {
497
+ throw new QueryBuilderError(`Query parameter '${key}' is not allowed.`)
498
+ }
499
+ const paramHandler = QueryParameters.get(param)
500
+ if (!paramHandler) {
501
+ throw new QueryBuilderError(
502
+ `Invalid query parameter '${param}' in '${key}=${value}'.`
503
+ )
504
+ }
505
+ paramHandler(this, key, value)
506
+ }
507
+ return this
508
+ }
509
+
510
+ // @override
511
+ findById(id) {
512
+ // Remember id so Model.createNotFoundError() can report it:
513
+ this.context({ byId: id })
514
+ return super.findById(id)
515
+ }
516
+
517
+ // @override
518
+ patchAndFetchById(id, data) {
519
+ this.context({ byId: id })
520
+ return super.patchAndFetchById(id, data)
521
+ }
522
+
523
+ // @override
524
+ updateAndFetchById(id, data) {
525
+ this.context({ byId: id })
526
+ return super.updateAndFetchById(id, data)
527
+ }
528
+
529
+ // @override
530
+ deleteById(id) {
531
+ this.context({ byId: id })
532
+ return super.deleteById(id)
533
+ }
534
+
535
+ patchById(id, data) {
536
+ return this.findById(id).patch(data)
537
+ }
538
+
539
+ updateById(id, data) {
540
+ return this.findById(id).update(data)
541
+ }
542
+
543
+ // Extend Objection's `patchAndFetch()` and `updateAndFetch()` to also support
544
+ // arrays of models, not only single instances.
545
+ // See: https://gitter.im/Vincit/objection.js?at=5f994a5900a0f3369d366d6f
546
+
547
+ patchAndFetch(data) {
548
+ return isArray(data)
549
+ ? this.#upsertAndFetch(data)
550
+ : super.patchAndFetch(data)
551
+ }
552
+
553
+ updateAndFetch(data) {
554
+ return isArray(data)
555
+ ? this.#upsertAndFetch(data, { update: true })
556
+ : super.updateAndFetch(data)
557
+ }
558
+
559
+ upsertAndFetch(data) {
560
+ return this.#upsertAndFetch(data, {
561
+ // Insert missing nodes only at root by allowing `insertMissing`,
562
+ // but set `noInsert` for all relations:
563
+ insertMissing: true,
564
+ noInsert: this.modelClass().getRelationNames()
565
+ })
566
+ }
567
+
568
+ #upsertAndFetch(data, options) {
569
+ return this.upsertGraphAndFetch(data, {
570
+ fetchStrategy: 'OnlyNeeded',
571
+ noInset: true,
572
+ noDelete: true,
573
+ noRelate: true,
574
+ noUnrelate: true,
575
+ ...options
576
+ })
577
+ }
578
+
579
+ insertDitoGraph(data, options) {
580
+ return this.#handleDitoGraph(
581
+ 'insertGraph',
582
+ data,
583
+ options,
584
+ insertDitoGraphOptions
585
+ )
586
+ }
587
+
588
+ insertDitoGraphAndFetch(data, options) {
589
+ return this.#handleDitoGraph(
590
+ 'insertGraphAndFetch',
591
+ data,
592
+ options,
593
+ insertDitoGraphOptions
594
+ )
595
+ }
596
+
597
+ upsertDitoGraph(data, options) {
598
+ return this.#handleDitoGraph(
599
+ 'upsertGraph',
600
+ data,
601
+ options,
602
+ upsertDitoGraphOptions
603
+ )
604
+ }
605
+
606
+ upsertDitoGraphAndFetch(data, options) {
607
+ return this.#handleDitoGraph(
608
+ 'upsertGraphAndFetch',
609
+ data,
610
+ options,
611
+ upsertDitoGraphOptions
612
+ )
613
+ }
614
+
615
+ patchDitoGraph(data, options) {
616
+ return this.#handleDitoGraph(
617
+ 'upsertGraph',
618
+ data,
619
+ options,
620
+ patchDitoGraphOptions
621
+ )
622
+ }
623
+
624
+ patchDitoGraphAndFetch(data, options) {
625
+ return this.#handleDitoGraph(
626
+ 'upsertGraphAndFetch',
627
+ data,
628
+ options,
629
+ patchDitoGraphOptions
630
+ )
631
+ }
632
+
633
+ updateDitoGraph(data, options) {
634
+ return this.#handleDitoGraph(
635
+ 'upsertGraph',
636
+ data,
637
+ options,
638
+ updateDitoGraphOptions
639
+ )
640
+ }
641
+
642
+ updateDitoGraphAndFetch(data, options) {
643
+ return this.#handleDitoGraph(
644
+ 'upsertGraphAndFetch',
645
+ data,
646
+ options,
647
+ updateDitoGraphOptions
648
+ )
649
+ }
650
+
651
+ upsertDitoGraphAndFetchById(id, data, options) {
652
+ this.context({ byId: id })
653
+ return this.upsertDitoGraphAndFetch(
654
+ {
655
+ ...data,
656
+ ...this.modelClass().getReference(id)
657
+ },
658
+ options
659
+ )
660
+ }
661
+
662
+ patchDitoGraphAndFetchById(id, data, options) {
663
+ this.context({ byId: id })
664
+ return this.patchDitoGraphAndFetch(
665
+ {
666
+ ...data,
667
+ ...this.modelClass().getReference(id)
668
+ },
669
+ options
670
+ )
671
+ }
672
+
673
+ updateDitoGraphAndFetchById(id, data, options) {
674
+ this.context({ byId: id })
675
+ return this.updateDitoGraphAndFetch(
676
+ {
677
+ ...data,
678
+ ...this.modelClass().getReference(id)
679
+ },
680
+ options
681
+ )
682
+ }
683
+
684
+ #handleDitoGraph(method, data, options, defaultOptions) {
685
+ const handleGraph = data => {
686
+ const graphProcessor = new DitoGraphProcessor(
687
+ this.modelClass(),
688
+ data,
689
+ {
690
+ ...defaultOptions,
691
+ ...options
692
+ },
693
+ {
694
+ processOverrides: true,
695
+ processRelates: true
696
+ }
697
+ )
698
+ this[method](graphProcessor.getData(), graphProcessor.getOptions())
699
+ }
700
+
701
+ if (options?.cyclic && method.startsWith('upsert')) {
702
+ // `_upsertCyclicDitoGraphAndFetch()` needs to run asynchronously,
703
+ // but we can't do so here and `runBefore()` executes too late,
704
+ // so use `#executeFirst()` to work around it.
705
+ this.#executeFirst = async () => {
706
+ this.#executeFirst = null
707
+ handleGraph(
708
+ await this.clone().#upsertCyclicDitoGraphAndFetch(data, options)
709
+ )
710
+ }
711
+ } else {
712
+ handleGraph(data)
713
+ }
714
+
715
+ return this
716
+ }
717
+
718
+ async #upsertCyclicDitoGraphAndFetch(data, options) {
719
+ // TODO: This is part of a workaround for the following Objection.js issue.
720
+ // Replace with a normal `upsertGraphAndFetch()` once it is fixed:
721
+ // https://github.com/Vincit/objection.js/issues/1482
722
+
723
+ // First, collect all #id identifiers and #ref references in the graph,
724
+ // along with their data paths.
725
+ const identifiers = {}
726
+ const references = {}
727
+
728
+ const { uidProp, uidRefProp } = this.modelClass()
729
+
730
+ walkGraph(data, (value, path) => {
731
+ if (isObject(value)) {
732
+ const { [uidProp]: id, [uidRefProp]: ref } = value
733
+ if (id) {
734
+ // TODO: Also store the correct `idColumn` property for the given path
735
+ identifiers[id] = path.join('/')
736
+ } else if (ref) {
737
+ references[path.join('/')] = ref
738
+ }
739
+ }
740
+ })
741
+
742
+ // Now clone the data and delete all references from it, for the initial
743
+ // upsert.
744
+ const cloned = clone(data)
745
+ const sparseArrays = {}
746
+ for (const path of Object.keys(references)) {
747
+ const parts = parseDataPath(path)
748
+ const key = parts.pop()
749
+ const parent = getValueAtDataPath(cloned, parts)
750
+ delete parent[key]
751
+ if (isArray(parent)) {
752
+ // For arrays, deleting the entry at `key` will leave 'holes' in the
753
+ // array and make it sparse. Collect it in order to compress it later.
754
+ sparseArrays[normalizeDataPath(parts)] = parent
755
+ }
756
+ }
757
+
758
+ // Now condense the sparse arrays to remove the empty entries again:
759
+ for (const [dataPath, array] of Object.entries(sparseArrays)) {
760
+ setValueAtDataPath(cloned, dataPath, Object.values(array))
761
+ }
762
+
763
+ // TODO: The model isn't necessarily fetched with data in the same order as
764
+ // `cloned` defines, e.g. if there is sorting in the database. A solid
765
+ // implementation of this would take care of that and map entries from
766
+ // `model` back to `cloned`, so that the `setDataPath` calls below would
767
+ // still work in such cases.
768
+ const { cyclic, ...opts } = options
769
+ const model = await this.upsertDitoGraphAndFetch(cloned, opts)
770
+
771
+ // Now for each identifier, create an object containing only the final id in
772
+ // the fetched model data:
773
+ const links = {}
774
+ for (const [identifier, path] of Object.entries(identifiers)) {
775
+ // TODO: Use the correct `idColumn` property for the given path
776
+ const { id } = getValueAtDataPath(model, path)
777
+ links[identifier] = { id }
778
+ }
779
+
780
+ // And finally replace all references with the final ids, before upserting
781
+ // once again:
782
+ for (const [path, reference] of Object.entries(references)) {
783
+ const link = links[reference]
784
+ if (link) {
785
+ setValueAtDataPath(model, path, link)
786
+ }
787
+ }
788
+
789
+ return model
790
+ }
791
+
792
+ static mixin(target) {
793
+ // Expose a selection of QueryBuilder methods directly on the target,
794
+ // redirecting the calls to `this.query()[method](...)`
795
+ for (const method of mixinMethods) {
796
+ if (method in target) {
797
+ console.warn(
798
+ `There is already a property named '${method}' on '${target}'`
799
+ )
800
+ } else {
801
+ Object.defineProperty(target, method, {
802
+ value(...args) {
803
+ return this.query()[method](...args)
804
+ },
805
+ configurable: true,
806
+ enumerable: false
807
+ })
808
+ }
809
+ }
810
+ }
811
+ }
812
+
813
+ KnexHelper.mixin(QueryBuilder.prototype)
814
+
815
+ // Add conversion of identifiers to all `where` statements, as well as to
816
+ // `select` and `orderBy`, by detecting use of model properties and expanding
817
+ // them to `${tableRefFor(modelClass}.${propertyName}`, for unambiguous
818
+ // identification of used properties in complex statements.
819
+ for (const key of [
820
+ 'where', 'andWhere', 'orWhere',
821
+ 'whereNot', 'orWhereNot',
822
+ 'whereIn', 'orWhereIn',
823
+ 'whereNotIn', 'orWhereNotIn',
824
+ 'whereNull', 'orWhereNull',
825
+ 'whereNotNull', 'orWhereNotNull',
826
+ 'whereBetween', 'andWhereBetween', 'orWhereBetween',
827
+ 'whereNotBetween', 'andWhereNotBetween', 'orWhereNotBetween',
828
+ 'whereColumn', 'andWhereColumn', 'orWhereColumn',
829
+ 'whereNotColumn', 'andWhereNotColumn', 'orWhereNotColumn',
830
+ 'whereComposite', 'andWhereComposite', 'orWhereComposite',
831
+ 'whereInComposite',
832
+ 'whereNotInComposite',
833
+
834
+ 'having', 'orHaving',
835
+ 'havingIn', 'orHavingIn',
836
+ 'havingNotIn', 'orHavingNotIn',
837
+ 'havingNull', 'orHavingNull',
838
+ 'havingNotNull', 'orHavingNotNull',
839
+ 'havingBetween', 'orHavingBetween',
840
+ 'havingNotBetween', 'orHavingNotBetween',
841
+
842
+ 'select', 'column', 'columns', 'first',
843
+
844
+ 'groupBy', 'orderBy'
845
+ ]) {
846
+ const method = QueryBuilder.prototype[key]
847
+ QueryBuilder.prototype[key] = function (...args) {
848
+ const modelClass = this.modelClass()
849
+ const { properties } = modelClass.definition
850
+
851
+ // Expands all identifiers known to the model to their extended versions.
852
+ const expandIdentifier = identifier => {
853
+ // Support expansion of identifiers with aliases, e.g. `name AS newName`
854
+ const alias = (
855
+ isString(identifier) &&
856
+ identifier.match(/^\s*([a-z][\w_]+)(\s+AS\s+.*)$/i)
857
+ )
858
+ return alias
859
+ ? `${expandIdentifier(alias[1])}${alias[2]}`
860
+ : identifier === '*' || identifier in properties
861
+ ? `${this.tableRefFor(modelClass)}.${identifier}`
862
+ : identifier
863
+ }
864
+
865
+ const convertArgument = arg => {
866
+ if (isString(arg)) {
867
+ arg = expandIdentifier(arg)
868
+ } else if (isArray(arg)) {
869
+ arg = arg.map(expandIdentifier)
870
+ } else if (isPlainObject(arg)) {
871
+ arg = mapKeys(arg, expandIdentifier)
872
+ }
873
+ return arg
874
+ }
875
+
876
+ const length = ['select', 'column', 'columns', 'first'].includes(key)
877
+ ? args.length
878
+ : 1
879
+ for (let i = 0; i < length; i++) {
880
+ args[i] = convertArgument(args[i])
881
+ }
882
+ return method.call(this, ...args)
883
+ }
884
+ }
885
+
886
+ function getScopes(scopes) {
887
+ let checkAllowedScopes = true
888
+ const last = scopes.at(-1)
889
+ if (isBoolean(last)) {
890
+ checkAllowedScopes = last
891
+ scopes = scopes.slice(0, -1)
892
+ }
893
+ return { checkAllowedScopes, scopes }
894
+ }
895
+
896
+ function filterScopes(scopes, callback) {
897
+ return Object.fromEntries(
898
+ Object.entries(scopes).filter(
899
+ ([scope, graph]) => callback(scope, graph)
900
+ )
901
+ )
902
+ }
903
+
904
+ // The default options for insertDitoGraph(), upsertDitoGraph(),
905
+ // updateDitoGraph() and patchDitoGraph()
906
+ const insertDitoGraphOptions = {
907
+ // When working with large graphs, using the 'OnlyNeeded' fetch-strategy
908
+ // will reduce the number of update queries from a lot to only those
909
+ // rows that have changes.
910
+ fetchStrategy: 'OnlyNeeded',
911
+ relate: true,
912
+ allowRefs: true
913
+ }
914
+
915
+ const upsertDitoGraphOptions = {
916
+ ...insertDitoGraphOptions,
917
+ insertMissing: true,
918
+ unrelate: true
919
+ }
920
+
921
+ const patchDitoGraphOptions = {
922
+ ...upsertDitoGraphOptions,
923
+ insertMissing: false
924
+ }
925
+
926
+ const updateDitoGraphOptions = {
927
+ ...patchDitoGraphOptions,
928
+ insertMissing: false,
929
+ update: true
930
+ }
931
+
932
+ function addGraphScope(modelClass, expr, scopes, modifiers, isRoot = false) {
933
+ if (isRoot) {
934
+ expr = clone(expr)
935
+ } else {
936
+ // Only add the scope if it's not already defined by the graph expression
937
+ // and if it's actually available in the model's list of modifiers.
938
+ for (const scope of scopes) {
939
+ if (
940
+ !expr.$modify.includes(scope) && (
941
+ modelClass.hasScope(scope) ||
942
+ modifiers[scope]
943
+ )
944
+ ) {
945
+ expr.$modify.push(scope)
946
+ }
947
+ }
948
+ }
949
+ const relations = modelClass.getRelations()
950
+ for (const key in expr) {
951
+ // All enumerable properties that don't start with '$' are child nodes.
952
+ if (key[0] !== '$') {
953
+ const childExpr = expr[key]
954
+ const relation = relations[childExpr.$relation || key]
955
+ if (!relation) {
956
+ throw new RelationError(`Invalid child expression: '${key}'`)
957
+ }
958
+ addGraphScope(relation.relatedModelClass, childExpr, scopes, modifiers)
959
+ }
960
+ }
961
+ return expr
962
+ }
963
+
964
+ // List of all `QueryBuilder` methods to be mixed into `Model` as a short-cut
965
+ // for `model.query().METHOD()`
966
+ //
967
+ // Use this code to find all `QueryBuilder` methods:
968
+ //
969
+ // function getAllPropertyNames(obj) {
970
+ // const proto = Object.getPrototypeOf(obj)
971
+ // const inherited = proto ? getAllPropertyNames(proto) : []
972
+ // return [...new Set(Object.getOwnPropertyNames(obj).concat(inherited))]
973
+ // }
974
+ //
975
+ // console.dir(getAllPropertyNames(QueryBuilder.prototype).sort(), {
976
+ // colors: true,
977
+ // depth: null,
978
+ // maxArrayLength: null
979
+ // })
980
+
981
+ const mixinMethods = [
982
+ 'first',
983
+ 'find',
984
+ 'findOne',
985
+ 'findById',
986
+
987
+ 'withGraph',
988
+ 'withGraphFetched',
989
+ 'withGraphJoined',
990
+ 'clearWithGraph',
991
+
992
+ 'withScope',
993
+ 'applyScope',
994
+ 'clearWithScope',
995
+
996
+ 'clear',
997
+ 'select',
998
+
999
+ 'insert',
1000
+ 'upsert',
1001
+
1002
+ 'update',
1003
+ 'patch',
1004
+ 'delete',
1005
+
1006
+ 'updateById',
1007
+ 'patchById',
1008
+ 'deleteById',
1009
+
1010
+ 'truncate',
1011
+
1012
+ 'insertAndFetch',
1013
+ 'upsertAndFetch',
1014
+ 'updateAndFetch',
1015
+ 'patchAndFetch',
1016
+
1017
+ 'updateAndFetchById',
1018
+ 'patchAndFetchById',
1019
+
1020
+ 'insertGraph',
1021
+ 'upsertGraph',
1022
+ 'insertGraphAndFetch',
1023
+ 'upsertGraphAndFetch',
1024
+
1025
+ 'insertDitoGraph',
1026
+ 'upsertDitoGraph',
1027
+ 'updateDitoGraph',
1028
+ 'patchDitoGraph',
1029
+ 'insertDitoGraphAndFetch',
1030
+ 'upsertDitoGraphAndFetch',
1031
+ 'updateDitoGraphAndFetch',
1032
+ 'patchDitoGraphAndFetch',
1033
+
1034
+ 'upsertDitoGraphAndFetchById',
1035
+ 'updateDitoGraphAndFetchById',
1036
+ 'patchDitoGraphAndFetchById',
1037
+
1038
+ 'where',
1039
+ 'whereNot',
1040
+ 'whereRaw',
1041
+ 'whereWrapped',
1042
+ 'whereExists',
1043
+ 'whereNotExists',
1044
+ 'whereIn',
1045
+ 'whereNotIn',
1046
+ 'whereNull',
1047
+ 'whereNotNull',
1048
+ 'whereBetween',
1049
+ 'whereNotBetween',
1050
+ 'whereColumn',
1051
+ 'whereNotColumn',
1052
+ 'whereComposite',
1053
+ 'whereInComposite',
1054
+ 'whereNotInComposite',
1055
+ 'whereJsonHasAny',
1056
+ 'whereJsonHasAll',
1057
+ 'whereJsonIsArray',
1058
+ 'whereJsonNotArray',
1059
+ 'whereJsonIsObject',
1060
+ 'whereJsonNotObject',
1061
+ 'whereJsonSubsetOf',
1062
+ 'whereJsonNotSubsetOf',
1063
+ 'whereJsonSupersetOf',
1064
+ 'whereJsonNotSupersetOf',
1065
+
1066
+ 'having',
1067
+ 'havingIn',
1068
+ 'havingNotIn',
1069
+ 'havingNull',
1070
+ 'havingNotNull',
1071
+ 'havingExists',
1072
+ 'havingNotExists',
1073
+ 'havingBetween',
1074
+ 'havingNotBetween',
1075
+ 'havingRaw',
1076
+ 'havingWrapped'
1077
+ ]