velocious 1.0.466 → 1.0.468
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/environment-handlers/node/cli/commands/generate/frontend-models.js +155 -35
- package/build/frontend-model-controller.js +209 -57
- 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/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +82 -9
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +137 -35
- package/build/src/frontend-model-controller.d.ts +61 -5
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +190 -58
- 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/environment-handlers/node/cli/commands/generate/frontend-models.js +155 -35
- package/src/frontend-model-controller.js +209 -57
- package/src/frontend-models/query.js +55 -33
- package/src/utils/ransack.js +44 -22
|
@@ -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
|
|
@@ -577,6 +584,12 @@ export default class FrontendModelController extends Controller {
|
|
|
577
584
|
* Frontend model ability override.
|
|
578
585
|
* @type {import("./authorization/ability.js").default | undefined} */
|
|
579
586
|
_frontendModelAbilityOverride = undefined
|
|
587
|
+
/**
|
|
588
|
+
* Original deserialized custom-command client payload, captured before route
|
|
589
|
+
* framework params are merged in, so a typed command method receives the client's
|
|
590
|
+
* own arguments rather than the route metadata. Only set on the shared-endpoint path.
|
|
591
|
+
* @type {Record<string, ?> | undefined} */
|
|
592
|
+
_frontendModelCustomCommandClientArguments = undefined
|
|
580
593
|
|
|
581
594
|
/**
|
|
582
595
|
* Runs frontend model params.
|
|
@@ -1194,7 +1207,11 @@ export default class FrontendModelController extends Controller {
|
|
|
1194
1207
|
* @returns {FrontendModelSort[]} - Frontend sort definitions.
|
|
1195
1208
|
*/
|
|
1196
1209
|
frontendModelSort() {
|
|
1197
|
-
|
|
1210
|
+
try {
|
|
1211
|
+
return normalizeQuerySort(this.frontendModelParams().sort)
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
1214
|
+
}
|
|
1198
1215
|
}
|
|
1199
1216
|
|
|
1200
1217
|
/**
|
|
@@ -1205,7 +1222,7 @@ export default class FrontendModelController extends Controller {
|
|
|
1205
1222
|
try {
|
|
1206
1223
|
return normalizeQueryGroup(this.frontendModelParams().group)
|
|
1207
1224
|
} catch (error) {
|
|
1208
|
-
|
|
1225
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
1209
1226
|
}
|
|
1210
1227
|
}
|
|
1211
1228
|
|
|
@@ -1240,11 +1257,11 @@ export default class FrontendModelController extends Controller {
|
|
|
1240
1257
|
try {
|
|
1241
1258
|
const pluck = normalizeQueryPluck(this.frontendModelParams().pluck)
|
|
1242
1259
|
|
|
1243
|
-
this.
|
|
1260
|
+
this.assertFrontendModelPluckDefinitionsAllowed(pluck)
|
|
1244
1261
|
|
|
1245
1262
|
return pluck
|
|
1246
1263
|
} catch (error) {
|
|
1247
|
-
|
|
1264
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
1248
1265
|
}
|
|
1249
1266
|
}
|
|
1250
1267
|
|
|
@@ -1517,6 +1534,7 @@ export default class FrontendModelController extends Controller {
|
|
|
1517
1534
|
const ransack = this.frontendModelRansack()
|
|
1518
1535
|
|
|
1519
1536
|
if (ransack) {
|
|
1537
|
+
this.assertFrontendModelRansackAllowed(ransack)
|
|
1520
1538
|
query.ransack(ransack)
|
|
1521
1539
|
}
|
|
1522
1540
|
|
|
@@ -1639,7 +1657,7 @@ export default class FrontendModelController extends Controller {
|
|
|
1639
1657
|
})
|
|
1640
1658
|
|
|
1641
1659
|
if (!columnName) {
|
|
1642
|
-
throw
|
|
1660
|
+
throw frontendModelQueryError(`Unknown pluck column "${pluckEntry.column}" for ${targetModelClass.name}`)
|
|
1643
1661
|
}
|
|
1644
1662
|
|
|
1645
1663
|
if (pluckEntry.path.length > 0) {
|
|
@@ -1735,11 +1753,11 @@ export default class FrontendModelController extends Controller {
|
|
|
1735
1753
|
}
|
|
1736
1754
|
|
|
1737
1755
|
/**
|
|
1738
|
-
*
|
|
1756
|
+
* Asserts frontend-model pluck definitions only reference exposed resource attributes.
|
|
1739
1757
|
* @param {FrontendModelPluck[]} pluck - Pluck descriptors.
|
|
1740
1758
|
* @returns {void}
|
|
1741
1759
|
*/
|
|
1742
|
-
|
|
1760
|
+
assertFrontendModelPluckDefinitionsAllowed(pluck) {
|
|
1743
1761
|
const modelClass = this.frontendModelClass()
|
|
1744
1762
|
|
|
1745
1763
|
for (const pluckEntry of pluck) {
|
|
@@ -1753,9 +1771,103 @@ export default class FrontendModelController extends Controller {
|
|
|
1753
1771
|
})
|
|
1754
1772
|
|
|
1755
1773
|
if (!columnName) {
|
|
1756
|
-
throw
|
|
1774
|
+
throw frontendModelQueryError(`Unknown pluck column "${pluckEntry.column}" for ${targetModelClass.name}`)
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* Asserts frontend-model Ransack definitions only reference exposed resource attributes.
|
|
1781
|
+
* @param {Record<string, ?>} ransack - Ransack descriptor.
|
|
1782
|
+
* @returns {void}
|
|
1783
|
+
*/
|
|
1784
|
+
assertFrontendModelRansackAllowed(ransack) {
|
|
1785
|
+
const {s, ...filterParams} = ransack
|
|
1786
|
+
|
|
1787
|
+
if (Object.keys(filterParams).length > 0) {
|
|
1788
|
+
this.assertFrontendModelRansackGroupAllowed({
|
|
1789
|
+
group: this.frontendModelRansackGroup(filterParams)
|
|
1790
|
+
})
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
if (typeof s === "string" && s.trim().length > 0) {
|
|
1794
|
+
for (const sort of this.frontendModelRansackSorts(s)) {
|
|
1795
|
+
this.assertFrontendModelRansackAttributeAllowed({
|
|
1796
|
+
attributeName: sort.attribute,
|
|
1797
|
+
modelClass: this.frontendModelClass(),
|
|
1798
|
+
operationName: "ransack sort"
|
|
1799
|
+
})
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Runs normalized frontend-model Ransack group.
|
|
1806
|
+
* @param {Record<string, ?>} filterParams - Ransack filter params.
|
|
1807
|
+
* @returns {import("./utils/ransack.js").RansackGroup} Normalized Ransack group.
|
|
1808
|
+
*/
|
|
1809
|
+
frontendModelRansackGroup(filterParams) {
|
|
1810
|
+
try {
|
|
1811
|
+
return normalizeRansackGroup(this.frontendModelClass(), filterParams)
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Runs normalized frontend-model Ransack sorts.
|
|
1819
|
+
* @param {string} sortString - Ransack sort string.
|
|
1820
|
+
* @returns {import("./utils/ransack.js").RansackSort[]} Normalized Ransack sorts.
|
|
1821
|
+
*/
|
|
1822
|
+
frontendModelRansackSorts(sortString) {
|
|
1823
|
+
try {
|
|
1824
|
+
return parseRansackSort(this.frontendModelClass(), sortString)
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
/**
|
|
1831
|
+
* Asserts a normalized frontend-model Ransack group only references exposed attributes.
|
|
1832
|
+
* @param {object} args - Assertion args.
|
|
1833
|
+
* @param {import("./utils/ransack.js").RansackGroup} args.group - Ransack group.
|
|
1834
|
+
* @returns {void}
|
|
1835
|
+
*/
|
|
1836
|
+
assertFrontendModelRansackGroupAllowed({group}) {
|
|
1837
|
+
for (const condition of group.conditions) {
|
|
1838
|
+
for (const attribute of condition.attributes) {
|
|
1839
|
+
const targetModelClass = this.frontendModelSearchTargetModelClass({
|
|
1840
|
+
modelClass: this.frontendModelClass(),
|
|
1841
|
+
path: attribute.path
|
|
1842
|
+
})
|
|
1843
|
+
|
|
1844
|
+
this.assertFrontendModelRansackAttributeAllowed({
|
|
1845
|
+
attributeName: attribute.attributeName,
|
|
1846
|
+
modelClass: targetModelClass,
|
|
1847
|
+
operationName: "ransack"
|
|
1848
|
+
})
|
|
1757
1849
|
}
|
|
1758
1850
|
}
|
|
1851
|
+
|
|
1852
|
+
for (const grouping of group.groupings) {
|
|
1853
|
+
this.assertFrontendModelRansackGroupAllowed({group: grouping})
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* Asserts one normalized frontend-model Ransack attribute is exposed by its resource.
|
|
1859
|
+
* @param {object} args - Assertion args.
|
|
1860
|
+
* @param {string} args.attributeName - Attribute name.
|
|
1861
|
+
* @param {typeof import("./database/record/index.js").default} args.modelClass - Target model class.
|
|
1862
|
+
* @param {string} args.operationName - Operation name for errors.
|
|
1863
|
+
* @returns {void}
|
|
1864
|
+
*/
|
|
1865
|
+
assertFrontendModelRansackAttributeAllowed({attributeName, modelClass, operationName}) {
|
|
1866
|
+
const attributeNames = this.frontendModelResourceAttributeNamesForModelClass(modelClass)
|
|
1867
|
+
|
|
1868
|
+
if (attributeNames && !attributeNames.has(attributeName)) {
|
|
1869
|
+
throw frontendModelQueryError(`Unknown ${operationName} attribute "${attributeName}" for ${modelClass.name}`)
|
|
1870
|
+
}
|
|
1759
1871
|
}
|
|
1760
1872
|
|
|
1761
1873
|
/**
|
|
@@ -1772,7 +1884,7 @@ export default class FrontendModelController extends Controller {
|
|
|
1772
1884
|
const relationship = targetModelClass.getRelationshipsMap()[relationshipName]
|
|
1773
1885
|
|
|
1774
1886
|
if (!relationship) {
|
|
1775
|
-
throw
|
|
1887
|
+
throw frontendModelQueryError(`Unknown search relationship "${relationshipName}" for ${targetModelClass.name}`)
|
|
1776
1888
|
}
|
|
1777
1889
|
|
|
1778
1890
|
const relationshipTargetModelClass = relationship.getTargetModelClass()
|
|
@@ -1807,7 +1919,7 @@ export default class FrontendModelController extends Controller {
|
|
|
1807
1919
|
})
|
|
1808
1920
|
|
|
1809
1921
|
if (!columnName) {
|
|
1810
|
-
throw
|
|
1922
|
+
throw frontendModelQueryError(`Unknown search column "${search.column}" for ${targetModelClass.name}`)
|
|
1811
1923
|
}
|
|
1812
1924
|
|
|
1813
1925
|
if (search.path.length > 0) {
|
|
@@ -1958,7 +2070,7 @@ export default class FrontendModelController extends Controller {
|
|
|
1958
2070
|
const relationship = modelClass.getRelationshipsMap()[relationshipName]
|
|
1959
2071
|
|
|
1960
2072
|
if (!relationship) {
|
|
1961
|
-
throw
|
|
2073
|
+
throw frontendModelQueryError(`Unknown join relationship "${relationshipName}" for ${modelClass.name}`)
|
|
1962
2074
|
}
|
|
1963
2075
|
|
|
1964
2076
|
const targetModelClass = relationship.getTargetModelClass()
|
|
@@ -2049,18 +2161,18 @@ export default class FrontendModelController extends Controller {
|
|
|
2049
2161
|
}
|
|
2050
2162
|
|
|
2051
2163
|
/**
|
|
2052
|
-
*
|
|
2164
|
+
* Asserts a selected frontend-model attribute list only references exposed attributes.
|
|
2053
2165
|
* @param {object} args - Args.
|
|
2054
2166
|
* @param {string[]} args.attributeNames - Selected attribute names.
|
|
2055
2167
|
* @param {typeof import("./database/record/index.js").default} args.modelClass - Model class.
|
|
2056
2168
|
* @param {"select" | "selectsExtra"} args.operationName - Selection operation.
|
|
2057
|
-
* @returns {string[]} -
|
|
2169
|
+
* @returns {string[]} - Allowed selected attribute names.
|
|
2058
2170
|
*/
|
|
2059
|
-
|
|
2171
|
+
assertFrontendModelSelectedAttributesAllowed({attributeNames, modelClass, operationName}) {
|
|
2060
2172
|
for (const attributeName of attributeNames) {
|
|
2061
2173
|
if (this.frontendModelAttributeIsExposed({attributeName, modelClass})) continue
|
|
2062
2174
|
|
|
2063
|
-
throw
|
|
2175
|
+
throw frontendModelQueryError(`Unknown ${operationName} attribute "${attributeName}" for ${modelClass.name}`)
|
|
2064
2176
|
}
|
|
2065
2177
|
|
|
2066
2178
|
return attributeNames
|
|
@@ -2163,7 +2275,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2163
2275
|
const relationship = modelClass.getRelationshipsMap()[attributeName]
|
|
2164
2276
|
|
|
2165
2277
|
if (!relationship) {
|
|
2166
|
-
throw
|
|
2278
|
+
throw frontendModelQueryError(`Unknown where relationship "${attributeName}" for ${modelClass.name}`)
|
|
2167
2279
|
}
|
|
2168
2280
|
|
|
2169
2281
|
const targetModelClass = relationship.getTargetModelClass()
|
|
@@ -2184,7 +2296,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2184
2296
|
continue
|
|
2185
2297
|
}
|
|
2186
2298
|
|
|
2187
|
-
throw
|
|
2299
|
+
throw frontendModelQueryError(`Unknown where column "${attributeName}" for ${modelClass.name}`)
|
|
2188
2300
|
}
|
|
2189
2301
|
}
|
|
2190
2302
|
|
|
@@ -2251,7 +2363,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2251
2363
|
})
|
|
2252
2364
|
|
|
2253
2365
|
if (!columnName) {
|
|
2254
|
-
throw
|
|
2366
|
+
throw frontendModelQueryError(`Unknown group column "${group.column}" for ${targetModelClass.name}`)
|
|
2255
2367
|
}
|
|
2256
2368
|
|
|
2257
2369
|
this.ensureFrontendModelJoinPath({path: group.path, query})
|
|
@@ -2369,7 +2481,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2369
2481
|
const translationPath = sort.path.concat(["currentTranslation"])
|
|
2370
2482
|
|
|
2371
2483
|
if (!translationColumnName) {
|
|
2372
|
-
throw
|
|
2484
|
+
throw frontendModelQueryError(`Unknown translated sort column "${sort.column}" for ${targetModelClass.name}`)
|
|
2373
2485
|
}
|
|
2374
2486
|
|
|
2375
2487
|
this.ensureFrontendModelSortJoinPath({path: translationPath, query})
|
|
@@ -2383,7 +2495,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2383
2495
|
}
|
|
2384
2496
|
|
|
2385
2497
|
if (!columnName) {
|
|
2386
|
-
throw
|
|
2498
|
+
throw frontendModelQueryError(`Unknown sort column "${sort.column}" for ${targetModelClass.name}`)
|
|
2387
2499
|
}
|
|
2388
2500
|
|
|
2389
2501
|
this.ensureFrontendModelSortJoinPath({path: sort.path, query})
|
|
@@ -2440,7 +2552,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2440
2552
|
|
|
2441
2553
|
if (!selectedAttributes) return null
|
|
2442
2554
|
|
|
2443
|
-
return this.
|
|
2555
|
+
return this.assertFrontendModelSelectedAttributesAllowed({
|
|
2444
2556
|
attributeNames: selectedAttributes,
|
|
2445
2557
|
modelClass,
|
|
2446
2558
|
operationName: "select"
|
|
@@ -2461,7 +2573,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2461
2573
|
|
|
2462
2574
|
if (!extraAttributes) return null
|
|
2463
2575
|
|
|
2464
|
-
return this.
|
|
2576
|
+
return this.assertFrontendModelSelectedAttributesAllowed({
|
|
2465
2577
|
attributeNames: extraAttributes,
|
|
2466
2578
|
modelClass,
|
|
2467
2579
|
operationName: "selectsExtra"
|
|
@@ -3472,6 +3584,14 @@ export default class FrontendModelController extends Controller {
|
|
|
3472
3584
|
viewPath
|
|
3473
3585
|
})
|
|
3474
3586
|
|
|
3587
|
+
// Preserve the client's own command arguments before route framework params won
|
|
3588
|
+
// the `controllerParams` merge above, so a typed command method (`async name(args)`)
|
|
3589
|
+
// receives the client payload — not the route's member id / model / controller keys.
|
|
3590
|
+
const customCommandController = /** @type {FrontendModelController} */ (/** @type {unknown} */ (controllerInstance))
|
|
3591
|
+
|
|
3592
|
+
customCommandController._frontendModelCustomCommandClientArguments =
|
|
3593
|
+
(payload && typeof payload === "object" && !Array.isArray(payload)) ? /** @type {Record<string, ?>} */ (payload) : {}
|
|
3594
|
+
|
|
3475
3595
|
await this.withFrontendModelRequestContext(controllerParams, response, async () => {
|
|
3476
3596
|
await controllerInstance._runBeforeCallbacks()
|
|
3477
3597
|
const controllerMethods = /** @type {Record<string, () => Promise<void> | void>} */ (/** @type {?} */ (controllerInstance))
|
|
@@ -3651,7 +3771,13 @@ export default class FrontendModelController extends Controller {
|
|
|
3651
3771
|
return this.frontendModelErrorPayload(`Missing frontend-model custom command '${methodName}'.`)
|
|
3652
3772
|
}
|
|
3653
3773
|
|
|
3654
|
-
|
|
3774
|
+
// Pass the client command arguments as the method's first argument so a command
|
|
3775
|
+
// method can take a typed args object (`async name(args)`) and the generated
|
|
3776
|
+
// frontend method can forward the backend method's `@param`. `this.params()` is
|
|
3777
|
+
// unchanged, so existing parameterless methods keep working. The args are untrusted
|
|
3778
|
+
// client input typed only by the declared contract, so methods must still validate.
|
|
3779
|
+
const commandArguments = this.frontendModelCustomCommandArguments(params)
|
|
3780
|
+
const responsePayload = await commandMethod.call(resource, commandArguments)
|
|
3655
3781
|
|
|
3656
3782
|
if (!responsePayload || typeof responsePayload !== "object") {
|
|
3657
3783
|
return {status: "success"}
|
|
@@ -3666,6 +3792,32 @@ export default class FrontendModelController extends Controller {
|
|
|
3666
3792
|
)
|
|
3667
3793
|
}
|
|
3668
3794
|
|
|
3795
|
+
/**
|
|
3796
|
+
* Resolves the typed argument object passed to a custom command method. On the
|
|
3797
|
+
* shared-endpoint path the original client payload was captured before route
|
|
3798
|
+
* framework params were merged, so it is returned verbatim (a client `id` survives
|
|
3799
|
+
* a member route). On the direct path it falls back to the request params with the
|
|
3800
|
+
* framework keys the command route hook injected stripped out.
|
|
3801
|
+
* @param {Record<string, ?>} params - Deserialized frontend-model params.
|
|
3802
|
+
* @returns {Record<string, ?>} - Client command arguments.
|
|
3803
|
+
*/
|
|
3804
|
+
frontendModelCustomCommandArguments(params) {
|
|
3805
|
+
if (this._frontendModelCustomCommandClientArguments) {
|
|
3806
|
+
return this._frontendModelCustomCommandClientArguments
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
const {
|
|
3810
|
+
action: _action,
|
|
3811
|
+
controller: _controller,
|
|
3812
|
+
frontendModelCustomCommandMethodName: _methodName,
|
|
3813
|
+
frontendModelCustomCommandScope: _scope,
|
|
3814
|
+
model: _model,
|
|
3815
|
+
...commandArguments
|
|
3816
|
+
} = params
|
|
3817
|
+
|
|
3818
|
+
return commandArguments
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3669
3821
|
/**
|
|
3670
3822
|
* Walks a custom-command response payload and replaces any backend `Record`
|
|
3671
3823
|
* instance with the resource's per-action serialized form so handlers can
|