velocious 1.0.465 → 1.0.467
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/build/configuration.js +2 -2
- package/build/database/pool/async-tracked-multi-connection.js +7 -2
- package/build/database/pool/single-multi-use.js +38 -0
- package/build/environment-handlers/node/cli/commands/lint/relationships.js +95 -2
- package/build/frontend-model-controller.js +162 -56
- package/build/frontend-models/query.js +55 -33
- package/build/src/configuration.js +3 -3
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +9 -3
- package/build/src/database/pool/single-multi-use.d.ts +1 -0
- package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
- package/build/src/database/pool/single-multi-use.js +34 -1
- package/build/src/environment-handlers/node/cli/commands/lint/relationships.d.ts +29 -0
- package/build/src/environment-handlers/node/cli/commands/lint/relationships.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/lint/relationships.js +83 -3
- package/build/src/frontend-model-controller.d.ts +45 -5
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +155 -57
- package/build/src/frontend-models/query.d.ts +8 -0
- package/build/src/frontend-models/query.d.ts.map +1 -1
- package/build/src/frontend-models/query.js +53 -34
- package/build/src/utils/ransack.d.ts +11 -3
- package/build/src/utils/ransack.d.ts.map +1 -1
- package/build/src/utils/ransack.js +42 -23
- package/build/utils/ransack.js +44 -22
- package/package.json +1 -1
- package/src/configuration.js +2 -2
- package/src/database/pool/async-tracked-multi-connection.js +7 -2
- package/src/database/pool/single-multi-use.js +38 -0
- package/src/environment-handlers/node/cli/commands/lint/relationships.js +95 -2
- package/src/frontend-model-controller.js +162 -56
- package/src/frontend-models/query.js +55 -33
- package/src/types/external-modules.d.ts +13 -0
- package/src/utils/ransack.js +44 -22
package/README.md
CHANGED
|
@@ -578,6 +578,7 @@ Use `await FrontendModelBase.waitForIdle()` when a test harness or app lifecycle
|
|
|
578
578
|
Frontend-model HTTP requests always use `credentials: "include"` so shared custom commands can set session cookies without app-level transport overrides.
|
|
579
579
|
|
|
580
580
|
Unexpected frontend-model endpoint failures stay client-safe in production with `errorMessage: "Request failed."`.
|
|
581
|
+
Invalid client query descriptors, such as unknown `select`, `where`, `search`, `joins`, `group`, `sort`, `pluck`, or Ransack attributes, return the specific frontend-model query error message with `velocious.code: "frontend-model-query-error"` and are not emitted as framework errors.
|
|
581
582
|
In `development` and `test`, Velocious also includes `debugErrorClass`, `debugErrorMessage`, and `debugBacktrace` fields so browser/system-test failures are easier to diagnose without exposing those details in production.
|
|
582
583
|
Other non-production environments, such as `staging`, keep the same client-safe default unless you explicitly opt in with `exposeInternalErrorsToClients: true`:
|
|
583
584
|
|
|
@@ -1619,7 +1620,7 @@ configuration.getErrorEvents().on("all-error", ({error, errorType}) => {
|
|
|
1619
1620
|
})
|
|
1620
1621
|
```
|
|
1621
1622
|
|
|
1622
|
-
Genuinely unexpected frontend-model command failures reach this bus too. The frontend-model controller catches them to return a client-safe `Request failed.` response, but it also emits them as `framework-error`/`all-error` (with `context.frontendModelEndpoint === true`) so they are reported instead of being silently swallowed. Expected user-flow errors are excluded: validation failures are forwarded with their real message (for example `Name can't be blank`)
|
|
1623
|
+
Genuinely unexpected frontend-model command failures reach this bus too. The frontend-model controller catches them to return a client-safe `Request failed.` response, but it also emits them as `framework-error`/`all-error` (with `context.frontendModelEndpoint === true`) so they are reported instead of being silently swallowed. Expected user-flow errors are excluded: validation failures are forwarded with their real message (for example `Name can't be blank`), invalid client query descriptors are returned as frontend-model query errors, and `error.velocious`-annotated / `safeToExpose` / `errorType`-marked errors keep their expected-error status — none of these reach the error bus.
|
|
1623
1624
|
|
|
1624
1625
|
## Use the Websocket client API (HTTP-like)
|
|
1625
1626
|
|
package/build/configuration.js
CHANGED
|
@@ -2230,8 +2230,8 @@ export default class VelociousConfiguration {
|
|
|
2230
2230
|
|
|
2231
2231
|
if (!matches) continue
|
|
2232
2232
|
|
|
2233
|
-
this.withoutCurrentConnectionContexts(() => {
|
|
2234
|
-
|
|
2233
|
+
void this.withoutCurrentConnectionContexts(() => {
|
|
2234
|
+
return Promise
|
|
2235
2235
|
.resolve()
|
|
2236
2236
|
.then(() => this._deliverWebsocketChannelBroadcast(subscription, body, {eventId: meta?.eventId}))
|
|
2237
2237
|
.catch((error) => {
|
|
@@ -6,6 +6,7 @@ import BasePool, {POOL_CONFIGURATION_KEY} from "./base.js"
|
|
|
6
6
|
export const CLOSED_CONNECTION = Symbol("velociousClosedConnection")
|
|
7
7
|
const IDLE_CONNECTION_CHECKED_IN_AT = Symbol("velociousIdleConnectionCheckedInAt")
|
|
8
8
|
const CONNECTION_CHECKED_OUT_AT = Symbol("velociousConnectionCheckedOutAt")
|
|
9
|
+
const SUPPRESSED_CONNECTION_CONTEXT = Symbol("velociousSuppressedConnectionContext")
|
|
9
10
|
const DEFAULT_MAX_CONNECTIONS = 10
|
|
10
11
|
const DEFAULT_IDLE_TIMEOUT_MILLIS = 5000
|
|
11
12
|
const DEFAULT_CHECKOUT_TIMEOUT_MILLIS = 10000
|
|
@@ -103,7 +104,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
103
104
|
* Runs a callback without the inherited current connection context.
|
|
104
105
|
* @type {(callback: () => ?) => ?}
|
|
105
106
|
*/
|
|
106
|
-
const withoutCurrentConnectionContext = (callback) => this.asyncLocalStorage.run(
|
|
107
|
+
const withoutCurrentConnectionContext = (callback) => this.asyncLocalStorage.run(SUPPRESSED_CONNECTION_CONTEXT, callback)
|
|
107
108
|
this._withoutCurrentConnectionContext = withoutCurrentConnectionContext
|
|
108
109
|
}
|
|
109
110
|
|
|
@@ -720,6 +721,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
720
721
|
const id = this.asyncLocalStorage.getStore()
|
|
721
722
|
|
|
722
723
|
if (id === undefined) return this.currentFallbackConnectionOrFail()
|
|
724
|
+
if (id === SUPPRESSED_CONNECTION_CONTEXT) return this.currentFallbackConnectionOrFail()
|
|
723
725
|
|
|
724
726
|
this.ensureConnectionIsInUse(id)
|
|
725
727
|
|
|
@@ -815,6 +817,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
815
817
|
getCurrentContextConnection() {
|
|
816
818
|
const id = this.asyncLocalStorage.getStore()
|
|
817
819
|
|
|
820
|
+
if (id === SUPPRESSED_CONNECTION_CONTEXT) return undefined
|
|
818
821
|
if (id === undefined) return this._testSharedConnection
|
|
819
822
|
|
|
820
823
|
return this.getCurrentConnection()
|
|
@@ -825,7 +828,9 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
825
828
|
* @returns {boolean} - Whether nested code can reuse the current connection context.
|
|
826
829
|
*/
|
|
827
830
|
hasCurrentConnectionContext() {
|
|
828
|
-
|
|
831
|
+
const id = this.asyncLocalStorage.getStore()
|
|
832
|
+
|
|
833
|
+
return id !== undefined && id !== SUPPRESSED_CONNECTION_CONTEXT
|
|
829
834
|
}
|
|
830
835
|
|
|
831
836
|
/**
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import BasePool from "./base.js"
|
|
4
4
|
|
|
5
5
|
export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
|
|
6
|
+
suppressedConnectionContextCount = 0
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Runs checkin.
|
|
8
10
|
* @param {import("../drivers/base.js").default} connection - Connection.
|
|
@@ -58,6 +60,32 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Runs without current connection context.
|
|
65
|
+
* @template T
|
|
66
|
+
* @param {() => T} callback - Callback to run without the shared current connection.
|
|
67
|
+
* @returns {T} - Callback result.
|
|
68
|
+
*/
|
|
69
|
+
withoutCurrentConnectionContext(callback) {
|
|
70
|
+
this.suppressedConnectionContextCount += 1
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = callback()
|
|
74
|
+
|
|
75
|
+
if (result instanceof Promise) {
|
|
76
|
+
return /** @type {T} */ (result.finally(() => {
|
|
77
|
+
this.suppressedConnectionContextCount -= 1
|
|
78
|
+
}))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.suppressedConnectionContextCount -= 1
|
|
82
|
+
return result
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.suppressedConnectionContextCount -= 1
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
/**
|
|
62
90
|
* Clears schema metadata cached by the reusable connection if it exists.
|
|
63
91
|
* @returns {void} - No return value.
|
|
@@ -97,9 +125,19 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
|
|
|
97
125
|
* @returns {import("../drivers/base.js").default | undefined} - The current context connection.
|
|
98
126
|
*/
|
|
99
127
|
getCurrentContextConnection() {
|
|
128
|
+
if (this.suppressedConnectionContextCount > 0) return undefined
|
|
129
|
+
|
|
100
130
|
return this.connection
|
|
101
131
|
}
|
|
102
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Returns whether the shared connection is available to the current execution context.
|
|
135
|
+
* @returns {boolean} - Whether nested code can reuse the shared connection.
|
|
136
|
+
*/
|
|
137
|
+
hasCurrentConnectionContext() {
|
|
138
|
+
return this.suppressedConnectionContextCount === 0
|
|
139
|
+
}
|
|
140
|
+
|
|
103
141
|
/**
|
|
104
142
|
* Runs get debug snapshot.
|
|
105
143
|
* @returns {import("./base.js").DatabasePoolDebugSnapshot} - Diagnostic snapshot for this pool.
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
import BaseCommand from "../../../../../cli/base-command.js"
|
|
4
4
|
import fs from "node:fs/promises"
|
|
5
5
|
import path from "node:path"
|
|
6
|
+
import requireContext from "require-context"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {(id: string) => {default: typeof import("../../../../../database/record/index.js").default}} ModelFileRequireContextIdFunctionType
|
|
10
|
+
* @typedef {ModelFileRequireContextIdFunctionType & {keys: () => string[]}} ModelFileRequireContextType
|
|
11
|
+
*/
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* Lints model relationships: every non-polymorphic belongs-to relationship should have an inverse
|
|
@@ -27,7 +33,9 @@ export default class VelociousCliCommandsLintRelationships extends BaseCommand {
|
|
|
27
33
|
// current configuration, so make this command's configuration the current one.
|
|
28
34
|
this.getConfiguration().setCurrent()
|
|
29
35
|
|
|
30
|
-
await this.
|
|
36
|
+
if (!await this._registerStaticModelFiles()) {
|
|
37
|
+
await this.getConfiguration().initializeModels()
|
|
38
|
+
}
|
|
31
39
|
|
|
32
40
|
const ignoredRelationships = await this._loadIgnoredRelationships()
|
|
33
41
|
const offences = []
|
|
@@ -66,7 +74,11 @@ export default class VelociousCliCommandsLintRelationships extends BaseCommand {
|
|
|
66
74
|
if (candidate.through) return false
|
|
67
75
|
|
|
68
76
|
try {
|
|
69
|
-
|
|
77
|
+
const candidateTargetModelClass = candidate.getTargetModelClass()
|
|
78
|
+
|
|
79
|
+
if (!candidateTargetModelClass) return false
|
|
80
|
+
|
|
81
|
+
return this._modelClassesMatch(candidateTargetModelClass, modelClass)
|
|
70
82
|
} catch {
|
|
71
83
|
// A has-many/has-one with an unresolvable target can't be the inverse of this belongs-to.
|
|
72
84
|
// It is reported separately when its own model's belongs-to relationships are linted.
|
|
@@ -97,6 +109,87 @@ export default class VelociousCliCommandsLintRelationships extends BaseCommand {
|
|
|
97
109
|
return {offences}
|
|
98
110
|
}
|
|
99
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Registers model classes from the conventional src/models directory without
|
|
114
|
+
* running the application's full database/server initialization.
|
|
115
|
+
* @returns {Promise<boolean>} Whether static model files were registered.
|
|
116
|
+
*/
|
|
117
|
+
async _registerStaticModelFiles() {
|
|
118
|
+
if (this.args.testing) return false
|
|
119
|
+
|
|
120
|
+
const modelsDirectory = path.join(this.directory(), "src/models")
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const stats = await fs.stat(modelsDirectory)
|
|
124
|
+
|
|
125
|
+
if (!stats.isDirectory()) return false
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (/** @type {NodeJS.ErrnoException} */ (error).code == "ENOENT") return false
|
|
128
|
+
|
|
129
|
+
throw error
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if ((await this._javascriptFilesInDirectory(modelsDirectory)).length === 0) return false
|
|
133
|
+
|
|
134
|
+
/** @type {ModelFileRequireContextType} */
|
|
135
|
+
const requireContextModels = requireContext(modelsDirectory, true, /^(.+)\.js$/)
|
|
136
|
+
|
|
137
|
+
const modelFileNames = requireContextModels.keys()
|
|
138
|
+
for (const fileName of modelFileNames) {
|
|
139
|
+
const modelClassImport = requireContextModels(fileName)
|
|
140
|
+
const modelClass = modelClassImport.default
|
|
141
|
+
|
|
142
|
+
if (!modelClass) {
|
|
143
|
+
throw new Error(`Model wasn't exported from: ${fileName}`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
modelClass.registerRecordClass({configuration: this.getConfiguration()})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Finds JavaScript files below a directory.
|
|
154
|
+
* @param {string} directory - Directory to scan.
|
|
155
|
+
* @returns {Promise<string[]>} JavaScript file paths.
|
|
156
|
+
*/
|
|
157
|
+
async _javascriptFilesInDirectory(directory) {
|
|
158
|
+
const filePaths = []
|
|
159
|
+
const entries = await fs.readdir(directory, {withFileTypes: true})
|
|
160
|
+
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
const entryPath = path.join(directory, entry.name)
|
|
163
|
+
|
|
164
|
+
if (entry.isDirectory()) {
|
|
165
|
+
filePaths.push(...await this._javascriptFilesInDirectory(entryPath))
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
170
|
+
filePaths.push(entryPath)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return filePaths
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Checks whether two model class objects describe the same registered model.
|
|
179
|
+
* @param {typeof import("../../../../../database/record/index.js").default} leftModelClass - Candidate target model class.
|
|
180
|
+
* @param {typeof import("../../../../../database/record/index.js").default} rightModelClass - Belongs-to source model class.
|
|
181
|
+
* @returns {boolean} Whether both model classes represent the same model identity.
|
|
182
|
+
*/
|
|
183
|
+
_modelClassesMatch(leftModelClass, rightModelClass) {
|
|
184
|
+
if (leftModelClass === rightModelClass) return true
|
|
185
|
+
// `translates()` creates an internal translation class; apps may also define
|
|
186
|
+
// a concrete class for the same model/table so generated code has a stable
|
|
187
|
+
// file and type name.
|
|
188
|
+
if (leftModelClass.getModelName() != rightModelClass.getModelName()) return false
|
|
189
|
+
|
|
190
|
+
return leftModelClass.tableName() == rightModelClass.tableName()
|
|
191
|
+
}
|
|
192
|
+
|
|
100
193
|
/**
|
|
101
194
|
* Loads the ignored relationship keys from the lint config file. The file is optional; when the
|
|
102
195
|
* default path doesn't exist, no relationships are ignored. An explicitly passed `--config` path
|