@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,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
|
+
]
|