velocious 1.0.451 → 1.0.453
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 +11 -5
- package/build/configuration-types.js +23 -0
- package/build/configuration.js +5 -5
- package/build/database/drivers/mssql/index.js +1 -1
- package/build/database/drivers/mysql/index.js +1 -1
- package/build/database/drivers/pgsql/index.js +1 -1
- package/build/database/drivers/sqlite/base.js +1 -1
- package/build/database/record/attachments/attachment-record.js +121 -0
- package/build/database/record/attachments/store.js +2 -0
- package/build/database/table-data/index.js +1 -1
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +7 -2
- package/build/frontend-model-controller.js +302 -113
- package/build/frontend-model-resource/base-resource.js +2 -1
- package/build/frontend-model-resource/velocious-attachment-resource.js +221 -0
- package/build/frontend-models/base.js +127 -1
- package/build/frontend-models/built-in-resources.js +32 -0
- package/build/frontend-models/websocket-publishers.js +13 -3
- package/build/routes/hooks/frontend-model-command-route-hook.js +3 -2
- package/build/src/configuration-types.d.ts +56 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +21 -1
- package/build/src/configuration.d.ts +9 -17
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +6 -6
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +2 -2
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +2 -2
- package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/index.js +2 -2
- package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/base.js +2 -2
- package/build/src/database/record/attachments/attachment-record.d.ts +67 -0
- package/build/src/database/record/attachments/attachment-record.d.ts.map +1 -0
- package/build/src/database/record/attachments/attachment-record.js +106 -0
- package/build/src/database/record/attachments/store.d.ts.map +1 -1
- package/build/src/database/record/attachments/store.js +2 -1
- package/build/src/database/table-data/index.js +2 -2
- 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 +7 -3
- package/build/src/frontend-model-controller.d.ts +82 -9
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +270 -112
- package/build/src/frontend-model-resource/base-resource.d.ts +2 -0
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +3 -2
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +105 -0
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -0
- package/build/src/frontend-model-resource/velocious-attachment-resource.js +185 -0
- package/build/src/frontend-models/base.d.ts +87 -1
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +112 -2
- package/build/src/frontend-models/built-in-resources.d.ts +18 -0
- package/build/src/frontend-models/built-in-resources.d.ts.map +1 -0
- package/build/src/frontend-models/built-in-resources.js +29 -0
- package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-publishers.js +14 -4
- package/build/src/routes/hooks/frontend-model-command-route-hook.d.ts.map +1 -1
- package/build/src/routes/hooks/frontend-model-command-route-hook.js +4 -3
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/configuration-types.js +23 -0
- package/src/configuration.js +5 -5
- package/src/database/drivers/mssql/index.js +1 -1
- package/src/database/drivers/mysql/index.js +1 -1
- package/src/database/drivers/pgsql/index.js +1 -1
- package/src/database/drivers/sqlite/base.js +1 -1
- package/src/database/record/attachments/attachment-record.js +121 -0
- package/src/database/record/attachments/store.js +2 -0
- package/src/database/table-data/index.js +1 -1
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +7 -2
- package/src/frontend-model-controller.js +302 -113
- package/src/frontend-model-resource/base-resource.js +2 -1
- package/src/frontend-model-resource/velocious-attachment-resource.js +221 -0
- package/src/frontend-models/base.js +127 -1
- package/src/frontend-models/built-in-resources.js +32 -0
- package/src/frontend-models/websocket-publishers.js +13 -3
- package/src/routes/hooks/frontend-model-command-route-hook.js +3 -2
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import * as inflection from "inflection"
|
|
4
4
|
import Controller from "./controller.js"
|
|
5
|
+
import FrontendModelBaseResource from "./frontend-model-resource/base-resource.js"
|
|
5
6
|
import Response from "./http-server/client/response.js"
|
|
6
|
-
import {
|
|
7
|
+
import {frontendModelResourcesWithBuiltInsForBackendProject} from "./frontend-models/built-in-resources.js"
|
|
8
|
+
import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcePath} from "./frontend-models/resource-definition.js"
|
|
7
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"
|
|
8
10
|
import {assignSafeProperty, deserializeFrontendModelTransportValue, isBackendModelInstance, serializeFrontendModelTransportValue} from "./frontend-models/transport-serialization.js"
|
|
9
11
|
import RoutesResolver from "./routes/resolver.js"
|
|
@@ -139,6 +141,14 @@ function normalizeFrontendModelSelect(select, rootModelName = null) {
|
|
|
139
141
|
* @property {number | null} perPage - Page size.
|
|
140
142
|
*/
|
|
141
143
|
|
|
144
|
+
/**
|
|
145
|
+
* @typedef {import("./configuration-types.js").ClientErrorPayloadContext & {
|
|
146
|
+
* action: string,
|
|
147
|
+
* expectedError: boolean,
|
|
148
|
+
* frontendModelEndpoint: true
|
|
149
|
+
* }} FrontendModelEndpointErrorContext
|
|
150
|
+
*/
|
|
151
|
+
|
|
142
152
|
const frontendModelJoinedPathsSymbol = Symbol("frontendModelJoinedPaths")
|
|
143
153
|
const frontendModelGroupedColumnsSymbol = Symbol("frontendModelGroupedColumns")
|
|
144
154
|
const frontendModelWhereNoMatchSymbol = Symbol("frontendModelWhereNoMatch")
|
|
@@ -184,32 +194,69 @@ function frontendModelValidationErrorForError(error) {
|
|
|
184
194
|
* developer for the frontend" — the framework treats it as
|
|
185
195
|
* user-facing: surface the message, forward the metadata, and skip
|
|
186
196
|
* the noisy endpoint-error log.
|
|
187
|
-
* @param {
|
|
188
|
-
* @returns {boolean}
|
|
197
|
+
* @param {unknown} error - Caught error.
|
|
198
|
+
* @returns {boolean} Whether the error has Velocious frontend metadata.
|
|
189
199
|
*/
|
|
190
200
|
function frontendModelErrorHasVelociousMetadata(error) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
if (!error || typeof error !== "object") return false
|
|
202
|
+
|
|
203
|
+
// Runtime checks above narrow this caught value to the metadata record shape.
|
|
204
|
+
const errorRecord = /** @type {{velocious?: import("./configuration-types.js").ClientErrorPayloadReporterPayload}} */ (error)
|
|
205
|
+
|
|
206
|
+
return isPlainObject(errorRecord.velocious)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Whether the error has a frontend-model error type marker.
|
|
211
|
+
* @param {unknown} error - Caught error.
|
|
212
|
+
* @returns {boolean} Whether the error has an error type.
|
|
213
|
+
*/
|
|
214
|
+
function frontendModelErrorHasErrorType(error) {
|
|
215
|
+
if (!error || typeof error !== "object") return false
|
|
216
|
+
|
|
217
|
+
// Runtime checks above narrow this caught value to the marker record shape.
|
|
218
|
+
const errorRecord = /** @type {{errorType?: string}} */ (error)
|
|
219
|
+
|
|
220
|
+
return typeof errorRecord.errorType === "string" && errorRecord.errorType.length > 0
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Whether the error is an expected frontend-model user-flow failure.
|
|
225
|
+
* @param {unknown} error - Caught error.
|
|
226
|
+
* @returns {boolean} Whether the error is expected.
|
|
227
|
+
*/
|
|
228
|
+
function frontendModelExpectedError(error) {
|
|
229
|
+
if (error instanceof ValidationError) return true
|
|
230
|
+
if (error instanceof VelociousError && error.safeToExpose) return true
|
|
231
|
+
if (frontendModelErrorHasVelociousMetadata(error)) return true
|
|
232
|
+
|
|
233
|
+
return frontendModelErrorHasErrorType(error)
|
|
196
234
|
}
|
|
197
235
|
|
|
198
236
|
/**
|
|
199
237
|
* Runs frontend model velocious metadata for error.
|
|
200
|
-
* @param {
|
|
201
|
-
* @returns {
|
|
238
|
+
* @param {unknown} error - Caught error.
|
|
239
|
+
* @returns {import("./configuration-types.js").ClientErrorPayloadReporterPayload | null} Frontend-model Velocious metadata when present.
|
|
202
240
|
*/
|
|
203
241
|
function frontendModelVelociousMetadataForError(error) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
242
|
+
const errorCode = error instanceof VelociousError && error.safeToExpose && typeof error.code === "string" && error.code.length > 0
|
|
243
|
+
? error.code
|
|
244
|
+
: null
|
|
245
|
+
|
|
246
|
+
if (!frontendModelErrorHasVelociousMetadata(error)) {
|
|
247
|
+
return errorCode ? {code: errorCode} : null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// frontendModelErrorHasVelociousMetadata guards the caught value before this cast.
|
|
251
|
+
const errorRecord = /** @type {{velocious: import("./configuration-types.js").ClientErrorPayloadReporterPayload}} */ (error)
|
|
252
|
+
const metadata = errorRecord.velocious
|
|
253
|
+
|
|
254
|
+
return errorCode ? {...metadata, code: errorCode} : metadata
|
|
208
255
|
}
|
|
209
256
|
|
|
210
257
|
/**
|
|
211
258
|
* Runs frontend model client message for error.
|
|
212
|
-
* @param {
|
|
259
|
+
* @param {unknown} error - Caught error.
|
|
213
260
|
* @returns {string} - Message safe to return to API clients.
|
|
214
261
|
*/
|
|
215
262
|
function frontendModelClientMessageForError(error) {
|
|
@@ -237,8 +284,8 @@ function frontendModelClientMessageForError(error) {
|
|
|
237
284
|
* @param {object} args - Arguments.
|
|
238
285
|
* @param {import("./configuration.js").default} args.configuration - Current configuration.
|
|
239
286
|
* @param {string} args.environment - Current environment.
|
|
240
|
-
* @param {
|
|
241
|
-
* @returns {
|
|
287
|
+
* @param {unknown} args.error - Caught error.
|
|
288
|
+
* @returns {import("./configuration-types.js").ClientErrorPayloadReporterPayload} - Optional debug payload for non-production environments.
|
|
242
289
|
*/
|
|
243
290
|
function frontendModelDebugPayloadForError({configuration, environment, error}) {
|
|
244
291
|
const debugAllowed = frontendModelDebugErrorEnvironments.has(environment) || environment !== "production" && configuration.getExposeInternalErrorsToClients()
|
|
@@ -634,7 +681,7 @@ export default class FrontendModelController extends Controller {
|
|
|
634
681
|
const backendProjects = this.getConfiguration().getBackendProjects()
|
|
635
682
|
|
|
636
683
|
for (const backendProject of backendProjects) {
|
|
637
|
-
const resources =
|
|
684
|
+
const resources = frontendModelResourcesWithBuiltInsForBackendProject(backendProject)
|
|
638
685
|
|
|
639
686
|
if (modelName && modelName.length > 0 && resources[modelName]) {
|
|
640
687
|
const resourceDefinition = resources[modelName]
|
|
@@ -688,7 +735,7 @@ export default class FrontendModelController extends Controller {
|
|
|
688
735
|
* @returns {{backendProject: import("./configuration-types.js").BackendProjectConfiguration, modelName: string, resourceClass: import("./configuration-types.js").FrontendModelResourceClassType, resourceConfiguration: import("./configuration-types.js").NormalizedFrontendModelResourceConfiguration} | null} - Frontend model resource configuration for model name.
|
|
689
736
|
*/
|
|
690
737
|
frontendModelResourceConfigurationForBackendProjectModelName({backendProject, modelName}) {
|
|
691
|
-
const resources =
|
|
738
|
+
const resources = frontendModelResourcesWithBuiltInsForBackendProject(backendProject)
|
|
692
739
|
const resourceDefinition = resources[modelName]
|
|
693
740
|
|
|
694
741
|
if (!resourceDefinition) return null
|
|
@@ -982,16 +1029,31 @@ export default class FrontendModelController extends Controller {
|
|
|
982
1029
|
}
|
|
983
1030
|
|
|
984
1031
|
/**
|
|
985
|
-
* Runs frontend model authorized query.
|
|
1032
|
+
* Runs frontend model ability authorized query.
|
|
986
1033
|
* @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
|
|
987
1034
|
* @returns {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} - Authorized query for the action.
|
|
988
1035
|
*/
|
|
989
|
-
|
|
1036
|
+
frontendModelAbilityAuthorizedQuery(action) {
|
|
990
1037
|
const abilityAction = this.frontendModelAbilityAction(action)
|
|
991
1038
|
|
|
992
1039
|
return this.frontendModelClass().accessibleFor(abilityAction, this.currentAbility())
|
|
993
1040
|
}
|
|
994
1041
|
|
|
1042
|
+
/**
|
|
1043
|
+
* Runs frontend model authorized query.
|
|
1044
|
+
* @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
|
|
1045
|
+
* @returns {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} - Authorized query for the action.
|
|
1046
|
+
*/
|
|
1047
|
+
frontendModelAuthorizedQuery(action) {
|
|
1048
|
+
const resource = this.frontendModelResourceInstance()
|
|
1049
|
+
|
|
1050
|
+
if (resource.authorizedQuery !== FrontendModelBaseResource.prototype.authorizedQuery) {
|
|
1051
|
+
return resource.authorizedQuery(action)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
return this.frontendModelAbilityAuthorizedQuery(action)
|
|
1055
|
+
}
|
|
1056
|
+
|
|
995
1057
|
/**
|
|
996
1058
|
* Runs frontend model primary key value.
|
|
997
1059
|
* @param {import("./database/record/index.js").default} model - Model instance.
|
|
@@ -1593,7 +1655,11 @@ export default class FrontendModelController extends Controller {
|
|
|
1593
1655
|
modelClass,
|
|
1594
1656
|
path: pluckEntry.path
|
|
1595
1657
|
})
|
|
1596
|
-
const columnName = this.
|
|
1658
|
+
const columnName = this.resolveFrontendModelQueryableColumnName({
|
|
1659
|
+
attributeName: pluckEntry.column,
|
|
1660
|
+
modelClass: targetModelClass,
|
|
1661
|
+
operationName: "pluck"
|
|
1662
|
+
})
|
|
1597
1663
|
|
|
1598
1664
|
if (!columnName) {
|
|
1599
1665
|
throw new Error(`Unknown pluck column "${pluckEntry.column}" for ${targetModelClass.name}`)
|
|
@@ -1672,7 +1738,11 @@ export default class FrontendModelController extends Controller {
|
|
|
1672
1738
|
modelClass,
|
|
1673
1739
|
path: search.path
|
|
1674
1740
|
})
|
|
1675
|
-
const columnName = this.
|
|
1741
|
+
const columnName = this.resolveFrontendModelQueryableColumnName({
|
|
1742
|
+
attributeName: search.column,
|
|
1743
|
+
modelClass: targetModelClass,
|
|
1744
|
+
operationName: "search"
|
|
1745
|
+
})
|
|
1676
1746
|
|
|
1677
1747
|
if (!columnName) {
|
|
1678
1748
|
throw new Error(`Unknown search column "${search.column}" for ${targetModelClass.name}`)
|
|
@@ -1850,6 +1920,110 @@ export default class FrontendModelController extends Controller {
|
|
|
1850
1920
|
}
|
|
1851
1921
|
}
|
|
1852
1922
|
|
|
1923
|
+
/**
|
|
1924
|
+
* Runs frontend model exposed attribute names for model class.
|
|
1925
|
+
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
1926
|
+
* @returns {Set<string> | null} - Exposed attribute names, or null when no resource metadata is available.
|
|
1927
|
+
*/
|
|
1928
|
+
frontendModelExposedAttributeNamesForModelClass(modelClass) {
|
|
1929
|
+
const frontendModelResource = this.frontendModelResourceConfigurationForModelClass(modelClass)
|
|
1930
|
+
const attributes = frontendModelResource?.resourceConfiguration.attributes
|
|
1931
|
+
|
|
1932
|
+
if (!attributes) return null
|
|
1933
|
+
|
|
1934
|
+
if (Array.isArray(attributes)) {
|
|
1935
|
+
const attributeNames = attributes
|
|
1936
|
+
.map((entry) => {
|
|
1937
|
+
if (typeof entry === "string") return entry
|
|
1938
|
+
if (!entry || typeof entry !== "object") return null
|
|
1939
|
+
|
|
1940
|
+
const name = /**
|
|
1941
|
+
* Types the following value.
|
|
1942
|
+
* @type {Record<string, ?>} */ (entry).name
|
|
1943
|
+
|
|
1944
|
+
return typeof name === "string" && name.length > 0 ? name : null
|
|
1945
|
+
})
|
|
1946
|
+
.filter((entry) => typeof entry === "string")
|
|
1947
|
+
|
|
1948
|
+
return new Set(attributeNames)
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
if (typeof attributes === "object") {
|
|
1952
|
+
return new Set(Object.keys(attributes))
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
return null
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
/**
|
|
1959
|
+
* Resolves a frontend-supplied key to its canonical model attribute name.
|
|
1960
|
+
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
1961
|
+
* @param {string} key - Frontend key or raw column key.
|
|
1962
|
+
* @returns {string | null} - Canonical attribute name.
|
|
1963
|
+
*/
|
|
1964
|
+
frontendModelAttributeNameForKey(modelClass, key) {
|
|
1965
|
+
const resolvedAttributeName = modelClass.resolveAttributeName(key)
|
|
1966
|
+
|
|
1967
|
+
if (resolvedAttributeName) return resolvedAttributeName
|
|
1968
|
+
|
|
1969
|
+
const columnAttributeName = modelClass.getColumnNameToAttributeNameMap()[key]
|
|
1970
|
+
|
|
1971
|
+
return columnAttributeName || null
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
/**
|
|
1975
|
+
* Checks if a frontend-supplied attribute is exposed by the resource.
|
|
1976
|
+
* @param {object} args - Args.
|
|
1977
|
+
* @param {string} args.attributeName - Requested attribute name.
|
|
1978
|
+
* @param {typeof import("./database/record/index.js").default} args.modelClass - Model class.
|
|
1979
|
+
* @returns {boolean} - Whether the resource permits the attribute.
|
|
1980
|
+
*/
|
|
1981
|
+
frontendModelAttributeIsExposed({attributeName, modelClass}) {
|
|
1982
|
+
const exposedAttributeNames = this.frontendModelExposedAttributeNamesForModelClass(modelClass)
|
|
1983
|
+
|
|
1984
|
+
if (!exposedAttributeNames) return true
|
|
1985
|
+
|
|
1986
|
+
return exposedAttributeNames.has(attributeName)
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* Validates a selected frontend-model attribute list against resource metadata.
|
|
1991
|
+
* @param {object} args - Args.
|
|
1992
|
+
* @param {string[]} args.attributeNames - Selected attribute names.
|
|
1993
|
+
* @param {typeof import("./database/record/index.js").default} args.modelClass - Model class.
|
|
1994
|
+
* @param {"select" | "selectsExtra"} args.operationName - Selection operation.
|
|
1995
|
+
* @returns {string[]} - Validated selected attribute names.
|
|
1996
|
+
*/
|
|
1997
|
+
validateFrontendModelSelectedAttributes({attributeNames, modelClass, operationName}) {
|
|
1998
|
+
for (const attributeName of attributeNames) {
|
|
1999
|
+
if (this.frontendModelAttributeIsExposed({attributeName, modelClass})) continue
|
|
2000
|
+
|
|
2001
|
+
throw new Error(`Unknown ${operationName} attribute "${attributeName}" for ${modelClass.name}`)
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
return attributeNames
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Resolves a user-queryable frontend attribute to a database column.
|
|
2009
|
+
* @param {object} args - Args.
|
|
2010
|
+
* @param {string} args.attributeName - Requested attribute name.
|
|
2011
|
+
* @param {typeof import("./database/record/index.js").default} args.modelClass - Model class.
|
|
2012
|
+
* @param {"group" | "pluck" | "search" | "sort" | "where"} args.operationName - Query operation.
|
|
2013
|
+
* @returns {string | undefined} - Resolved column name.
|
|
2014
|
+
*/
|
|
2015
|
+
resolveFrontendModelQueryableColumnName({attributeName, modelClass, operationName}) {
|
|
2016
|
+
void operationName
|
|
2017
|
+
|
|
2018
|
+
const resolvedAttributeName = this.frontendModelAttributeNameForKey(modelClass, attributeName)
|
|
2019
|
+
|
|
2020
|
+
if (resolvedAttributeName && !this.frontendModelAttributeIsExposed({attributeName: resolvedAttributeName, modelClass})) {
|
|
2021
|
+
return undefined
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
return this.resolveFrontendModelColumnName(modelClass, attributeName)
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1853
2027
|
/**
|
|
1854
2028
|
* Resolves a key that may be either a camelCase attribute name or a raw DB
|
|
1855
2029
|
* column name to its canonical column name. Returns `undefined` when the
|
|
@@ -1880,7 +2054,11 @@ export default class FrontendModelController extends Controller {
|
|
|
1880
2054
|
*/
|
|
1881
2055
|
applyFrontendModelWhereForPath({modelClass, path, query, where}) {
|
|
1882
2056
|
for (const [attributeName, value] of Object.entries(where)) {
|
|
1883
|
-
const columnName = this.
|
|
2057
|
+
const columnName = this.resolveFrontendModelQueryableColumnName({
|
|
2058
|
+
attributeName,
|
|
2059
|
+
modelClass,
|
|
2060
|
+
operationName: "where"
|
|
2061
|
+
})
|
|
1884
2062
|
|
|
1885
2063
|
if (columnName) {
|
|
1886
2064
|
this.ensureFrontendModelJoinPath({path, query})
|
|
@@ -2004,7 +2182,11 @@ export default class FrontendModelController extends Controller {
|
|
|
2004
2182
|
modelClass,
|
|
2005
2183
|
path: group.path
|
|
2006
2184
|
})
|
|
2007
|
-
const columnName = this.
|
|
2185
|
+
const columnName = this.resolveFrontendModelQueryableColumnName({
|
|
2186
|
+
attributeName: group.column,
|
|
2187
|
+
modelClass: targetModelClass,
|
|
2188
|
+
operationName: "group"
|
|
2189
|
+
})
|
|
2008
2190
|
|
|
2009
2191
|
if (!columnName) {
|
|
2010
2192
|
throw new Error(`Unknown group column "${group.column}" for ${targetModelClass.name}`)
|
|
@@ -2117,7 +2299,11 @@ export default class FrontendModelController extends Controller {
|
|
|
2117
2299
|
const translatedAttributeNames = Object.keys(translatedAttributesMap)
|
|
2118
2300
|
const isTranslatedSortAttribute = translatedAttributeNames.includes(sort.column)
|
|
2119
2301
|
|
|
2120
|
-
const columnName = this.
|
|
2302
|
+
const columnName = this.resolveFrontendModelQueryableColumnName({
|
|
2303
|
+
attributeName: sort.column,
|
|
2304
|
+
modelClass: targetModelClass,
|
|
2305
|
+
operationName: "sort"
|
|
2306
|
+
})
|
|
2121
2307
|
const direction = sort.direction.toUpperCase()
|
|
2122
2308
|
|
|
2123
2309
|
if (isTranslatedSortAttribute) {
|
|
@@ -2194,7 +2380,15 @@ export default class FrontendModelController extends Controller {
|
|
|
2194
2380
|
|
|
2195
2381
|
if (!select) return null
|
|
2196
2382
|
|
|
2197
|
-
|
|
2383
|
+
const selectedAttributes = select[modelClass.getModelName()] || null
|
|
2384
|
+
|
|
2385
|
+
if (!selectedAttributes) return null
|
|
2386
|
+
|
|
2387
|
+
return this.validateFrontendModelSelectedAttributes({
|
|
2388
|
+
attributeNames: selectedAttributes,
|
|
2389
|
+
modelClass,
|
|
2390
|
+
operationName: "select"
|
|
2391
|
+
})
|
|
2198
2392
|
}
|
|
2199
2393
|
|
|
2200
2394
|
/**
|
|
@@ -2207,7 +2401,15 @@ export default class FrontendModelController extends Controller {
|
|
|
2207
2401
|
|
|
2208
2402
|
if (!selectsExtra) return null
|
|
2209
2403
|
|
|
2210
|
-
|
|
2404
|
+
const extraAttributes = selectsExtra[modelClass.getModelName()] || null
|
|
2405
|
+
|
|
2406
|
+
if (!extraAttributes) return null
|
|
2407
|
+
|
|
2408
|
+
return this.validateFrontendModelSelectedAttributes({
|
|
2409
|
+
attributeNames: extraAttributes,
|
|
2410
|
+
modelClass,
|
|
2411
|
+
operationName: "selectsExtra"
|
|
2412
|
+
})
|
|
2211
2413
|
}
|
|
2212
2414
|
|
|
2213
2415
|
/**
|
|
@@ -2274,42 +2476,6 @@ export default class FrontendModelController extends Controller {
|
|
|
2274
2476
|
return null
|
|
2275
2477
|
}
|
|
2276
2478
|
|
|
2277
|
-
/**
|
|
2278
|
-
* Runs frontend model non default attributes for model class.
|
|
2279
|
-
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
2280
|
-
* @returns {string[]} - Attribute names explicitly marked selectedByDefault: false.
|
|
2281
|
-
*/
|
|
2282
|
-
frontendModelNonDefaultAttributesForModelClass(modelClass) {
|
|
2283
|
-
const frontendModelResource = this.frontendModelResourceConfigurationForModelClass(modelClass)
|
|
2284
|
-
const attributes = frontendModelResource?.resourceConfiguration.attributes
|
|
2285
|
-
|
|
2286
|
-
if (!attributes) return []
|
|
2287
|
-
|
|
2288
|
-
if (Array.isArray(attributes)) {
|
|
2289
|
-
return attributes
|
|
2290
|
-
.filter((entry) => {
|
|
2291
|
-
if (typeof entry !== "object" || !entry) return false
|
|
2292
|
-
|
|
2293
|
-
return /** @type {Record<string, ?>} */ (entry).selectedByDefault === false
|
|
2294
|
-
})
|
|
2295
|
-
.map((entry) => /**
|
|
2296
|
-
* Types the following value.
|
|
2297
|
-
* @type {Record<string, ?>} */ (/**
|
|
2298
|
-
* Types the following value.
|
|
2299
|
-
* @type {?} */ (entry)).name)
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
if (typeof attributes === "object") {
|
|
2303
|
-
return Object.entries(attributes)
|
|
2304
|
-
.filter(([, config]) => typeof config === "object" && config && /**
|
|
2305
|
-
* Types the following value.
|
|
2306
|
-
* @type {Record<string, ?>} */ (config).selectedByDefault === false)
|
|
2307
|
-
.map(([name]) => name)
|
|
2308
|
-
}
|
|
2309
|
-
|
|
2310
|
-
return []
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
2479
|
/**
|
|
2314
2480
|
* Runs serialize frontend model attributes.
|
|
2315
2481
|
* @param {import("./database/record/index.js").default} model - Model instance.
|
|
@@ -2407,12 +2573,10 @@ export default class FrontendModelController extends Controller {
|
|
|
2407
2573
|
return modelAttributes
|
|
2408
2574
|
}
|
|
2409
2575
|
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
delete serializedAttributes[excludedName]
|
|
2415
|
-
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Serialized attributes.
|
|
2578
|
+
* @type {Record<string, ?>} */
|
|
2579
|
+
const serializedAttributes = {}
|
|
2416
2580
|
|
|
2417
2581
|
for (const attributeName of defaultAttributes) {
|
|
2418
2582
|
if (!attributeExists(attributeName)) continue
|
|
@@ -2454,7 +2618,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2454
2618
|
* @type {typeof import("./database/record/index.js").default} */ (model.constructor).getModelName()
|
|
2455
2619
|
|
|
2456
2620
|
for (const backendProject of backendProjects) {
|
|
2457
|
-
const resources =
|
|
2621
|
+
const resources = frontendModelResourcesWithBuiltInsForBackendProject(backendProject)
|
|
2458
2622
|
const resourceDefinition = resources[modelClassName]
|
|
2459
2623
|
const resourceClass = resourceDefinition ? frontendModelResourceClassFromDefinition(resourceDefinition) : null
|
|
2460
2624
|
|
|
@@ -2775,12 +2939,43 @@ export default class FrontendModelController extends Controller {
|
|
|
2775
2939
|
return this.frontendModelErrorPayload(frontendModelClientSafeErrorMessage)
|
|
2776
2940
|
}
|
|
2777
2941
|
|
|
2942
|
+
/**
|
|
2943
|
+
* Builds frontend-model endpoint error context for logging and client payload reporters.
|
|
2944
|
+
* @param {object} args - Error context args.
|
|
2945
|
+
* @param {string} args.action - Endpoint/action label.
|
|
2946
|
+
* @param {unknown} args.error - Caught error.
|
|
2947
|
+
* @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url" | "custom-command"} [args.commandType] - Frontend-model command type.
|
|
2948
|
+
* @param {string | undefined} [args.model] - Request model name when available.
|
|
2949
|
+
* @param {string | undefined} [args.requestId] - Batch request id when available.
|
|
2950
|
+
* @returns {FrontendModelEndpointErrorContext} Frontend-model endpoint error context.
|
|
2951
|
+
*/
|
|
2952
|
+
frontendModelEndpointErrorContext({action, commandType, error, model, requestId}) {
|
|
2953
|
+
let resolvedModel = model
|
|
2954
|
+
|
|
2955
|
+
if (!resolvedModel) {
|
|
2956
|
+
const cachedParams = this._frontendModelParamsOverride || this._frontendModelParams
|
|
2957
|
+
const paramsModel = cachedParams ? cachedParams.model : undefined
|
|
2958
|
+
resolvedModel = typeof paramsModel === "string" && paramsModel.length > 0 ? paramsModel : undefined
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
return {
|
|
2962
|
+
action,
|
|
2963
|
+
commandType,
|
|
2964
|
+
controller: this.constructor.name,
|
|
2965
|
+
expectedError: frontendModelExpectedError(error),
|
|
2966
|
+
frontendModelEndpoint: true,
|
|
2967
|
+
model: resolvedModel,
|
|
2968
|
+
requestId
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2778
2972
|
/**
|
|
2779
2973
|
* Runs frontend model client error payload for error.
|
|
2780
|
-
* @param {
|
|
2781
|
-
* @
|
|
2974
|
+
* @param {unknown} error - Caught error.
|
|
2975
|
+
* @param {FrontendModelEndpointErrorContext | undefined} [endpointErrorContext] - Frontend-model endpoint error context.
|
|
2976
|
+
* @returns {Promise<import("./configuration-types.js").ClientErrorPayloadReporterPayload>} - Client payload for the current environment.
|
|
2782
2977
|
*/
|
|
2783
|
-
async frontendModelClientErrorPayloadForError(error) {
|
|
2978
|
+
async frontendModelClientErrorPayloadForError(error, endpointErrorContext) {
|
|
2784
2979
|
const velociousMetadata = frontendModelVelociousMetadataForError(error)
|
|
2785
2980
|
const normalizedError = error instanceof Error ? error : new Error(String(error))
|
|
2786
2981
|
|
|
@@ -2818,9 +3013,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2818
3013
|
...(velociousMetadata ? {velocious: velociousMetadata} : {}),
|
|
2819
3014
|
...validationErrorsPayload,
|
|
2820
3015
|
...(await this.getConfiguration().clientErrorPayloadForError({
|
|
2821
|
-
context: {
|
|
2822
|
-
controller: this.constructor.name
|
|
2823
|
-
},
|
|
3016
|
+
context: endpointErrorContext || {controller: this.constructor.name},
|
|
2824
3017
|
error: normalizedError,
|
|
2825
3018
|
request: this.getRequest()
|
|
2826
3019
|
}))
|
|
@@ -2838,23 +3031,12 @@ export default class FrontendModelController extends Controller {
|
|
|
2838
3031
|
* @returns {Promise<void>} - Resolves after logging.
|
|
2839
3032
|
*/
|
|
2840
3033
|
async frontendModelLogEndpointError({action, error, commandType, model, requestId}) {
|
|
2841
|
-
|
|
2842
|
-
// failures the developer has marked as expected (bad password,
|
|
2843
|
-
// validation message, etc.). Surface the message + metadata to
|
|
2844
|
-
// the client (handled by frontendModelClientErrorPayloadForError),
|
|
2845
|
-
// but skip the error log so monitoring stays focused on real
|
|
2846
|
-
// backend failures.
|
|
2847
|
-
if (frontendModelErrorHasVelociousMetadata(error)) return
|
|
3034
|
+
const errorContext = this.frontendModelEndpointErrorContext({action, commandType, error, model, requestId})
|
|
2848
3035
|
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
resolvedModel = this.frontendModelParams().model
|
|
2854
|
-
} catch {
|
|
2855
|
-
resolvedModel = undefined
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
3036
|
+
// Expected user-flow errors are surfaced to clients by
|
|
3037
|
+
// frontendModelClientErrorPayloadForError, but skipped here so monitoring
|
|
3038
|
+
// stays focused on real backend failures.
|
|
3039
|
+
if (errorContext.expectedError) return
|
|
2858
3040
|
|
|
2859
3041
|
const errorMessage = error instanceof Error
|
|
2860
3042
|
? `${error.message}\n${error.stack || ""}`
|
|
@@ -2864,27 +3046,22 @@ export default class FrontendModelController extends Controller {
|
|
|
2864
3046
|
action,
|
|
2865
3047
|
commandType,
|
|
2866
3048
|
error: errorMessage,
|
|
2867
|
-
model:
|
|
3049
|
+
model: errorContext.model,
|
|
2868
3050
|
requestId
|
|
2869
3051
|
}])
|
|
2870
3052
|
|
|
2871
3053
|
// Surface genuinely unexpected backend failures on the framework-error
|
|
2872
3054
|
// channel so process-level bug reporters capture them, instead of the
|
|
2873
3055
|
// controller silently swallowing them behind the generic "Request
|
|
2874
|
-
// failed." client message.
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
const errorPayload = {
|
|
2880
|
-
context: {action, commandType, frontendModelEndpoint: true, model: resolvedModel, requestId},
|
|
2881
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
2882
|
-
request: this.getRequest()
|
|
2883
|
-
}
|
|
2884
|
-
|
|
2885
|
-
this.getConfiguration().getErrorEvents().emit("framework-error", errorPayload)
|
|
2886
|
-
this.getConfiguration().getErrorEvents().emit("all-error", {...errorPayload, errorType: "framework-error"})
|
|
3056
|
+
// failed." client message.
|
|
3057
|
+
const errorPayload = {
|
|
3058
|
+
context: errorContext,
|
|
3059
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
3060
|
+
request: this.getRequest()
|
|
2887
3061
|
}
|
|
3062
|
+
|
|
3063
|
+
this.getConfiguration().getErrorEvents().emit("framework-error", errorPayload)
|
|
3064
|
+
this.getConfiguration().getErrorEvents().emit("all-error", {...errorPayload, errorType: "framework-error"})
|
|
2888
3065
|
}
|
|
2889
3066
|
|
|
2890
3067
|
/**
|
|
@@ -2903,12 +3080,14 @@ export default class FrontendModelController extends Controller {
|
|
|
2903
3080
|
* @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(responsePayload))
|
|
2904
3081
|
})
|
|
2905
3082
|
} catch (error) {
|
|
2906
|
-
|
|
3083
|
+
const errorContext = this.frontendModelEndpointErrorContext({action, commandType: action, error})
|
|
3084
|
+
|
|
3085
|
+
await this.frontendModelLogEndpointError({action, commandType: action, error, model: errorContext.model})
|
|
2907
3086
|
|
|
2908
3087
|
await this.render({
|
|
2909
3088
|
json: /**
|
|
2910
3089
|
* Types the following value.
|
|
2911
|
-
* @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error)))
|
|
3090
|
+
* @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error, errorContext)))
|
|
2912
3091
|
})
|
|
2913
3092
|
}
|
|
2914
3093
|
}
|
|
@@ -3183,7 +3362,7 @@ export default class FrontendModelController extends Controller {
|
|
|
3183
3362
|
response: responsePayload || this.frontendModelErrorPayload("Action halted by beforeAction.")
|
|
3184
3363
|
})
|
|
3185
3364
|
} catch (error) {
|
|
3186
|
-
|
|
3365
|
+
const errorContext = this.frontendModelEndpointErrorContext({
|
|
3187
3366
|
action: "frontendApi",
|
|
3188
3367
|
commandType,
|
|
3189
3368
|
error,
|
|
@@ -3191,9 +3370,17 @@ export default class FrontendModelController extends Controller {
|
|
|
3191
3370
|
requestId
|
|
3192
3371
|
})
|
|
3193
3372
|
|
|
3373
|
+
await this.frontendModelLogEndpointError({
|
|
3374
|
+
action: errorContext.action,
|
|
3375
|
+
commandType: errorContext.commandType,
|
|
3376
|
+
error,
|
|
3377
|
+
model: errorContext.model,
|
|
3378
|
+
requestId: errorContext.requestId
|
|
3379
|
+
})
|
|
3380
|
+
|
|
3194
3381
|
responses.push({
|
|
3195
3382
|
requestId,
|
|
3196
|
-
response: await this.frontendModelClientErrorPayloadForError(error)
|
|
3383
|
+
response: await this.frontendModelClientErrorPayloadForError(error, errorContext)
|
|
3197
3384
|
})
|
|
3198
3385
|
}
|
|
3199
3386
|
}
|
|
@@ -3414,12 +3601,14 @@ export default class FrontendModelController extends Controller {
|
|
|
3414
3601
|
* @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(responsePayload))
|
|
3415
3602
|
})
|
|
3416
3603
|
} catch (error) {
|
|
3417
|
-
|
|
3604
|
+
const errorContext = this.frontendModelEndpointErrorContext({action: "frontendCustomCommand", commandType: "custom-command", error})
|
|
3605
|
+
|
|
3606
|
+
await this.frontendModelLogEndpointError({action: errorContext.action, commandType: errorContext.commandType, error, model: errorContext.model})
|
|
3418
3607
|
|
|
3419
3608
|
await this.render({
|
|
3420
3609
|
json: /**
|
|
3421
3610
|
* Types the following value.
|
|
3422
|
-
* @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error)))
|
|
3611
|
+
* @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error, errorContext)))
|
|
3423
3612
|
})
|
|
3424
3613
|
}
|
|
3425
3614
|
}
|
|
@@ -14,6 +14,7 @@ import isPlainObject from "../utils/plain-object.js"
|
|
|
14
14
|
* @typedef {import("../controller.js").default & {
|
|
15
15
|
* currentAbility: () => import("../authorization/ability.js").default | undefined,
|
|
16
16
|
* frontendModelAbilityAction: (action: FrontendModelResourceAction) => string,
|
|
17
|
+
* frontendModelAbilityAuthorizedQuery: (action: FrontendModelResourceAction) => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
|
|
17
18
|
* frontendModelAuthorizedQuery: (action: FrontendModelResourceAction) => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
|
|
18
19
|
* frontendModelIndexQuery: () => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
|
|
19
20
|
* frontendModelParams: () => import("../configuration-types.js").VelociousParams,
|
|
@@ -393,7 +394,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
393
394
|
*/
|
|
394
395
|
authorizedQuery(action) {
|
|
395
396
|
// Narrows the controller query to this resource's model class.
|
|
396
|
-
return /** @type {import("../database/query/model-class-query.js").default<TModelClass>} */ (this.typedControllerInstance().
|
|
397
|
+
return /** @type {import("../database/query/model-class-query.js").default<TModelClass>} */ (this.typedControllerInstance().frontendModelAbilityAuthorizedQuery(action))
|
|
397
398
|
}
|
|
398
399
|
|
|
399
400
|
|