velocious 1.0.452 → 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 +9 -3
- 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 +174 -55
- 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/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 +56 -6
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +154 -52
- 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/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 +174 -55
- 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
package/README.md
CHANGED
|
@@ -447,6 +447,7 @@ Frontend-model `where(...)` supports nested relationship descriptors (for exampl
|
|
|
447
447
|
Frontend-model `joins(...)` supports relationship-object descriptors only (for example `Task.joins({project: {creatingUser: true}})`) and rejects raw SQL join strings.
|
|
448
448
|
Frontend-model `distinct(...)` only accepts booleans (`true` by default) and is applied server-side through the backend query API.
|
|
449
449
|
Frontend-model `pluck(...)` validates attribute/path descriptors against configured model metadata and does not accept SQL fragments.
|
|
450
|
+
Frontend-model query fields are limited to attributes exposed by the backend resource. Use `{name: "attributeName", selectedByDefault: false}` for fields that may be selected or filtered explicitly but should stay out of default payloads.
|
|
450
451
|
|
|
451
452
|
When backend payloads include `__preloadedRelationships`, nested frontend-model relationships are hydrated recursively. Relationship methods can use `getRelationshipByName("relationship").loaded()` and will throw when a relationship was not preloaded.
|
|
452
453
|
|
|
@@ -546,11 +547,14 @@ For frontend models, configure `resourceConfig().attachments` and use:
|
|
|
546
547
|
await frontendTask.update({descriptionFile: file})
|
|
547
548
|
const descriptionFile = await frontendTask.descriptionFile().download()
|
|
548
549
|
const descriptionFileUrl = await frontendTask.descriptionFile().url()
|
|
550
|
+
const descriptionFileMetadata = await frontendTask.descriptionFile().first()
|
|
551
|
+
const filesMetadata = await frontendTask.files().toArray()
|
|
549
552
|
await frontendTask.attach(file)
|
|
550
553
|
```
|
|
551
554
|
|
|
552
555
|
Frontend model attachment input does not support `{path: ...}`.
|
|
553
556
|
Use `File`/`Blob`/bytes/`contentBase64` payloads instead.
|
|
557
|
+
Attachment metadata is exposed through the built-in `VelociousAttachment` frontend model with safe fields only: `id`, `recordType`, `recordId`, `name`, `position`, `filename`, `contentType`, `byteSize`, `createdAt`, and `updatedAt`. Storage internals such as `driver`, `storageKey`, and `contentBase64` remain hidden and non-queryable. Direct metadata queries require owner filters: `recordType`, `recordId`, and `name`.
|
|
554
558
|
|
|
555
559
|
When your frontend app calls a backend on another host/port (or under a path prefix), configure transport once:
|
|
556
560
|
|
|
@@ -1002,6 +1006,8 @@ npx velocious g:migration create-tasks
|
|
|
1002
1006
|
```
|
|
1003
1007
|
|
|
1004
1008
|
## Write a migration
|
|
1009
|
+
Implicit `id` primary keys and `references(...)` columns use UUIDs by default. Use an explicit numeric `id` or reference `type` only for legacy schemas or external compatibility.
|
|
1010
|
+
|
|
1005
1011
|
```js
|
|
1006
1012
|
import Migration from "velocious/build/src/database/migration/index.js"
|
|
1007
1013
|
|
|
@@ -1011,9 +1017,9 @@ export default class CreateEvents extends Migration {
|
|
|
1011
1017
|
t.timestamps()
|
|
1012
1018
|
})
|
|
1013
1019
|
|
|
1014
|
-
//
|
|
1015
|
-
await this.createTable("
|
|
1016
|
-
t.
|
|
1020
|
+
// Legacy numeric primary key
|
|
1021
|
+
await this.createTable("legacy_events", {id: {type: "bigint"}}, (t) => {
|
|
1022
|
+
t.references("task", {type: "bigint"})
|
|
1017
1023
|
t.timestamps()
|
|
1018
1024
|
})
|
|
1019
1025
|
|
|
@@ -203,7 +203,7 @@ export default class VelociousDatabaseDriversMssql extends Base{
|
|
|
203
203
|
* Runs primary key type.
|
|
204
204
|
* @returns {string} - The primary key type.
|
|
205
205
|
*/
|
|
206
|
-
primaryKeyType() { return "
|
|
206
|
+
primaryKeyType() { return "uuid" }
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
209
|
* Runs query actual.
|
|
@@ -211,7 +211,7 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
211
211
|
* Runs primary key type.
|
|
212
212
|
* @returns {string} - The primary key type.
|
|
213
213
|
*/
|
|
214
|
-
primaryKeyType() { return "
|
|
214
|
+
primaryKeyType() { return "uuid" }
|
|
215
215
|
|
|
216
216
|
/**
|
|
217
217
|
* Runs retryable database error.
|
|
@@ -269,7 +269,7 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
|
|
|
269
269
|
* Runs primary key type.
|
|
270
270
|
* @returns {string} - The type of the primary key for this driver.
|
|
271
271
|
*/
|
|
272
|
-
primaryKeyType() { return "
|
|
272
|
+
primaryKeyType() { return "uuid" }
|
|
273
273
|
|
|
274
274
|
/**
|
|
275
275
|
* Runs query to sql.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import DatabaseRecord from "../index.js"
|
|
4
|
+
import RecordAttachmentsStore from "./store.js"
|
|
5
|
+
|
|
6
|
+
const INTEGER_STRING_PATTERN = /^-?\d+$/
|
|
7
|
+
|
|
8
|
+
/** Frontend-readable metadata row for `velocious_attachments`. */
|
|
9
|
+
export default class VelociousAttachment extends DatabaseRecord {
|
|
10
|
+
/**
|
|
11
|
+
* Returns the backing attachment table name.
|
|
12
|
+
* @returns {string} - Backing attachment table name.
|
|
13
|
+
*/
|
|
14
|
+
static tableName() {
|
|
15
|
+
return "velocious_attachments"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ensures the framework-owned attachment table exists before loading metadata.
|
|
20
|
+
* @param {object} args - Options object.
|
|
21
|
+
* @param {import("../../../configuration.js").default} args.configuration - Configuration instance.
|
|
22
|
+
* @returns {Promise<void>} - Resolves when complete.
|
|
23
|
+
*/
|
|
24
|
+
static async initializeRecord({configuration}) {
|
|
25
|
+
const store = new RecordAttachmentsStore({
|
|
26
|
+
configuration,
|
|
27
|
+
databaseIdentifier: this.getConfiguredDatabaseIdentifier()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
await store.ensureReady()
|
|
31
|
+
await super.initializeRecord({configuration})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns the attachment id.
|
|
36
|
+
* @returns {string} - Attachment id.
|
|
37
|
+
*/
|
|
38
|
+
id() { return this.readAttribute("id") }
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns the owner model name.
|
|
42
|
+
* @returns {string} - Owner model name.
|
|
43
|
+
*/
|
|
44
|
+
recordType() { return this.readAttribute("recordType") }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns the owner record id.
|
|
48
|
+
* @returns {string} - Owner record id.
|
|
49
|
+
*/
|
|
50
|
+
recordId() { return this.readAttribute("recordId") }
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns the attachment name on the owner model.
|
|
54
|
+
* @returns {string} - Attachment name on the owner model.
|
|
55
|
+
*/
|
|
56
|
+
name() { return this.readAttribute("name") }
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns the attachment position.
|
|
60
|
+
* @returns {number} - Attachment position.
|
|
61
|
+
*/
|
|
62
|
+
position() { return this.readAttribute("position") }
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns the attachment filename.
|
|
66
|
+
* @returns {string} - Attachment filename.
|
|
67
|
+
*/
|
|
68
|
+
filename() { return this.readAttribute("filename") }
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns the attachment content type.
|
|
72
|
+
* @returns {string | null} - Attachment content type.
|
|
73
|
+
*/
|
|
74
|
+
contentType() { return this.readAttribute("contentType") }
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns the attachment byte size.
|
|
78
|
+
* @returns {number} - Attachment byte size.
|
|
79
|
+
*/
|
|
80
|
+
byteSize() { return this.safeIntegerAttribute({attributeName: "byteSize", expectedDescription: "attachment byte size"}) }
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns the created-at timestamp in milliseconds.
|
|
84
|
+
* @returns {number} - Created-at timestamp in milliseconds.
|
|
85
|
+
*/
|
|
86
|
+
createdAtMs() { return this.safeIntegerAttribute({attributeName: "createdAtMs", expectedDescription: "safe millisecond timestamp"}) }
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns the updated-at timestamp in milliseconds.
|
|
90
|
+
* @returns {number} - Updated-at timestamp in milliseconds.
|
|
91
|
+
*/
|
|
92
|
+
updatedAtMs() { return this.safeIntegerAttribute({attributeName: "updatedAtMs", expectedDescription: "safe millisecond timestamp"}) }
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns a checked integer attribute value.
|
|
96
|
+
* @param {object} args - Options object.
|
|
97
|
+
* @param {"byteSize" | "createdAtMs" | "updatedAtMs"} args.attributeName - Integer attribute name.
|
|
98
|
+
* @param {string} args.expectedDescription - Description for error messages.
|
|
99
|
+
* @returns {number} - Safe integer value.
|
|
100
|
+
*/
|
|
101
|
+
safeIntegerAttribute({attributeName, expectedDescription}) {
|
|
102
|
+
const value = this.readAttribute(attributeName)
|
|
103
|
+
let integer
|
|
104
|
+
|
|
105
|
+
if (typeof value === "number") {
|
|
106
|
+
integer = value
|
|
107
|
+
} else if (typeof value === "bigint") {
|
|
108
|
+
integer = Number(value)
|
|
109
|
+
} else if (typeof value === "string" && INTEGER_STRING_PATTERN.test(value)) {
|
|
110
|
+
integer = Number(value)
|
|
111
|
+
} else {
|
|
112
|
+
throw new Error(`Expected ${attributeName} to be a ${expectedDescription}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!Number.isSafeInteger(integer)) {
|
|
116
|
+
throw new Error(`Expected ${attributeName} to be a ${expectedDescription}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return integer
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -98,6 +98,8 @@ export default class RecordAttachmentsStore {
|
|
|
98
98
|
|
|
99
99
|
this._readyPromise = (async () => {
|
|
100
100
|
await this._withDb(async (db) => {
|
|
101
|
+
db.clearSchemaCache()
|
|
102
|
+
|
|
101
103
|
if (await db.tableExists(ATTACHMENTS_TABLE)) {
|
|
102
104
|
await this.ensureAttachmentStoreSchema({db})
|
|
103
105
|
return
|
|
@@ -188,7 +188,7 @@ export default class TableData {
|
|
|
188
188
|
const referenceArgs = args || {}
|
|
189
189
|
const reference = new TableReference(name, referenceArgs)
|
|
190
190
|
const {index, polymorphic, ...restArgs} = referenceArgs
|
|
191
|
-
const columnArgs = Object.assign({isNewColumn: true, type: "
|
|
191
|
+
const columnArgs = Object.assign({isNewColumn: true, type: "uuid"}, restArgs)
|
|
192
192
|
const column = new TableColumn(columnName, columnArgs)
|
|
193
193
|
const indexArgs = typeof index == "object" ? {unique: index.unique === true} : undefined
|
|
194
194
|
const tableIndex = new TableIndex([column], indexArgs)
|
|
@@ -2,7 +2,8 @@ import BaseCommand from "../../../../../cli/base-command.js"
|
|
|
2
2
|
import fs from "fs/promises"
|
|
3
3
|
import path from "node:path"
|
|
4
4
|
import * as inflection from "inflection"
|
|
5
|
-
import {
|
|
5
|
+
import {frontendModelResourceIsBuiltIn, frontendModelResourcesWithBuiltInsForBackendProject} from "../../../../../frontend-models/built-in-resources.js"
|
|
6
|
+
import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition} from "../../../../../frontend-models/resource-definition.js"
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Attribute metadata used for generated frontend-model JSDoc.
|
|
@@ -110,6 +111,10 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
110
111
|
this.validateModelConfig({availableFrontendModelClassNames, className, modelConfig, resourceClass})
|
|
111
112
|
|
|
112
113
|
if (generatedModelNames.has(className)) {
|
|
114
|
+
if (frontendModelResourceIsBuiltIn({modelName: modelClassName, resourceDefinition: resources[modelClassName]})) {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
113
118
|
throw new Error(`Duplicate frontend model definition for '${className}'`)
|
|
114
119
|
}
|
|
115
120
|
|
|
@@ -191,7 +196,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
191
196
|
* @returns {Record<string, import("../../../../../configuration-types.js").FrontendModelResourceDefinition>} - Resource definitions keyed by model class name.
|
|
192
197
|
*/
|
|
193
198
|
resourcesForBackendProject(backendProject) {
|
|
194
|
-
return
|
|
199
|
+
return frontendModelResourcesWithBuiltInsForBackendProject(backendProject)
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
/**
|
|
@@ -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"
|
|
@@ -679,7 +681,7 @@ export default class FrontendModelController extends Controller {
|
|
|
679
681
|
const backendProjects = this.getConfiguration().getBackendProjects()
|
|
680
682
|
|
|
681
683
|
for (const backendProject of backendProjects) {
|
|
682
|
-
const resources =
|
|
684
|
+
const resources = frontendModelResourcesWithBuiltInsForBackendProject(backendProject)
|
|
683
685
|
|
|
684
686
|
if (modelName && modelName.length > 0 && resources[modelName]) {
|
|
685
687
|
const resourceDefinition = resources[modelName]
|
|
@@ -733,7 +735,7 @@ export default class FrontendModelController extends Controller {
|
|
|
733
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.
|
|
734
736
|
*/
|
|
735
737
|
frontendModelResourceConfigurationForBackendProjectModelName({backendProject, modelName}) {
|
|
736
|
-
const resources =
|
|
738
|
+
const resources = frontendModelResourcesWithBuiltInsForBackendProject(backendProject)
|
|
737
739
|
const resourceDefinition = resources[modelName]
|
|
738
740
|
|
|
739
741
|
if (!resourceDefinition) return null
|
|
@@ -1027,16 +1029,31 @@ export default class FrontendModelController extends Controller {
|
|
|
1027
1029
|
}
|
|
1028
1030
|
|
|
1029
1031
|
/**
|
|
1030
|
-
* Runs frontend model authorized query.
|
|
1032
|
+
* Runs frontend model ability authorized query.
|
|
1031
1033
|
* @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
|
|
1032
1034
|
* @returns {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} - Authorized query for the action.
|
|
1033
1035
|
*/
|
|
1034
|
-
|
|
1036
|
+
frontendModelAbilityAuthorizedQuery(action) {
|
|
1035
1037
|
const abilityAction = this.frontendModelAbilityAction(action)
|
|
1036
1038
|
|
|
1037
1039
|
return this.frontendModelClass().accessibleFor(abilityAction, this.currentAbility())
|
|
1038
1040
|
}
|
|
1039
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
|
+
|
|
1040
1057
|
/**
|
|
1041
1058
|
* Runs frontend model primary key value.
|
|
1042
1059
|
* @param {import("./database/record/index.js").default} model - Model instance.
|
|
@@ -1638,7 +1655,11 @@ export default class FrontendModelController extends Controller {
|
|
|
1638
1655
|
modelClass,
|
|
1639
1656
|
path: pluckEntry.path
|
|
1640
1657
|
})
|
|
1641
|
-
const columnName = this.
|
|
1658
|
+
const columnName = this.resolveFrontendModelQueryableColumnName({
|
|
1659
|
+
attributeName: pluckEntry.column,
|
|
1660
|
+
modelClass: targetModelClass,
|
|
1661
|
+
operationName: "pluck"
|
|
1662
|
+
})
|
|
1642
1663
|
|
|
1643
1664
|
if (!columnName) {
|
|
1644
1665
|
throw new Error(`Unknown pluck column "${pluckEntry.column}" for ${targetModelClass.name}`)
|
|
@@ -1717,7 +1738,11 @@ export default class FrontendModelController extends Controller {
|
|
|
1717
1738
|
modelClass,
|
|
1718
1739
|
path: search.path
|
|
1719
1740
|
})
|
|
1720
|
-
const columnName = this.
|
|
1741
|
+
const columnName = this.resolveFrontendModelQueryableColumnName({
|
|
1742
|
+
attributeName: search.column,
|
|
1743
|
+
modelClass: targetModelClass,
|
|
1744
|
+
operationName: "search"
|
|
1745
|
+
})
|
|
1721
1746
|
|
|
1722
1747
|
if (!columnName) {
|
|
1723
1748
|
throw new Error(`Unknown search column "${search.column}" for ${targetModelClass.name}`)
|
|
@@ -1895,6 +1920,110 @@ export default class FrontendModelController extends Controller {
|
|
|
1895
1920
|
}
|
|
1896
1921
|
}
|
|
1897
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
|
+
|
|
1898
2027
|
/**
|
|
1899
2028
|
* Resolves a key that may be either a camelCase attribute name or a raw DB
|
|
1900
2029
|
* column name to its canonical column name. Returns `undefined` when the
|
|
@@ -1925,7 +2054,11 @@ export default class FrontendModelController extends Controller {
|
|
|
1925
2054
|
*/
|
|
1926
2055
|
applyFrontendModelWhereForPath({modelClass, path, query, where}) {
|
|
1927
2056
|
for (const [attributeName, value] of Object.entries(where)) {
|
|
1928
|
-
const columnName = this.
|
|
2057
|
+
const columnName = this.resolveFrontendModelQueryableColumnName({
|
|
2058
|
+
attributeName,
|
|
2059
|
+
modelClass,
|
|
2060
|
+
operationName: "where"
|
|
2061
|
+
})
|
|
1929
2062
|
|
|
1930
2063
|
if (columnName) {
|
|
1931
2064
|
this.ensureFrontendModelJoinPath({path, query})
|
|
@@ -2049,7 +2182,11 @@ export default class FrontendModelController extends Controller {
|
|
|
2049
2182
|
modelClass,
|
|
2050
2183
|
path: group.path
|
|
2051
2184
|
})
|
|
2052
|
-
const columnName = this.
|
|
2185
|
+
const columnName = this.resolveFrontendModelQueryableColumnName({
|
|
2186
|
+
attributeName: group.column,
|
|
2187
|
+
modelClass: targetModelClass,
|
|
2188
|
+
operationName: "group"
|
|
2189
|
+
})
|
|
2053
2190
|
|
|
2054
2191
|
if (!columnName) {
|
|
2055
2192
|
throw new Error(`Unknown group column "${group.column}" for ${targetModelClass.name}`)
|
|
@@ -2162,7 +2299,11 @@ export default class FrontendModelController extends Controller {
|
|
|
2162
2299
|
const translatedAttributeNames = Object.keys(translatedAttributesMap)
|
|
2163
2300
|
const isTranslatedSortAttribute = translatedAttributeNames.includes(sort.column)
|
|
2164
2301
|
|
|
2165
|
-
const columnName = this.
|
|
2302
|
+
const columnName = this.resolveFrontendModelQueryableColumnName({
|
|
2303
|
+
attributeName: sort.column,
|
|
2304
|
+
modelClass: targetModelClass,
|
|
2305
|
+
operationName: "sort"
|
|
2306
|
+
})
|
|
2166
2307
|
const direction = sort.direction.toUpperCase()
|
|
2167
2308
|
|
|
2168
2309
|
if (isTranslatedSortAttribute) {
|
|
@@ -2239,7 +2380,15 @@ export default class FrontendModelController extends Controller {
|
|
|
2239
2380
|
|
|
2240
2381
|
if (!select) return null
|
|
2241
2382
|
|
|
2242
|
-
|
|
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
|
+
})
|
|
2243
2392
|
}
|
|
2244
2393
|
|
|
2245
2394
|
/**
|
|
@@ -2252,7 +2401,15 @@ export default class FrontendModelController extends Controller {
|
|
|
2252
2401
|
|
|
2253
2402
|
if (!selectsExtra) return null
|
|
2254
2403
|
|
|
2255
|
-
|
|
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
|
+
})
|
|
2256
2413
|
}
|
|
2257
2414
|
|
|
2258
2415
|
/**
|
|
@@ -2319,42 +2476,6 @@ export default class FrontendModelController extends Controller {
|
|
|
2319
2476
|
return null
|
|
2320
2477
|
}
|
|
2321
2478
|
|
|
2322
|
-
/**
|
|
2323
|
-
* Runs frontend model non default attributes for model class.
|
|
2324
|
-
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
2325
|
-
* @returns {string[]} - Attribute names explicitly marked selectedByDefault: false.
|
|
2326
|
-
*/
|
|
2327
|
-
frontendModelNonDefaultAttributesForModelClass(modelClass) {
|
|
2328
|
-
const frontendModelResource = this.frontendModelResourceConfigurationForModelClass(modelClass)
|
|
2329
|
-
const attributes = frontendModelResource?.resourceConfiguration.attributes
|
|
2330
|
-
|
|
2331
|
-
if (!attributes) return []
|
|
2332
|
-
|
|
2333
|
-
if (Array.isArray(attributes)) {
|
|
2334
|
-
return attributes
|
|
2335
|
-
.filter((entry) => {
|
|
2336
|
-
if (typeof entry !== "object" || !entry) return false
|
|
2337
|
-
|
|
2338
|
-
return /** @type {Record<string, ?>} */ (entry).selectedByDefault === false
|
|
2339
|
-
})
|
|
2340
|
-
.map((entry) => /**
|
|
2341
|
-
* Types the following value.
|
|
2342
|
-
* @type {Record<string, ?>} */ (/**
|
|
2343
|
-
* Types the following value.
|
|
2344
|
-
* @type {?} */ (entry)).name)
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
if (typeof attributes === "object") {
|
|
2348
|
-
return Object.entries(attributes)
|
|
2349
|
-
.filter(([, config]) => typeof config === "object" && config && /**
|
|
2350
|
-
* Types the following value.
|
|
2351
|
-
* @type {Record<string, ?>} */ (config).selectedByDefault === false)
|
|
2352
|
-
.map(([name]) => name)
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
return []
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
2479
|
/**
|
|
2359
2480
|
* Runs serialize frontend model attributes.
|
|
2360
2481
|
* @param {import("./database/record/index.js").default} model - Model instance.
|
|
@@ -2452,12 +2573,10 @@ export default class FrontendModelController extends Controller {
|
|
|
2452
2573
|
return modelAttributes
|
|
2453
2574
|
}
|
|
2454
2575
|
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
delete serializedAttributes[excludedName]
|
|
2460
|
-
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Serialized attributes.
|
|
2578
|
+
* @type {Record<string, ?>} */
|
|
2579
|
+
const serializedAttributes = {}
|
|
2461
2580
|
|
|
2462
2581
|
for (const attributeName of defaultAttributes) {
|
|
2463
2582
|
if (!attributeExists(attributeName)) continue
|
|
@@ -2499,7 +2618,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2499
2618
|
* @type {typeof import("./database/record/index.js").default} */ (model.constructor).getModelName()
|
|
2500
2619
|
|
|
2501
2620
|
for (const backendProject of backendProjects) {
|
|
2502
|
-
const resources =
|
|
2621
|
+
const resources = frontendModelResourcesWithBuiltInsForBackendProject(backendProject)
|
|
2503
2622
|
const resourceDefinition = resources[modelClassName]
|
|
2504
2623
|
const resourceClass = resourceDefinition ? frontendModelResourceClassFromDefinition(resourceDefinition) : null
|
|
2505
2624
|
|
|
@@ -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
|
|