velocious 1.0.643 → 1.0.645
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 +21 -10
- package/build/authorization/ability.js +36 -13
- package/build/background-jobs/adapter.js +9 -0
- package/build/background-jobs/main.js +35 -10
- package/build/background-jobs/store.js +217 -38
- package/build/background-jobs/types.js +15 -0
- package/build/configuration-types.js +1 -1
- package/build/configuration.js +45 -9
- package/build/database/drivers/base-column.js +8 -0
- package/build/database/drivers/mssql/sql/update.js +1 -3
- package/build/database/drivers/mysql/sql/update.js +1 -3
- package/build/database/drivers/pgsql/column.js +22 -0
- package/build/database/drivers/pgsql/sql/update.js +1 -3
- package/build/database/drivers/sqlite/sql/alter-table.js +11 -1
- package/build/database/drivers/sqlite/sql/update.js +1 -3
- package/build/database/query/index.js +2 -2
- package/build/database/query/model-class-query.js +26 -10
- package/build/database/query/preloader/has-many.js +4 -1
- package/build/database/query/query-data.js +2 -1
- package/build/database/query/update-base.js +14 -0
- package/build/database/query/with-count.js +3 -1
- package/build/database/record/acts-as-list.js +37 -13
- package/build/database/record/attachments/store.js +336 -26
- package/build/database/record/auditing.js +6 -3
- package/build/database/record/index.js +197 -42
- package/build/database/record/instance-relationships/belongs-to.js +2 -1
- package/build/database/record/validators/uniqueness.js +8 -5
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
- package/build/frontend-model-controller.js +136 -53
- package/build/frontend-model-resource/base-resource.js +87 -37
- package/build/frontend-model-resource/velocious-attachment-resource.js +98 -31
- package/build/frontend-models/base.js +307 -59
- package/build/frontend-models/preloader.js +6 -3
- package/build/frontend-models/query.js +15 -6
- package/build/frontend-models/resource-definition.js +21 -2
- package/build/frontend-models/use-destroyed-event.js +1 -1
- package/build/frontend-models/use-model-class-event.js +2 -2
- package/build/frontend-models/use-updated-event.js +1 -1
- package/build/frontend-models/websocket-channel.js +256 -93
- package/build/frontend-models/websocket-publishers.js +218 -18
- package/build/http-server/client/websocket-session.js +13 -1
- package/build/http-server/websocket-channel.js +15 -2
- package/build/src/authorization/ability.d.ts +9 -3
- package/build/src/authorization/ability.d.ts.map +1 -1
- package/build/src/authorization/ability.js +33 -14
- package/build/src/background-jobs/adapter.d.ts +6 -0
- package/build/src/background-jobs/adapter.d.ts.map +1 -1
- package/build/src/background-jobs/adapter.js +9 -1
- package/build/src/background-jobs/main.d.ts +6 -0
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +34 -11
- package/build/src/background-jobs/store.d.ts +66 -12
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +191 -35
- package/build/src/background-jobs/types.d.ts +55 -0
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +16 -1
- package/build/src/configuration-types.d.ts +3 -3
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +2 -2
- package/build/src/configuration.d.ts +18 -8
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +38 -7
- package/build/src/database/drivers/base-column.d.ts +5 -0
- package/build/src/database/drivers/base-column.d.ts.map +1 -1
- package/build/src/database/drivers/base-column.js +8 -1
- package/build/src/database/drivers/mssql/sql/update.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/sql/update.js +2 -4
- package/build/src/database/drivers/mysql/sql/update.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/sql/update.js +2 -4
- package/build/src/database/drivers/pgsql/column.d.ts +5 -0
- package/build/src/database/drivers/pgsql/column.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/column.js +19 -1
- package/build/src/database/drivers/pgsql/sql/update.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/sql/update.js +2 -4
- package/build/src/database/drivers/sqlite/sql/alter-table.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/sql/alter-table.js +6 -2
- package/build/src/database/drivers/sqlite/sql/update.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/sql/update.js +2 -4
- package/build/src/database/query/index.d.ts +4 -4
- package/build/src/database/query/index.d.ts.map +1 -1
- package/build/src/database/query/index.js +3 -3
- package/build/src/database/query/model-class-query.d.ts +8 -2
- package/build/src/database/query/model-class-query.d.ts.map +1 -1
- package/build/src/database/query/model-class-query.js +25 -11
- package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-many.js +4 -2
- package/build/src/database/query/query-data.d.ts.map +1 -1
- package/build/src/database/query/query-data.js +3 -2
- package/build/src/database/query/update-base.d.ts +7 -0
- package/build/src/database/query/update-base.d.ts.map +1 -1
- package/build/src/database/query/update-base.js +13 -1
- package/build/src/database/query/with-count.d.ts.map +1 -1
- package/build/src/database/query/with-count.js +3 -2
- package/build/src/database/record/acts-as-list.d.ts.map +1 -1
- package/build/src/database/record/acts-as-list.js +35 -14
- package/build/src/database/record/attachments/store.d.ts +67 -0
- package/build/src/database/record/attachments/store.d.ts.map +1 -1
- package/build/src/database/record/attachments/store.js +301 -25
- package/build/src/database/record/auditing.d.ts.map +1 -1
- package/build/src/database/record/auditing.js +7 -4
- package/build/src/database/record/index.d.ts +78 -26
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +171 -42
- package/build/src/database/record/instance-relationships/belongs-to.d.ts.map +1 -1
- package/build/src/database/record/instance-relationships/belongs-to.js +3 -2
- package/build/src/database/record/validators/uniqueness.d.ts.map +1 -1
- package/build/src/database/record/validators/uniqueness.js +8 -6
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +4 -4
- 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 +24 -12
- package/build/src/frontend-model-controller.d.ts +29 -8
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +122 -51
- package/build/src/frontend-model-resource/base-resource.d.ts +51 -24
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +83 -37
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +45 -26
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/velocious-attachment-resource.js +89 -31
- package/build/src/frontend-models/base.d.ts +53 -20
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +275 -59
- package/build/src/frontend-models/preloader.d.ts.map +1 -1
- package/build/src/frontend-models/preloader.js +6 -4
- package/build/src/frontend-models/query.d.ts +2 -2
- package/build/src/frontend-models/query.d.ts.map +1 -1
- package/build/src/frontend-models/query.js +14 -7
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +20 -3
- package/build/src/frontend-models/use-destroyed-event.d.ts +1 -1
- package/build/src/frontend-models/use-destroyed-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-destroyed-event.js +2 -2
- package/build/src/frontend-models/use-model-class-event.d.ts +2 -2
- package/build/src/frontend-models/use-model-class-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-model-class-event.js +3 -3
- package/build/src/frontend-models/use-updated-event.d.ts +1 -1
- package/build/src/frontend-models/use-updated-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-updated-event.js +2 -2
- package/build/src/frontend-models/websocket-channel.d.ts +60 -41
- package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-channel.js +225 -85
- package/build/src/frontend-models/websocket-publishers.d.ts +9 -0
- package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-publishers.js +190 -19
- package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
- package/build/src/http-server/client/websocket-session.js +11 -2
- package/build/src/http-server/websocket-channel.d.ts +20 -8
- package/build/src/http-server/websocket-channel.d.ts.map +1 -1
- package/build/src/http-server/websocket-channel.js +15 -3
- package/build/src/sync/assets/cache.d.ts +288 -0
- package/build/src/sync/assets/cache.d.ts.map +1 -0
- package/build/src/sync/assets/cache.js +883 -0
- package/build/src/sync/assets/types.d.ts +199 -0
- package/build/src/sync/assets/types.d.ts.map +1 -0
- package/build/src/sync/assets/types.js +56 -0
- package/build/src/sync/sync-api-client.d.ts.map +1 -1
- package/build/src/sync/sync-api-client.js +4 -3
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +3 -2
- package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
- package/build/src/sync/sync-envelope-replay-service.js +6 -5
- package/build/src/sync/sync-publisher.d.ts.map +1 -1
- package/build/src/sync/sync-publisher.js +4 -3
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts +2 -2
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts.map +1 -1
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +3 -3
- package/build/src/testing/test-profiler.d.ts +4 -4
- package/build/src/testing/test-profiler.d.ts.map +1 -1
- package/build/src/testing/test-profiler.js +3 -3
- package/build/src/testing/test-runner.d.ts +279 -117
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +503 -933
- package/build/src/testing/test.d.ts +11 -72
- package/build/src/testing/test.d.ts.map +1 -1
- package/build/src/testing/test.js +131 -284
- package/build/src/testing/velocious-attempt-executor.d.ts +54 -0
- package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -0
- package/build/src/testing/velocious-attempt-executor.js +408 -0
- package/build/src/testing/velocious-runner-reporter.d.ts +67 -0
- package/build/src/testing/velocious-runner-reporter.d.ts.map +1 -0
- package/build/src/testing/velocious-runner-reporter.js +240 -0
- package/build/src/testing/velocious-suite-hook-executor.d.ts +19 -0
- package/build/src/testing/velocious-suite-hook-executor.d.ts.map +1 -0
- package/build/src/testing/velocious-suite-hook-executor.js +41 -0
- package/build/src/testing/velocious-test-arguments.d.ts +39 -0
- package/build/src/testing/velocious-test-arguments.d.ts.map +1 -0
- package/build/src/testing/velocious-test-arguments.js +55 -0
- package/build/src/utils/model-primary-key.d.ts +73 -0
- package/build/src/utils/model-primary-key.d.ts.map +1 -0
- package/build/src/utils/model-primary-key.js +144 -0
- package/build/src/utils/sha256-bytes-hex.d.ts +7 -0
- package/build/src/utils/sha256-bytes-hex.d.ts.map +1 -0
- package/build/src/utils/sha256-bytes-hex.js +117 -0
- package/build/src/utils/sha256-hex.d.ts +2 -4
- package/build/src/utils/sha256-hex.d.ts.map +1 -1
- package/build/src/utils/sha256-hex.js +6 -120
- package/build/sync/assets/cache.js +998 -0
- package/build/sync/assets/types.js +62 -0
- package/build/sync/sync-api-client.js +3 -2
- package/build/sync/sync-client.js +2 -1
- package/build/sync/sync-envelope-replay-service.js +5 -4
- package/build/sync/sync-publisher.js +3 -2
- package/build/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
- package/build/testing/test-profiler.js +2 -2
- package/build/testing/test-runner.js +496 -990
- package/build/testing/test.js +164 -306
- package/build/testing/velocious-attempt-executor.js +441 -0
- package/build/testing/velocious-runner-reporter.js +264 -0
- package/build/testing/velocious-suite-hook-executor.js +43 -0
- package/build/testing/velocious-test-arguments.js +64 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/model-primary-key.js +171 -0
- package/build/utils/sha256-bytes-hex.js +132 -0
- package/build/utils/sha256-hex.js +7 -135
- package/package.json +5 -4
- package/src/authorization/ability.js +36 -13
- package/src/background-jobs/adapter.js +9 -0
- package/src/background-jobs/main.js +35 -10
- package/src/background-jobs/store.js +217 -38
- package/src/background-jobs/types.js +15 -0
- package/src/configuration-types.js +1 -1
- package/src/configuration.js +45 -9
- package/src/database/drivers/base-column.js +8 -0
- package/src/database/drivers/mssql/sql/update.js +1 -3
- package/src/database/drivers/mysql/sql/update.js +1 -3
- package/src/database/drivers/pgsql/column.js +22 -0
- package/src/database/drivers/pgsql/sql/update.js +1 -3
- package/src/database/drivers/sqlite/sql/alter-table.js +11 -1
- package/src/database/drivers/sqlite/sql/update.js +1 -3
- package/src/database/query/index.js +2 -2
- package/src/database/query/model-class-query.js +26 -10
- package/src/database/query/preloader/has-many.js +4 -1
- package/src/database/query/query-data.js +2 -1
- package/src/database/query/update-base.js +14 -0
- package/src/database/query/with-count.js +3 -1
- package/src/database/record/acts-as-list.js +37 -13
- package/src/database/record/attachments/store.js +336 -26
- package/src/database/record/auditing.js +6 -3
- package/src/database/record/index.js +197 -42
- package/src/database/record/instance-relationships/belongs-to.js +2 -1
- package/src/database/record/validators/uniqueness.js +8 -5
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
- package/src/frontend-model-controller.js +136 -53
- package/src/frontend-model-resource/base-resource.js +87 -37
- package/src/frontend-model-resource/velocious-attachment-resource.js +98 -31
- package/src/frontend-models/base.js +307 -59
- package/src/frontend-models/preloader.js +6 -3
- package/src/frontend-models/query.js +15 -6
- package/src/frontend-models/resource-definition.js +21 -2
- package/src/frontend-models/use-destroyed-event.js +1 -1
- package/src/frontend-models/use-model-class-event.js +2 -2
- package/src/frontend-models/use-updated-event.js +1 -1
- package/src/frontend-models/websocket-channel.js +256 -93
- package/src/frontend-models/websocket-publishers.js +218 -18
- package/src/http-server/client/websocket-session.js +13 -1
- package/src/http-server/websocket-channel.js +15 -2
- package/src/sync/assets/cache.js +998 -0
- package/src/sync/assets/types.js +62 -0
- package/src/sync/sync-api-client.js +3 -2
- package/src/sync/sync-client.js +2 -1
- package/src/sync/sync-envelope-replay-service.js +5 -4
- package/src/sync/sync-publisher.js +3 -2
- package/src/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
- package/src/testing/test-profiler.js +2 -2
- package/src/testing/test-runner.js +496 -990
- package/src/testing/test.js +164 -306
- package/src/testing/velocious-attempt-executor.js +441 -0
- package/src/testing/velocious-runner-reporter.js +264 -0
- package/src/testing/velocious-suite-hook-executor.js +43 -0
- package/src/testing/velocious-test-arguments.js +64 -0
- package/src/utils/model-primary-key.js +171 -0
- package/src/utils/sha256-bytes-hex.js +132 -0
- package/src/utils/sha256-hex.js +7 -135
- package/build/src/testing/testing-package-adapter.d.ts +0 -9
- package/build/src/testing/testing-package-adapter.d.ts.map +0 -1
- package/build/src/testing/testing-package-adapter.js +0 -69
- package/build/testing/testing-package-adapter.js +0 -76
- package/src/testing/testing-package-adapter.js +0 -76
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import VelociousWebsocketChannel from "../http-server/websocket-channel.js"
|
|
4
|
+
import {Buffer} from "node:buffer"
|
|
4
5
|
import Response from "../http-server/client/response.js"
|
|
5
|
-
import {
|
|
6
|
+
import {frontendModelResourcesWithBuiltInsForBackendProject} from "./built-in-resources.js"
|
|
7
|
+
import {frontendModelResourceClassFromDefinition} from "./resource-definition.js"
|
|
8
|
+
import {deserializeFrontendModelTransportValue, serializeFrontendModelTransportValue} from "./transport-serialization.js"
|
|
9
|
+
import {modelPrimaryKeyConditions} from "../utils/model-primary-key.js"
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Defines this typedef.
|
|
9
|
-
* @typedef {{action?: string, id?:
|
|
13
|
+
* @typedef {{action?: string, id?: import("../utils/model-primary-key.js").ModelPrimaryKeyValue, matchedEventFilterKeys?: string[], previousId?: import("../utils/model-primary-key.js").ModelPrimaryKeyValue, record?: import("./query.js").FrontendModelTransportValue, [key: string]: import("./query.js").FrontendModelTransportValue | string[] | undefined}} FrontendModelLifecycleBroadcastBody
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Record<string, import("./query.js").FrontendModelTransportValue>} DestroyAuthorizationRecord
|
|
10
17
|
*/
|
|
11
18
|
/**
|
|
12
19
|
* Defines this typedef.
|
|
@@ -22,6 +29,69 @@ const EVENT_FILTER_KEYS = new Set(["joins", "key", "searches", "where"])
|
|
|
22
29
|
// to avoid the configuration → logger → websocket-publishers import cycle.
|
|
23
30
|
const FRONTEND_MODELS_CHANNEL_NAME = "frontend-models"
|
|
24
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Checks whether a server-side broadcast value is a destroy-authorization record.
|
|
34
|
+
* @param {import("./query.js").FrontendModelTransportValue | undefined} value - Candidate value.
|
|
35
|
+
* @returns {value is DestroyAuthorizationRecord} - Whether the value is a column-keyed record.
|
|
36
|
+
*/
|
|
37
|
+
function isDestroyAuthorizationRecord(value) {
|
|
38
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checks whether a captured value is a serialized binary-column marker.
|
|
43
|
+
* @param {import("./query.js").FrontendModelTransportValue} value - Captured column value.
|
|
44
|
+
* @returns {value is {__velociousDestroyAuthorizationType: "binary", value: number[]}} - Whether the value contains serialized bytes.
|
|
45
|
+
*/
|
|
46
|
+
function isDestroyAuthorizationBinary(value) {
|
|
47
|
+
return Boolean(
|
|
48
|
+
value
|
|
49
|
+
&& typeof value === "object"
|
|
50
|
+
&& !Array.isArray(value)
|
|
51
|
+
&& "__velociousDestroyAuthorizationType" in value
|
|
52
|
+
&& value.__velociousDestroyAuthorizationType === "binary"
|
|
53
|
+
&& "value" in value
|
|
54
|
+
&& Array.isArray(value.value)
|
|
55
|
+
&& value.value.every((byte) => typeof byte === "number")
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Builds a PostgreSQL array expression whose elements are quoted by the active driver.
|
|
61
|
+
* @param {import("../database/drivers/base.js").default} driver - Active PostgreSQL driver.
|
|
62
|
+
* @param {import("./query.js").FrontendModelTransportValue[]} values - Captured array values.
|
|
63
|
+
* @returns {string} - PostgreSQL array expression.
|
|
64
|
+
*/
|
|
65
|
+
function pgsqlArrayValueSql(driver, values) {
|
|
66
|
+
const elements = values.map((value) => {
|
|
67
|
+
if (Array.isArray(value)) return pgsqlArrayValueSql(driver, value)
|
|
68
|
+
if (value === null) return "NULL"
|
|
69
|
+
|
|
70
|
+
return driver.quote(value)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return `ARRAY[${elements.join(", ")}]`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolves frontend resource identity attributes to backing database columns.
|
|
78
|
+
* @param {typeof import("../database/record/index.js").default} ModelClass - Backing model class.
|
|
79
|
+
* @param {import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition} primaryKey - Frontend resource identity definition.
|
|
80
|
+
* @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} id - Frontend resource identity.
|
|
81
|
+
* @returns {Record<string, import("../utils/model-primary-key.js").ModelPrimaryKeyScalar>} - Backing column conditions.
|
|
82
|
+
*/
|
|
83
|
+
function frontendModelPrimaryKeyDatabaseConditions(ModelClass, primaryKey, id) {
|
|
84
|
+
const resourceConditions = modelPrimaryKeyConditions(primaryKey, id)
|
|
85
|
+
/** @type {Record<string, import("../utils/model-primary-key.js").ModelPrimaryKeyScalar>} */
|
|
86
|
+
const databaseConditions = {}
|
|
87
|
+
|
|
88
|
+
for (const [attributeName, value] of Object.entries(resourceConditions)) {
|
|
89
|
+
databaseConditions[ModelClass.getColumnNameForAttributeName(attributeName)] = value
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return databaseConditions
|
|
93
|
+
}
|
|
94
|
+
|
|
25
95
|
/**
|
|
26
96
|
* Runs transport serialization options for a configuration.
|
|
27
97
|
* @param {import("../configuration.js").default} configuration - Configuration instance.
|
|
@@ -37,15 +107,11 @@ function transportSerializationOptionsForConfiguration(configuration) {
|
|
|
37
107
|
* Per-session channel subscription for frontend-model lifecycle events.
|
|
38
108
|
* Replaces the legacy `FrontendModelWebsocketChannel` (Phase 3).
|
|
39
109
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* per-record visibility guarantees for massively cheaper broadcast fan-out.
|
|
46
|
-
* Subscriber-provided event filters can still narrow which create/update
|
|
47
|
-
* events are delivered, but they are matching predicates rather than
|
|
48
|
-
* per-record authorization checks.
|
|
110
|
+
* `canSubscribe` resolves the caller's ability once and requires a read rule
|
|
111
|
+
* for the requested model class. Create/update delivery then reloads each
|
|
112
|
+
* record through that ability and serializes it through the subscribed
|
|
113
|
+
* frontend resource. Subscriber-provided event filters can further narrow
|
|
114
|
+
* those authorized events.
|
|
49
115
|
*
|
|
50
116
|
* Wire: subscribe with `subscribeChannel("frontend-models", {params: {model: ModelName}})`.
|
|
51
117
|
* Backend publishes `{action, id, record}` via
|
|
@@ -69,8 +135,7 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
69
135
|
this._eventFilters()
|
|
70
136
|
|
|
71
137
|
const configuration = this.session.configuration
|
|
72
|
-
const
|
|
73
|
-
const ModelClass = modelClasses[modelName]
|
|
138
|
+
const ModelClass = this._modelClass(modelName)
|
|
74
139
|
|
|
75
140
|
if (!ModelClass) return false
|
|
76
141
|
|
|
@@ -98,10 +163,28 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
98
163
|
return readRules.some((/** @type {{effect: string}} */ rule) => rule.effect === "allow")
|
|
99
164
|
}
|
|
100
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Resolves a subscription name through frontend resources before falling back to a backing model name.
|
|
168
|
+
* @param {string} modelName - Frontend resource name.
|
|
169
|
+
* @returns {typeof import("../database/record/index.js").default | undefined} - Backing model class.
|
|
170
|
+
*/
|
|
171
|
+
_modelClass(modelName) {
|
|
172
|
+
const configuration = this.session.configuration
|
|
173
|
+
|
|
174
|
+
for (const backendProject of configuration.getBackendProjects()) {
|
|
175
|
+
const resourceDefinition = frontendModelResourcesWithBuiltInsForBackendProject(backendProject)[modelName]
|
|
176
|
+
const resourceClass = resourceDefinition ? frontendModelResourceClassFromDefinition(resourceDefinition) : null
|
|
177
|
+
|
|
178
|
+
if (resourceClass?.ModelClass) return resourceClass.modelClass()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return configuration.getModelClasses()[modelName]
|
|
182
|
+
}
|
|
183
|
+
|
|
101
184
|
/**
|
|
102
185
|
* Runs deliver broadcast.
|
|
103
186
|
* @param {FrontendModelLifecycleBroadcastBody} body - Broadcast body.
|
|
104
|
-
* @param {
|
|
187
|
+
* @param {import("../http-server/websocket-channel.js").WebsocketBroadcastMetadata} [meta] - Optional server-side broadcast metadata.
|
|
105
188
|
* @returns {Promise<void>} Resolves after delivery.
|
|
106
189
|
*/
|
|
107
190
|
async deliverBroadcast(body, meta) {
|
|
@@ -111,33 +194,38 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
111
194
|
/**
|
|
112
195
|
* Runs deliver broadcast.
|
|
113
196
|
* @param {FrontendModelLifecycleBroadcastBody} body - Broadcast body.
|
|
114
|
-
* @param {
|
|
197
|
+
* @param {import("../http-server/websocket-channel.js").WebsocketBroadcastMetadata} [meta] - Optional server-side broadcast metadata.
|
|
115
198
|
* @returns {Promise<void>} Resolves after delivery.
|
|
116
199
|
*/
|
|
117
200
|
async _deliverBroadcast(body, meta) {
|
|
118
201
|
const hasEventFilters = this._hasEventFilterParams()
|
|
119
202
|
|
|
120
|
-
if (!this._hasProjectionParams() && !hasEventFilters) {
|
|
121
|
-
// Even unfiltered subscriptions must respect the subscriber's ability. A create/update carries
|
|
122
|
-
// the record, so only deliver it when the record is within the authenticated ability's scope.
|
|
123
|
-
// Destroys (and bodies without a usable id) carry no record, so pass them through unchanged.
|
|
124
|
-
if (body && typeof body === "object" && (body.action === "create" || body.action === "update") && body.id !== undefined && body.id !== null) {
|
|
125
|
-
const FrontendModelController = await this._frontendModelControllerClass()
|
|
126
|
-
|
|
127
|
-
if (!await this._eventIsAccessible(body.id, FrontendModelController)) return
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
this.sendMessage(body, meta)
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
|
|
134
203
|
if (!body || typeof body !== "object") {
|
|
135
204
|
if (!hasEventFilters || this._hasUnfilteredEventDelivery()) this.sendMessage(body, meta)
|
|
136
205
|
return
|
|
137
206
|
}
|
|
138
207
|
|
|
208
|
+
if (typeof body.model === "string" && body.model !== this._modelName()) return
|
|
209
|
+
|
|
139
210
|
if (body.action === "destroy") {
|
|
140
|
-
if (
|
|
211
|
+
if (body.id === undefined || body.id === null) return
|
|
212
|
+
|
|
213
|
+
const FrontendModelController = await this._frontendModelControllerClass()
|
|
214
|
+
const authorized = await this._destroyEventIsAuthorized(
|
|
215
|
+
body,
|
|
216
|
+
FrontendModelController,
|
|
217
|
+
meta?.broadcastParams?.destroyAuthorizationRecord
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if (!authorized) return
|
|
221
|
+
|
|
222
|
+
if (!hasEventFilters || this._hasDestroyEventDelivery() || this._hasUnfilteredEventDelivery()) {
|
|
223
|
+
this.sendMessage({
|
|
224
|
+
action: body.action,
|
|
225
|
+
id: body.id,
|
|
226
|
+
...(typeof body.model === "string" ? {model: body.model} : {})
|
|
227
|
+
}, meta)
|
|
228
|
+
}
|
|
141
229
|
return
|
|
142
230
|
}
|
|
143
231
|
|
|
@@ -147,34 +235,42 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
147
235
|
}
|
|
148
236
|
|
|
149
237
|
const FrontendModelController = await this._frontendModelControllerClass()
|
|
150
|
-
const matchedEventFilterKeys =
|
|
238
|
+
const matchedEventFilterKeys = hasEventFilters
|
|
239
|
+
? await this._matchedEventFilterKeysForEventId(body.id, FrontendModelController)
|
|
240
|
+
: []
|
|
241
|
+
const isIdentityTransition = body.action === "update" && body.previousId !== undefined && body.previousId !== null
|
|
151
242
|
|
|
152
|
-
if (hasEventFilters && matchedEventFilterKeys.length === 0 && !this._hasUnfilteredEventDelivery()) {
|
|
243
|
+
if (hasEventFilters && matchedEventFilterKeys.length === 0 && !this._hasUnfilteredEventDelivery() && !isIdentityTransition) {
|
|
153
244
|
return
|
|
154
245
|
}
|
|
155
246
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
247
|
+
const projectedRecord = await this._projectedRecordForEventId(body.id, FrontendModelController)
|
|
248
|
+
|
|
249
|
+
if (!projectedRecord) {
|
|
250
|
+
if (isIdentityTransition) {
|
|
251
|
+
this.sendMessage({
|
|
252
|
+
action: body.action,
|
|
253
|
+
id: body.id,
|
|
254
|
+
...(hasEventFilters ? {matchedEventFilterKeys} : {}),
|
|
255
|
+
...(typeof body.model === "string" ? {model: body.model} : {}),
|
|
256
|
+
previousId: body.previousId
|
|
257
|
+
}, meta)
|
|
166
258
|
}
|
|
259
|
+
return
|
|
260
|
+
}
|
|
167
261
|
|
|
168
|
-
|
|
262
|
+
const configuration = this.session.configuration
|
|
169
263
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
264
|
+
if (!configuration) {
|
|
265
|
+
throw new Error("Frontend model websocket channel has no configuration for transport serialization")
|
|
266
|
+
}
|
|
173
267
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Deliver body.
|
|
270
|
+
* @type {FrontendModelLifecycleBroadcastBody} */
|
|
271
|
+
let deliverBody = {
|
|
272
|
+
...body,
|
|
273
|
+
record: /** @type {import("./query.js").FrontendModelTransportValue} */ (serializeFrontendModelTransportValue(projectedRecord, transportSerializationOptionsForConfiguration(configuration)))
|
|
178
274
|
}
|
|
179
275
|
|
|
180
276
|
if (hasEventFilters) {
|
|
@@ -187,6 +283,103 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
187
283
|
this.sendMessage(deliverBody, meta)
|
|
188
284
|
}
|
|
189
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Requires a resync for relevant destroy events because their authorization
|
|
288
|
+
* snapshots are intentionally excluded from the persisted replay payload.
|
|
289
|
+
* @param {import("../http-server/websocket-channel.js").WebsocketJsonValue} body - Persisted broadcast payload.
|
|
290
|
+
* @returns {boolean} - Whether replay cannot safely authorize this event.
|
|
291
|
+
*/
|
|
292
|
+
_requiresReplayGap(body) {
|
|
293
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false
|
|
294
|
+
if (!("action" in body) || body.action !== "destroy") return false
|
|
295
|
+
if (!("id" in body) || body.id === undefined || body.id === null) return false
|
|
296
|
+
|
|
297
|
+
return !("model" in body) || typeof body.model !== "string" || body.model === this._modelName()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Checks a destroy against the subscriber's ordinary authorized query by
|
|
302
|
+
* replacing the deleted backing table with the captured pre-delete row. Values
|
|
303
|
+
* are quoted on this trusted database connection; no broadcast-provided SQL is run.
|
|
304
|
+
* @param {FrontendModelLifecycleBroadcastBody} body - Destroy broadcast body.
|
|
305
|
+
* @param {typeof import("../frontend-model-controller.js").default} FrontendModelController - Server-side frontend-model controller class.
|
|
306
|
+
* @param {import("./query.js").FrontendModelTransportValue | undefined} destroyAuthorizationRecord - Server-only pre-delete record from live broadcast metadata.
|
|
307
|
+
* @returns {Promise<boolean>} - Whether the subscriber could read the record before deletion.
|
|
308
|
+
*/
|
|
309
|
+
async _destroyEventIsAuthorized(body, FrontendModelController, destroyAuthorizationRecord) {
|
|
310
|
+
const id = body.id
|
|
311
|
+
|
|
312
|
+
if (id === undefined || id === null || !isDestroyAuthorizationRecord(destroyAuthorizationRecord)) return false
|
|
313
|
+
|
|
314
|
+
return await this._withEventTenant(id, async () => {
|
|
315
|
+
const controller = this._frontendModelController(FrontendModelController)
|
|
316
|
+
|
|
317
|
+
await controller.ensureFrontendModelClassInitialized()
|
|
318
|
+
|
|
319
|
+
const ModelClass = controller.frontendModelClass()
|
|
320
|
+
const primaryKey = controller.frontendModelPrimaryKey()
|
|
321
|
+
const ruleQueryFactory = () => this._destroyAuthorizationQuery(ModelClass, destroyAuthorizationRecord)
|
|
322
|
+
const query = controller.frontendModelAuthorizedQuery("find", {ruleQueryFactory})
|
|
323
|
+
|
|
324
|
+
this._applyDestroyAuthorizationRecordToQuery(query, ModelClass, destroyAuthorizationRecord)
|
|
325
|
+
query.where({
|
|
326
|
+
[ModelClass.tableName()]: frontendModelPrimaryKeyDatabaseConditions(ModelClass, primaryKey, id)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
return Boolean(await query.first())
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Builds a backing-model query whose source is the captured pre-delete row.
|
|
335
|
+
* @param {typeof import("../database/record/index.js").default} ModelClass - Backing model class.
|
|
336
|
+
* @param {DestroyAuthorizationRecord} destroyAuthorizationRecord - Captured pre-delete record.
|
|
337
|
+
* @returns {import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} - One-row model query.
|
|
338
|
+
*/
|
|
339
|
+
_destroyAuthorizationQuery(ModelClass, destroyAuthorizationRecord) {
|
|
340
|
+
const query = ModelClass._newQuery()
|
|
341
|
+
|
|
342
|
+
this._applyDestroyAuthorizationRecordToQuery(query, ModelClass, destroyAuthorizationRecord)
|
|
343
|
+
|
|
344
|
+
return query
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Replaces a query's backing table with a safely quoted one-row derived table.
|
|
349
|
+
* @param {import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} query - Query to update.
|
|
350
|
+
* @param {typeof import("../database/record/index.js").default} ModelClass - Backing model class.
|
|
351
|
+
* @param {DestroyAuthorizationRecord} destroyAuthorizationRecord - Captured pre-delete record.
|
|
352
|
+
* @returns {void}
|
|
353
|
+
*/
|
|
354
|
+
_applyDestroyAuthorizationRecordToQuery(query, ModelClass, destroyAuthorizationRecord) {
|
|
355
|
+
const selectedColumns = Object.entries(destroyAuthorizationRecord).map(([columnName, serializedValue]) => {
|
|
356
|
+
const value = isDestroyAuthorizationBinary(serializedValue)
|
|
357
|
+
? Buffer.from(serializedValue.value)
|
|
358
|
+
: deserializeFrontendModelTransportValue(serializedValue)
|
|
359
|
+
const column = ModelClass.getColumnsHash()[columnName]
|
|
360
|
+
|
|
361
|
+
if (!column) throw new Error(`Cannot authorize a destroyed ${ModelClass.name} with unknown column ${columnName}`)
|
|
362
|
+
const quotedValue = query.driver.getType() == "pgsql" && column.getType() === "ARRAY" && Array.isArray(value)
|
|
363
|
+
? pgsqlArrayValueSql(query.driver, value)
|
|
364
|
+
: value === null ? "NULL" : query.driver.quote(value)
|
|
365
|
+
|
|
366
|
+
const selectedValue = query.driver.getType() == "pgsql"
|
|
367
|
+
? `CAST(${quotedValue} AS ${column.getDatabaseType()})`
|
|
368
|
+
: quotedValue
|
|
369
|
+
|
|
370
|
+
return `${selectedValue} AS ${query.driver.quoteColumn(columnName)}`
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
if (selectedColumns.length === 0) {
|
|
374
|
+
throw new Error(`Cannot authorize a destroyed ${ModelClass.name} without captured attributes`)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const froms = query.getFroms()
|
|
378
|
+
|
|
379
|
+
froms.splice(0, froms.length)
|
|
380
|
+
query.from(`(SELECT ${selectedColumns.join(", ")}) AS ${query.driver.quoteTable(ModelClass.tableName())}`)
|
|
381
|
+
}
|
|
382
|
+
|
|
190
383
|
/**
|
|
191
384
|
* Runs matches.
|
|
192
385
|
* @param {Record<string, import("./query.js").FrontendModelTransportValue>} broadcastParams - Params from `broadcastToChannel`.
|
|
@@ -227,19 +420,6 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
227
420
|
: null
|
|
228
421
|
}
|
|
229
422
|
|
|
230
|
-
/**
|
|
231
|
-
* Runs has projection params.
|
|
232
|
-
* @returns {boolean} - Whether this subscription requested per-event record projection.
|
|
233
|
-
*/
|
|
234
|
-
_hasProjectionParams() {
|
|
235
|
-
return this.params.select !== undefined
|
|
236
|
-
|| this.params.selectsExtra !== undefined
|
|
237
|
-
|| this.params.preload !== undefined
|
|
238
|
-
|| this.params.withCount !== undefined
|
|
239
|
-
|| this.params.abilities !== undefined
|
|
240
|
-
|| this.params.queryData !== undefined
|
|
241
|
-
}
|
|
242
|
-
|
|
243
423
|
/**
|
|
244
424
|
* Runs has event filter params.
|
|
245
425
|
* @returns {boolean} - Whether this subscription requested event query filters.
|
|
@@ -359,7 +539,7 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
359
539
|
|
|
360
540
|
/**
|
|
361
541
|
* Resolves tenant for event.
|
|
362
|
-
* @param {
|
|
542
|
+
* @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} id - Event record id.
|
|
363
543
|
* @returns {Promise<ReturnType<typeof JSON.parse>>} - Resolved tenant.
|
|
364
544
|
*/
|
|
365
545
|
async _resolveEventTenant(id) {
|
|
@@ -390,7 +570,7 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
390
570
|
* the subscriber's own tenant/ability scope. When no tenant resolves (non-multitenant configs), the
|
|
391
571
|
* callback runs directly so the ambient context is preserved.
|
|
392
572
|
* @template T
|
|
393
|
-
* @param {
|
|
573
|
+
* @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} id - Event record id.
|
|
394
574
|
* @param {() => Promise<T>} callback - Authorized-query callback.
|
|
395
575
|
* @returns {Promise<T>} - Callback result.
|
|
396
576
|
*/
|
|
@@ -412,30 +592,9 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
412
592
|
})
|
|
413
593
|
}
|
|
414
594
|
|
|
415
|
-
/**
|
|
416
|
-
* Whether the broadcast record is within the subscriber's authenticated ability scope. Used to gate
|
|
417
|
-
* unfiltered/unprojected create/update delivery so a scoped token never receives a record it cannot read.
|
|
418
|
-
* @param {string | number} id - Event record id.
|
|
419
|
-
* @param {typeof import("../frontend-model-controller.js").default} FrontendModelController - Server-side frontend-model controller class.
|
|
420
|
-
* @returns {Promise<boolean>} True when the record is readable by this subscription.
|
|
421
|
-
*/
|
|
422
|
-
async _eventIsAccessible(id, FrontendModelController) {
|
|
423
|
-
return await this._withEventTenant(id, async () => {
|
|
424
|
-
const controller = this._frontendModelController(FrontendModelController)
|
|
425
|
-
|
|
426
|
-
await controller.ensureFrontendModelClassInitialized()
|
|
427
|
-
|
|
428
|
-
const ModelClass = controller.frontendModelClass()
|
|
429
|
-
const primaryKey = ModelClass.primaryKey()
|
|
430
|
-
const query = controller.frontendModelAuthorizedQuery("find").where({[ModelClass.tableName()]: {[primaryKey]: id}})
|
|
431
|
-
|
|
432
|
-
return Boolean(await query.first())
|
|
433
|
-
})
|
|
434
|
-
}
|
|
435
|
-
|
|
436
595
|
/**
|
|
437
596
|
* Runs matched event filter keys for event id.
|
|
438
|
-
* @param {
|
|
597
|
+
* @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} id - Event record id.
|
|
439
598
|
* @param {typeof import("../frontend-model-controller.js").default} FrontendModelController - Server-side frontend-model controller class.
|
|
440
599
|
* @returns {Promise<string[]>} - Event filter keys matched by the record.
|
|
441
600
|
*/
|
|
@@ -463,7 +622,7 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
463
622
|
* @param {object} args - Filter args.
|
|
464
623
|
* @param {typeof import("../frontend-model-controller.js").default} args.FrontendModelController - Server-side frontend-model controller class.
|
|
465
624
|
* @param {import("./query.js").FrontendModelEventFilterPayloadEntry} args.eventFilter - Event filter payload.
|
|
466
|
-
* @param {
|
|
625
|
+
* @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} args.id - Event record id.
|
|
467
626
|
* @returns {Promise<boolean>} Whether the record matches the filter.
|
|
468
627
|
*/
|
|
469
628
|
async _eventMatchesFilter({FrontendModelController, eventFilter, id}) {
|
|
@@ -477,12 +636,14 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
477
636
|
await controller.ensureFrontendModelClassInitialized()
|
|
478
637
|
|
|
479
638
|
const ModelClass = controller.frontendModelClass()
|
|
480
|
-
const primaryKey =
|
|
639
|
+
const primaryKey = controller.frontendModelPrimaryKey()
|
|
481
640
|
const where = controller.frontendModelWhere()
|
|
482
641
|
const joins = controller.frontendModelJoins()
|
|
483
642
|
// Start from the subscriber's authorized scope so a filter can only ever match records the
|
|
484
643
|
// subscription's ability permits to read.
|
|
485
|
-
let query = controller.frontendModelAuthorizedQuery("find").where({
|
|
644
|
+
let query = controller.frontendModelAuthorizedQuery("find").where({
|
|
645
|
+
[ModelClass.tableName()]: frontendModelPrimaryKeyDatabaseConditions(ModelClass, primaryKey, id)
|
|
646
|
+
})
|
|
486
647
|
|
|
487
648
|
if (where) controller.applyFrontendModelWhere({query, where})
|
|
488
649
|
if (joins) controller.applyFrontendModelJoins({joins, query})
|
|
@@ -497,7 +658,7 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
497
658
|
|
|
498
659
|
/**
|
|
499
660
|
* Runs projected record for event id.
|
|
500
|
-
* @param {
|
|
661
|
+
* @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} id - Event record id.
|
|
501
662
|
* @param {typeof import("../frontend-model-controller.js").default} FrontendModelController - Server-side frontend-model controller class.
|
|
502
663
|
* @returns {Promise<Record<string, import("./query.js").FrontendModelTransportValue> | null>} - Serialized projected record.
|
|
503
664
|
*/
|
|
@@ -508,10 +669,12 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
508
669
|
await controller.ensureFrontendModelClassInitialized()
|
|
509
670
|
|
|
510
671
|
const ModelClass = controller.frontendModelClass()
|
|
511
|
-
const primaryKey =
|
|
672
|
+
const primaryKey = controller.frontendModelPrimaryKey()
|
|
512
673
|
// Reload through the subscriber's authorized scope so projected records are only ever sent for
|
|
513
674
|
// rows the subscription's ability permits to read.
|
|
514
|
-
let query = controller.frontendModelAuthorizedQuery("find").where({
|
|
675
|
+
let query = controller.frontendModelAuthorizedQuery("find").where({
|
|
676
|
+
[ModelClass.tableName()]: frontendModelPrimaryKeyDatabaseConditions(ModelClass, primaryKey, id)
|
|
677
|
+
})
|
|
515
678
|
const preload = controller.frontendModelPreload()
|
|
516
679
|
|
|
517
680
|
if (preload) query = query.preload(preload)
|