velocious 1.0.466 → 1.0.467
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 +2 -1
- package/build/configuration.js +2 -2
- package/build/database/pool/async-tracked-multi-connection.js +7 -2
- package/build/database/pool/single-multi-use.js +38 -0
- package/build/frontend-model-controller.js +162 -56
- package/build/frontend-models/query.js +55 -33
- package/build/src/configuration.js +3 -3
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +9 -3
- package/build/src/database/pool/single-multi-use.d.ts +1 -0
- package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
- package/build/src/database/pool/single-multi-use.js +34 -1
- package/build/src/frontend-model-controller.d.ts +45 -5
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +155 -57
- package/build/src/frontend-models/query.d.ts +8 -0
- package/build/src/frontend-models/query.d.ts.map +1 -1
- package/build/src/frontend-models/query.js +53 -34
- package/build/src/utils/ransack.d.ts +11 -3
- package/build/src/utils/ransack.d.ts.map +1 -1
- package/build/src/utils/ransack.js +42 -23
- package/build/utils/ransack.js +44 -22
- package/package.json +1 -1
- package/src/configuration.js +2 -2
- package/src/database/pool/async-tracked-multi-connection.js +7 -2
- package/src/database/pool/single-multi-use.js +38 -0
- package/src/frontend-model-controller.js +162 -56
- package/src/frontend-models/query.js +55 -33
- package/src/utils/ransack.js +44 -22
package/README.md
CHANGED
|
@@ -578,6 +578,7 @@ Use `await FrontendModelBase.waitForIdle()` when a test harness or app lifecycle
|
|
|
578
578
|
Frontend-model HTTP requests always use `credentials: "include"` so shared custom commands can set session cookies without app-level transport overrides.
|
|
579
579
|
|
|
580
580
|
Unexpected frontend-model endpoint failures stay client-safe in production with `errorMessage: "Request failed."`.
|
|
581
|
+
Invalid client query descriptors, such as unknown `select`, `where`, `search`, `joins`, `group`, `sort`, `pluck`, or Ransack attributes, return the specific frontend-model query error message with `velocious.code: "frontend-model-query-error"` and are not emitted as framework errors.
|
|
581
582
|
In `development` and `test`, Velocious also includes `debugErrorClass`, `debugErrorMessage`, and `debugBacktrace` fields so browser/system-test failures are easier to diagnose without exposing those details in production.
|
|
582
583
|
Other non-production environments, such as `staging`, keep the same client-safe default unless you explicitly opt in with `exposeInternalErrorsToClients: true`:
|
|
583
584
|
|
|
@@ -1619,7 +1620,7 @@ configuration.getErrorEvents().on("all-error", ({error, errorType}) => {
|
|
|
1619
1620
|
})
|
|
1620
1621
|
```
|
|
1621
1622
|
|
|
1622
|
-
Genuinely unexpected frontend-model command failures reach this bus too. The frontend-model controller catches them to return a client-safe `Request failed.` response, but it also emits them as `framework-error`/`all-error` (with `context.frontendModelEndpoint === true`) so they are reported instead of being silently swallowed. Expected user-flow errors are excluded: validation failures are forwarded with their real message (for example `Name can't be blank`)
|
|
1623
|
+
Genuinely unexpected frontend-model command failures reach this bus too. The frontend-model controller catches them to return a client-safe `Request failed.` response, but it also emits them as `framework-error`/`all-error` (with `context.frontendModelEndpoint === true`) so they are reported instead of being silently swallowed. Expected user-flow errors are excluded: validation failures are forwarded with their real message (for example `Name can't be blank`), invalid client query descriptors are returned as frontend-model query errors, and `error.velocious`-annotated / `safeToExpose` / `errorType`-marked errors keep their expected-error status — none of these reach the error bus.
|
|
1623
1624
|
|
|
1624
1625
|
## Use the Websocket client API (HTTP-like)
|
|
1625
1626
|
|
package/build/configuration.js
CHANGED
|
@@ -2230,8 +2230,8 @@ export default class VelociousConfiguration {
|
|
|
2230
2230
|
|
|
2231
2231
|
if (!matches) continue
|
|
2232
2232
|
|
|
2233
|
-
this.withoutCurrentConnectionContexts(() => {
|
|
2234
|
-
|
|
2233
|
+
void this.withoutCurrentConnectionContexts(() => {
|
|
2234
|
+
return Promise
|
|
2235
2235
|
.resolve()
|
|
2236
2236
|
.then(() => this._deliverWebsocketChannelBroadcast(subscription, body, {eventId: meta?.eventId}))
|
|
2237
2237
|
.catch((error) => {
|
|
@@ -6,6 +6,7 @@ import BasePool, {POOL_CONFIGURATION_KEY} from "./base.js"
|
|
|
6
6
|
export const CLOSED_CONNECTION = Symbol("velociousClosedConnection")
|
|
7
7
|
const IDLE_CONNECTION_CHECKED_IN_AT = Symbol("velociousIdleConnectionCheckedInAt")
|
|
8
8
|
const CONNECTION_CHECKED_OUT_AT = Symbol("velociousConnectionCheckedOutAt")
|
|
9
|
+
const SUPPRESSED_CONNECTION_CONTEXT = Symbol("velociousSuppressedConnectionContext")
|
|
9
10
|
const DEFAULT_MAX_CONNECTIONS = 10
|
|
10
11
|
const DEFAULT_IDLE_TIMEOUT_MILLIS = 5000
|
|
11
12
|
const DEFAULT_CHECKOUT_TIMEOUT_MILLIS = 10000
|
|
@@ -103,7 +104,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
103
104
|
* Runs a callback without the inherited current connection context.
|
|
104
105
|
* @type {(callback: () => ?) => ?}
|
|
105
106
|
*/
|
|
106
|
-
const withoutCurrentConnectionContext = (callback) => this.asyncLocalStorage.run(
|
|
107
|
+
const withoutCurrentConnectionContext = (callback) => this.asyncLocalStorage.run(SUPPRESSED_CONNECTION_CONTEXT, callback)
|
|
107
108
|
this._withoutCurrentConnectionContext = withoutCurrentConnectionContext
|
|
108
109
|
}
|
|
109
110
|
|
|
@@ -720,6 +721,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
720
721
|
const id = this.asyncLocalStorage.getStore()
|
|
721
722
|
|
|
722
723
|
if (id === undefined) return this.currentFallbackConnectionOrFail()
|
|
724
|
+
if (id === SUPPRESSED_CONNECTION_CONTEXT) return this.currentFallbackConnectionOrFail()
|
|
723
725
|
|
|
724
726
|
this.ensureConnectionIsInUse(id)
|
|
725
727
|
|
|
@@ -815,6 +817,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
815
817
|
getCurrentContextConnection() {
|
|
816
818
|
const id = this.asyncLocalStorage.getStore()
|
|
817
819
|
|
|
820
|
+
if (id === SUPPRESSED_CONNECTION_CONTEXT) return undefined
|
|
818
821
|
if (id === undefined) return this._testSharedConnection
|
|
819
822
|
|
|
820
823
|
return this.getCurrentConnection()
|
|
@@ -825,7 +828,9 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
825
828
|
* @returns {boolean} - Whether nested code can reuse the current connection context.
|
|
826
829
|
*/
|
|
827
830
|
hasCurrentConnectionContext() {
|
|
828
|
-
|
|
831
|
+
const id = this.asyncLocalStorage.getStore()
|
|
832
|
+
|
|
833
|
+
return id !== undefined && id !== SUPPRESSED_CONNECTION_CONTEXT
|
|
829
834
|
}
|
|
830
835
|
|
|
831
836
|
/**
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import BasePool from "./base.js"
|
|
4
4
|
|
|
5
5
|
export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
|
|
6
|
+
suppressedConnectionContextCount = 0
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Runs checkin.
|
|
8
10
|
* @param {import("../drivers/base.js").default} connection - Connection.
|
|
@@ -58,6 +60,32 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Runs without current connection context.
|
|
65
|
+
* @template T
|
|
66
|
+
* @param {() => T} callback - Callback to run without the shared current connection.
|
|
67
|
+
* @returns {T} - Callback result.
|
|
68
|
+
*/
|
|
69
|
+
withoutCurrentConnectionContext(callback) {
|
|
70
|
+
this.suppressedConnectionContextCount += 1
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = callback()
|
|
74
|
+
|
|
75
|
+
if (result instanceof Promise) {
|
|
76
|
+
return /** @type {T} */ (result.finally(() => {
|
|
77
|
+
this.suppressedConnectionContextCount -= 1
|
|
78
|
+
}))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.suppressedConnectionContextCount -= 1
|
|
82
|
+
return result
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.suppressedConnectionContextCount -= 1
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
/**
|
|
62
90
|
* Clears schema metadata cached by the reusable connection if it exists.
|
|
63
91
|
* @returns {void} - No return value.
|
|
@@ -97,9 +125,19 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
|
|
|
97
125
|
* @returns {import("../drivers/base.js").default | undefined} - The current context connection.
|
|
98
126
|
*/
|
|
99
127
|
getCurrentContextConnection() {
|
|
128
|
+
if (this.suppressedConnectionContextCount > 0) return undefined
|
|
129
|
+
|
|
100
130
|
return this.connection
|
|
101
131
|
}
|
|
102
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Returns whether the shared connection is available to the current execution context.
|
|
135
|
+
* @returns {boolean} - Whether nested code can reuse the shared connection.
|
|
136
|
+
*/
|
|
137
|
+
hasCurrentConnectionContext() {
|
|
138
|
+
return this.suppressedConnectionContextCount === 0
|
|
139
|
+
}
|
|
140
|
+
|
|
103
141
|
/**
|
|
104
142
|
* Runs get debug snapshot.
|
|
105
143
|
* @returns {import("./base.js").DatabasePoolDebugSnapshot} - Diagnostic snapshot for this pool.
|
|
@@ -6,7 +6,7 @@ import FrontendModelBaseResource from "./frontend-model-resource/base-resource.j
|
|
|
6
6
|
import Response from "./http-server/client/response.js"
|
|
7
7
|
import {frontendModelResourcesWithBuiltInsForBackendProject} from "./frontend-models/built-in-resources.js"
|
|
8
8
|
import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcePath, frontendModelResourcesForBackendProject} from "./frontend-models/resource-definition.js"
|
|
9
|
-
import {normalizeGroup as normalizeQueryGroup, normalizeJoins as normalizeQueryJoins, normalizePluck as normalizeQueryPluck, normalizePreload as normalizeQueryPreload, normalizeSearchOperator as normalizeQuerySearchOperator, normalizeSort as normalizeQuerySort} from "./frontend-models/query.js"
|
|
9
|
+
import {FrontendModelQueryError, normalizeGroup as normalizeQueryGroup, normalizeJoins as normalizeQueryJoins, normalizePluck as normalizeQueryPluck, normalizePreload as normalizeQueryPreload, normalizeSearchOperator as normalizeQuerySearchOperator, normalizeSort as normalizeQuerySort} from "./frontend-models/query.js"
|
|
10
10
|
import {assignSafeProperty, deserializeFrontendModelTransportValue, isBackendModelInstance, serializeFrontendModelTransportValue} from "./frontend-models/transport-serialization.js"
|
|
11
11
|
import RoutesResolver from "./routes/resolver.js"
|
|
12
12
|
import {ValidationError} from "./database/record/index.js"
|
|
@@ -14,6 +14,7 @@ import { normalizeDateStringForWrite } from "./database/datetime-storage.js"
|
|
|
14
14
|
import VelociousError from "./velocious-error.js"
|
|
15
15
|
import isDate from "./utils/is-date.js"
|
|
16
16
|
import isPlainObject from "./utils/plain-object.js"
|
|
17
|
+
import {RansackQueryError, normalizeRansackGroup, parseRansackSort} from "./utils/ransack.js"
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Runs normalize frontend model preload.
|
|
@@ -37,7 +38,7 @@ function normalizeFrontendModelJoins(joins) {
|
|
|
37
38
|
try {
|
|
38
39
|
return normalizeQueryJoins(joins)
|
|
39
40
|
} catch (error) {
|
|
40
|
-
|
|
41
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -52,7 +53,7 @@ function normalizeFrontendModelSelect(select, rootModelName = null) {
|
|
|
52
53
|
|
|
53
54
|
if (typeof select === "string") {
|
|
54
55
|
if (!rootModelName) {
|
|
55
|
-
throw
|
|
56
|
+
throw frontendModelQueryError("Invalid select shorthand without root model name")
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
return {[rootModelName]: [select]}
|
|
@@ -60,12 +61,12 @@ function normalizeFrontendModelSelect(select, rootModelName = null) {
|
|
|
60
61
|
|
|
61
62
|
if (Array.isArray(select)) {
|
|
62
63
|
if (!rootModelName) {
|
|
63
|
-
throw
|
|
64
|
+
throw frontendModelQueryError("Invalid select shorthand without root model name")
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
for (const attributeName of select) {
|
|
67
68
|
if (typeof attributeName !== "string") {
|
|
68
|
-
throw
|
|
69
|
+
throw frontendModelQueryError(`Invalid select attribute for ${rootModelName}: ${typeof attributeName}`)
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -73,7 +74,7 @@ function normalizeFrontendModelSelect(select, rootModelName = null) {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
if (!isPlainObject(select)) {
|
|
76
|
-
throw
|
|
77
|
+
throw frontendModelQueryError(`Invalid select type: ${typeof select}`)
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
/**
|
|
@@ -88,12 +89,12 @@ function normalizeFrontendModelSelect(select, rootModelName = null) {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
if (!Array.isArray(selectValue)) {
|
|
91
|
-
throw
|
|
92
|
+
throw frontendModelQueryError(`Invalid select value for ${modelName}: ${typeof selectValue}`)
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
for (const attributeName of selectValue) {
|
|
95
96
|
if (typeof attributeName !== "string") {
|
|
96
|
-
throw
|
|
97
|
+
throw frontendModelQueryError(`Invalid select attribute for ${modelName}: ${typeof attributeName}`)
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -175,27 +176,25 @@ function frontendModelQueryMetadata(query) {
|
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
/**
|
|
178
|
-
*
|
|
179
|
-
* @param {string} message -
|
|
180
|
-
* @returns {VelociousError}
|
|
179
|
+
* Builds a client-safe frontend-model query error.
|
|
180
|
+
* @param {string} message - Error message.
|
|
181
|
+
* @returns {VelociousError} Client-safe query error.
|
|
181
182
|
*/
|
|
182
|
-
function
|
|
183
|
-
return VelociousError.safe(message, {code: "frontend-model-
|
|
183
|
+
function frontendModelQueryError(message) {
|
|
184
|
+
return VelociousError.safe(message, {code: "frontend-model-query-error"})
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
/**
|
|
187
|
-
*
|
|
188
|
+
* Throws a client-safe frontend-model query error for typed query parser errors.
|
|
188
189
|
* @param {?} error - Error raised while normalizing client query params.
|
|
189
|
-
* @returns {
|
|
190
|
+
* @returns {never} Always throws.
|
|
190
191
|
*/
|
|
191
|
-
function
|
|
192
|
-
if (error instanceof
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
? error.message
|
|
196
|
-
: String(error)
|
|
192
|
+
function throwFrontendModelQueryErrorForParserError(error) {
|
|
193
|
+
if (error instanceof FrontendModelQueryError || error instanceof RansackQueryError) {
|
|
194
|
+
throw frontendModelQueryError(error.message)
|
|
195
|
+
}
|
|
197
196
|
|
|
198
|
-
|
|
197
|
+
throw error
|
|
199
198
|
}
|
|
200
199
|
|
|
201
200
|
/**
|
|
@@ -338,7 +337,7 @@ function normalizeFrontendModelSearches(searches) {
|
|
|
338
337
|
if (!searches) return []
|
|
339
338
|
|
|
340
339
|
if (!Array.isArray(searches)) {
|
|
341
|
-
throw
|
|
340
|
+
throw frontendModelQueryError(`Invalid searches type: ${typeof searches}`)
|
|
342
341
|
}
|
|
343
342
|
|
|
344
343
|
/**
|
|
@@ -348,7 +347,7 @@ function normalizeFrontendModelSearches(searches) {
|
|
|
348
347
|
|
|
349
348
|
for (const search of searches) {
|
|
350
349
|
if (!isPlainObject(search)) {
|
|
351
|
-
throw
|
|
350
|
+
throw frontendModelQueryError(`Invalid search entry type: ${typeof search}`)
|
|
352
351
|
}
|
|
353
352
|
|
|
354
353
|
const path = search.path
|
|
@@ -356,26 +355,34 @@ function normalizeFrontendModelSearches(searches) {
|
|
|
356
355
|
const operator = search.operator
|
|
357
356
|
|
|
358
357
|
if (!Array.isArray(path)) {
|
|
359
|
-
throw
|
|
358
|
+
throw frontendModelQueryError("Invalid search path: expected an array")
|
|
360
359
|
}
|
|
361
360
|
|
|
362
361
|
for (const pathEntry of path) {
|
|
363
362
|
if (typeof pathEntry !== "string" || pathEntry.length < 1) {
|
|
364
|
-
throw
|
|
363
|
+
throw frontendModelQueryError("Invalid search path entry: expected non-empty string")
|
|
365
364
|
}
|
|
366
365
|
}
|
|
367
366
|
|
|
368
367
|
if (typeof column !== "string" || column.length < 1) {
|
|
369
|
-
throw
|
|
368
|
+
throw frontendModelQueryError("Invalid search column: expected non-empty string")
|
|
370
369
|
}
|
|
371
370
|
|
|
372
371
|
if (typeof operator !== "string") {
|
|
373
|
-
throw
|
|
372
|
+
throw frontendModelQueryError(`Invalid search operator: ${operator}`)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let normalizedOperator
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
normalizedOperator = normalizeQuerySearchOperator(operator)
|
|
379
|
+
} catch (error) {
|
|
380
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
374
381
|
}
|
|
375
382
|
|
|
376
383
|
normalized.push({
|
|
377
384
|
column,
|
|
378
|
-
operator:
|
|
385
|
+
operator: normalizedOperator,
|
|
379
386
|
path: [...path],
|
|
380
387
|
value: search.value
|
|
381
388
|
})
|
|
@@ -393,7 +400,7 @@ function normalizeFrontendModelWhere(where) {
|
|
|
393
400
|
if (!where) return null
|
|
394
401
|
|
|
395
402
|
if (!isPlainObject(where)) {
|
|
396
|
-
throw
|
|
403
|
+
throw frontendModelQueryError(`Invalid where type: ${typeof where}`)
|
|
397
404
|
}
|
|
398
405
|
|
|
399
406
|
return where
|
|
@@ -408,7 +415,7 @@ function normalizeFrontendModelRansack(ransack) {
|
|
|
408
415
|
if (!ransack) return null
|
|
409
416
|
|
|
410
417
|
if (!isPlainObject(ransack)) {
|
|
411
|
-
throw
|
|
418
|
+
throw frontendModelQueryError(`Invalid ransack type: ${typeof ransack}`)
|
|
412
419
|
}
|
|
413
420
|
|
|
414
421
|
return ransack
|
|
@@ -425,11 +432,11 @@ function normalizeFrontendModelIntegerParam(value, name, min) {
|
|
|
425
432
|
if (value == null) return null
|
|
426
433
|
|
|
427
434
|
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
428
|
-
throw
|
|
435
|
+
throw frontendModelQueryError(`Invalid ${name}: expected integer number`)
|
|
429
436
|
}
|
|
430
437
|
|
|
431
438
|
if (value < min) {
|
|
432
|
-
throw
|
|
439
|
+
throw frontendModelQueryError(`Invalid ${name}: expected value >= ${min}`)
|
|
433
440
|
}
|
|
434
441
|
|
|
435
442
|
return value
|
|
@@ -462,7 +469,7 @@ function normalizeFrontendModelDistinct(distinct) {
|
|
|
462
469
|
if (distinct == null) return null
|
|
463
470
|
|
|
464
471
|
if (typeof distinct !== "boolean") {
|
|
465
|
-
throw
|
|
472
|
+
throw frontendModelQueryError(`Invalid distinct: expected boolean`)
|
|
466
473
|
}
|
|
467
474
|
|
|
468
475
|
return distinct
|
|
@@ -1194,7 +1201,11 @@ export default class FrontendModelController extends Controller {
|
|
|
1194
1201
|
* @returns {FrontendModelSort[]} - Frontend sort definitions.
|
|
1195
1202
|
*/
|
|
1196
1203
|
frontendModelSort() {
|
|
1197
|
-
|
|
1204
|
+
try {
|
|
1205
|
+
return normalizeQuerySort(this.frontendModelParams().sort)
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
1208
|
+
}
|
|
1198
1209
|
}
|
|
1199
1210
|
|
|
1200
1211
|
/**
|
|
@@ -1205,7 +1216,7 @@ export default class FrontendModelController extends Controller {
|
|
|
1205
1216
|
try {
|
|
1206
1217
|
return normalizeQueryGroup(this.frontendModelParams().group)
|
|
1207
1218
|
} catch (error) {
|
|
1208
|
-
|
|
1219
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
1209
1220
|
}
|
|
1210
1221
|
}
|
|
1211
1222
|
|
|
@@ -1240,11 +1251,11 @@ export default class FrontendModelController extends Controller {
|
|
|
1240
1251
|
try {
|
|
1241
1252
|
const pluck = normalizeQueryPluck(this.frontendModelParams().pluck)
|
|
1242
1253
|
|
|
1243
|
-
this.
|
|
1254
|
+
this.assertFrontendModelPluckDefinitionsAllowed(pluck)
|
|
1244
1255
|
|
|
1245
1256
|
return pluck
|
|
1246
1257
|
} catch (error) {
|
|
1247
|
-
|
|
1258
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
1248
1259
|
}
|
|
1249
1260
|
}
|
|
1250
1261
|
|
|
@@ -1517,6 +1528,7 @@ export default class FrontendModelController extends Controller {
|
|
|
1517
1528
|
const ransack = this.frontendModelRansack()
|
|
1518
1529
|
|
|
1519
1530
|
if (ransack) {
|
|
1531
|
+
this.assertFrontendModelRansackAllowed(ransack)
|
|
1520
1532
|
query.ransack(ransack)
|
|
1521
1533
|
}
|
|
1522
1534
|
|
|
@@ -1639,7 +1651,7 @@ export default class FrontendModelController extends Controller {
|
|
|
1639
1651
|
})
|
|
1640
1652
|
|
|
1641
1653
|
if (!columnName) {
|
|
1642
|
-
throw
|
|
1654
|
+
throw frontendModelQueryError(`Unknown pluck column "${pluckEntry.column}" for ${targetModelClass.name}`)
|
|
1643
1655
|
}
|
|
1644
1656
|
|
|
1645
1657
|
if (pluckEntry.path.length > 0) {
|
|
@@ -1735,11 +1747,11 @@ export default class FrontendModelController extends Controller {
|
|
|
1735
1747
|
}
|
|
1736
1748
|
|
|
1737
1749
|
/**
|
|
1738
|
-
*
|
|
1750
|
+
* Asserts frontend-model pluck definitions only reference exposed resource attributes.
|
|
1739
1751
|
* @param {FrontendModelPluck[]} pluck - Pluck descriptors.
|
|
1740
1752
|
* @returns {void}
|
|
1741
1753
|
*/
|
|
1742
|
-
|
|
1754
|
+
assertFrontendModelPluckDefinitionsAllowed(pluck) {
|
|
1743
1755
|
const modelClass = this.frontendModelClass()
|
|
1744
1756
|
|
|
1745
1757
|
for (const pluckEntry of pluck) {
|
|
@@ -1753,11 +1765,105 @@ export default class FrontendModelController extends Controller {
|
|
|
1753
1765
|
})
|
|
1754
1766
|
|
|
1755
1767
|
if (!columnName) {
|
|
1756
|
-
throw
|
|
1768
|
+
throw frontendModelQueryError(`Unknown pluck column "${pluckEntry.column}" for ${targetModelClass.name}`)
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* Asserts frontend-model Ransack definitions only reference exposed resource attributes.
|
|
1775
|
+
* @param {Record<string, ?>} ransack - Ransack descriptor.
|
|
1776
|
+
* @returns {void}
|
|
1777
|
+
*/
|
|
1778
|
+
assertFrontendModelRansackAllowed(ransack) {
|
|
1779
|
+
const {s, ...filterParams} = ransack
|
|
1780
|
+
|
|
1781
|
+
if (Object.keys(filterParams).length > 0) {
|
|
1782
|
+
this.assertFrontendModelRansackGroupAllowed({
|
|
1783
|
+
group: this.frontendModelRansackGroup(filterParams)
|
|
1784
|
+
})
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (typeof s === "string" && s.trim().length > 0) {
|
|
1788
|
+
for (const sort of this.frontendModelRansackSorts(s)) {
|
|
1789
|
+
this.assertFrontendModelRansackAttributeAllowed({
|
|
1790
|
+
attributeName: sort.attribute,
|
|
1791
|
+
modelClass: this.frontendModelClass(),
|
|
1792
|
+
operationName: "ransack sort"
|
|
1793
|
+
})
|
|
1757
1794
|
}
|
|
1758
1795
|
}
|
|
1759
1796
|
}
|
|
1760
1797
|
|
|
1798
|
+
/**
|
|
1799
|
+
* Runs normalized frontend-model Ransack group.
|
|
1800
|
+
* @param {Record<string, ?>} filterParams - Ransack filter params.
|
|
1801
|
+
* @returns {import("./utils/ransack.js").RansackGroup} Normalized Ransack group.
|
|
1802
|
+
*/
|
|
1803
|
+
frontendModelRansackGroup(filterParams) {
|
|
1804
|
+
try {
|
|
1805
|
+
return normalizeRansackGroup(this.frontendModelClass(), filterParams)
|
|
1806
|
+
} catch (error) {
|
|
1807
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/**
|
|
1812
|
+
* Runs normalized frontend-model Ransack sorts.
|
|
1813
|
+
* @param {string} sortString - Ransack sort string.
|
|
1814
|
+
* @returns {import("./utils/ransack.js").RansackSort[]} Normalized Ransack sorts.
|
|
1815
|
+
*/
|
|
1816
|
+
frontendModelRansackSorts(sortString) {
|
|
1817
|
+
try {
|
|
1818
|
+
return parseRansackSort(this.frontendModelClass(), sortString)
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Asserts a normalized frontend-model Ransack group only references exposed attributes.
|
|
1826
|
+
* @param {object} args - Assertion args.
|
|
1827
|
+
* @param {import("./utils/ransack.js").RansackGroup} args.group - Ransack group.
|
|
1828
|
+
* @returns {void}
|
|
1829
|
+
*/
|
|
1830
|
+
assertFrontendModelRansackGroupAllowed({group}) {
|
|
1831
|
+
for (const condition of group.conditions) {
|
|
1832
|
+
for (const attribute of condition.attributes) {
|
|
1833
|
+
const targetModelClass = this.frontendModelSearchTargetModelClass({
|
|
1834
|
+
modelClass: this.frontendModelClass(),
|
|
1835
|
+
path: attribute.path
|
|
1836
|
+
})
|
|
1837
|
+
|
|
1838
|
+
this.assertFrontendModelRansackAttributeAllowed({
|
|
1839
|
+
attributeName: attribute.attributeName,
|
|
1840
|
+
modelClass: targetModelClass,
|
|
1841
|
+
operationName: "ransack"
|
|
1842
|
+
})
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
for (const grouping of group.groupings) {
|
|
1847
|
+
this.assertFrontendModelRansackGroupAllowed({group: grouping})
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
/**
|
|
1852
|
+
* Asserts one normalized frontend-model Ransack attribute is exposed by its resource.
|
|
1853
|
+
* @param {object} args - Assertion args.
|
|
1854
|
+
* @param {string} args.attributeName - Attribute name.
|
|
1855
|
+
* @param {typeof import("./database/record/index.js").default} args.modelClass - Target model class.
|
|
1856
|
+
* @param {string} args.operationName - Operation name for errors.
|
|
1857
|
+
* @returns {void}
|
|
1858
|
+
*/
|
|
1859
|
+
assertFrontendModelRansackAttributeAllowed({attributeName, modelClass, operationName}) {
|
|
1860
|
+
const attributeNames = this.frontendModelResourceAttributeNamesForModelClass(modelClass)
|
|
1861
|
+
|
|
1862
|
+
if (attributeNames && !attributeNames.has(attributeName)) {
|
|
1863
|
+
throw frontendModelQueryError(`Unknown ${operationName} attribute "${attributeName}" for ${modelClass.name}`)
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1761
1867
|
/**
|
|
1762
1868
|
* Runs frontend model search target model class.
|
|
1763
1869
|
* @param {object} args - Search args.
|
|
@@ -1772,7 +1878,7 @@ export default class FrontendModelController extends Controller {
|
|
|
1772
1878
|
const relationship = targetModelClass.getRelationshipsMap()[relationshipName]
|
|
1773
1879
|
|
|
1774
1880
|
if (!relationship) {
|
|
1775
|
-
throw
|
|
1881
|
+
throw frontendModelQueryError(`Unknown search relationship "${relationshipName}" for ${targetModelClass.name}`)
|
|
1776
1882
|
}
|
|
1777
1883
|
|
|
1778
1884
|
const relationshipTargetModelClass = relationship.getTargetModelClass()
|
|
@@ -1807,7 +1913,7 @@ export default class FrontendModelController extends Controller {
|
|
|
1807
1913
|
})
|
|
1808
1914
|
|
|
1809
1915
|
if (!columnName) {
|
|
1810
|
-
throw
|
|
1916
|
+
throw frontendModelQueryError(`Unknown search column "${search.column}" for ${targetModelClass.name}`)
|
|
1811
1917
|
}
|
|
1812
1918
|
|
|
1813
1919
|
if (search.path.length > 0) {
|
|
@@ -1958,7 +2064,7 @@ export default class FrontendModelController extends Controller {
|
|
|
1958
2064
|
const relationship = modelClass.getRelationshipsMap()[relationshipName]
|
|
1959
2065
|
|
|
1960
2066
|
if (!relationship) {
|
|
1961
|
-
throw
|
|
2067
|
+
throw frontendModelQueryError(`Unknown join relationship "${relationshipName}" for ${modelClass.name}`)
|
|
1962
2068
|
}
|
|
1963
2069
|
|
|
1964
2070
|
const targetModelClass = relationship.getTargetModelClass()
|
|
@@ -2049,18 +2155,18 @@ export default class FrontendModelController extends Controller {
|
|
|
2049
2155
|
}
|
|
2050
2156
|
|
|
2051
2157
|
/**
|
|
2052
|
-
*
|
|
2158
|
+
* Asserts a selected frontend-model attribute list only references exposed attributes.
|
|
2053
2159
|
* @param {object} args - Args.
|
|
2054
2160
|
* @param {string[]} args.attributeNames - Selected attribute names.
|
|
2055
2161
|
* @param {typeof import("./database/record/index.js").default} args.modelClass - Model class.
|
|
2056
2162
|
* @param {"select" | "selectsExtra"} args.operationName - Selection operation.
|
|
2057
|
-
* @returns {string[]} -
|
|
2163
|
+
* @returns {string[]} - Allowed selected attribute names.
|
|
2058
2164
|
*/
|
|
2059
|
-
|
|
2165
|
+
assertFrontendModelSelectedAttributesAllowed({attributeNames, modelClass, operationName}) {
|
|
2060
2166
|
for (const attributeName of attributeNames) {
|
|
2061
2167
|
if (this.frontendModelAttributeIsExposed({attributeName, modelClass})) continue
|
|
2062
2168
|
|
|
2063
|
-
throw
|
|
2169
|
+
throw frontendModelQueryError(`Unknown ${operationName} attribute "${attributeName}" for ${modelClass.name}`)
|
|
2064
2170
|
}
|
|
2065
2171
|
|
|
2066
2172
|
return attributeNames
|
|
@@ -2163,7 +2269,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2163
2269
|
const relationship = modelClass.getRelationshipsMap()[attributeName]
|
|
2164
2270
|
|
|
2165
2271
|
if (!relationship) {
|
|
2166
|
-
throw
|
|
2272
|
+
throw frontendModelQueryError(`Unknown where relationship "${attributeName}" for ${modelClass.name}`)
|
|
2167
2273
|
}
|
|
2168
2274
|
|
|
2169
2275
|
const targetModelClass = relationship.getTargetModelClass()
|
|
@@ -2184,7 +2290,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2184
2290
|
continue
|
|
2185
2291
|
}
|
|
2186
2292
|
|
|
2187
|
-
throw
|
|
2293
|
+
throw frontendModelQueryError(`Unknown where column "${attributeName}" for ${modelClass.name}`)
|
|
2188
2294
|
}
|
|
2189
2295
|
}
|
|
2190
2296
|
|
|
@@ -2251,7 +2357,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2251
2357
|
})
|
|
2252
2358
|
|
|
2253
2359
|
if (!columnName) {
|
|
2254
|
-
throw
|
|
2360
|
+
throw frontendModelQueryError(`Unknown group column "${group.column}" for ${targetModelClass.name}`)
|
|
2255
2361
|
}
|
|
2256
2362
|
|
|
2257
2363
|
this.ensureFrontendModelJoinPath({path: group.path, query})
|
|
@@ -2369,7 +2475,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2369
2475
|
const translationPath = sort.path.concat(["currentTranslation"])
|
|
2370
2476
|
|
|
2371
2477
|
if (!translationColumnName) {
|
|
2372
|
-
throw
|
|
2478
|
+
throw frontendModelQueryError(`Unknown translated sort column "${sort.column}" for ${targetModelClass.name}`)
|
|
2373
2479
|
}
|
|
2374
2480
|
|
|
2375
2481
|
this.ensureFrontendModelSortJoinPath({path: translationPath, query})
|
|
@@ -2383,7 +2489,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2383
2489
|
}
|
|
2384
2490
|
|
|
2385
2491
|
if (!columnName) {
|
|
2386
|
-
throw
|
|
2492
|
+
throw frontendModelQueryError(`Unknown sort column "${sort.column}" for ${targetModelClass.name}`)
|
|
2387
2493
|
}
|
|
2388
2494
|
|
|
2389
2495
|
this.ensureFrontendModelSortJoinPath({path: sort.path, query})
|
|
@@ -2440,7 +2546,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2440
2546
|
|
|
2441
2547
|
if (!selectedAttributes) return null
|
|
2442
2548
|
|
|
2443
|
-
return this.
|
|
2549
|
+
return this.assertFrontendModelSelectedAttributesAllowed({
|
|
2444
2550
|
attributeNames: selectedAttributes,
|
|
2445
2551
|
modelClass,
|
|
2446
2552
|
operationName: "select"
|
|
@@ -2461,7 +2567,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2461
2567
|
|
|
2462
2568
|
if (!extraAttributes) return null
|
|
2463
2569
|
|
|
2464
|
-
return this.
|
|
2570
|
+
return this.assertFrontendModelSelectedAttributesAllowed({
|
|
2465
2571
|
attributeNames: extraAttributes,
|
|
2466
2572
|
modelClass,
|
|
2467
2573
|
operationName: "selectsExtra"
|