velocious 1.0.466 → 1.0.468
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/build/configuration.js +2 -2
- package/build/database/pool/async-tracked-multi-connection.js +7 -2
- package/build/database/pool/single-multi-use.js +38 -0
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +155 -35
- package/build/frontend-model-controller.js +209 -57
- package/build/frontend-models/query.js +55 -33
- package/build/src/configuration.js +3 -3
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +9 -3
- package/build/src/database/pool/single-multi-use.d.ts +1 -0
- package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
- package/build/src/database/pool/single-multi-use.js +34 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +82 -9
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +137 -35
- package/build/src/frontend-model-controller.d.ts +61 -5
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +190 -58
- package/build/src/frontend-models/query.d.ts +8 -0
- package/build/src/frontend-models/query.d.ts.map +1 -1
- package/build/src/frontend-models/query.js +53 -34
- package/build/src/utils/ransack.d.ts +11 -3
- package/build/src/utils/ransack.d.ts.map +1 -1
- package/build/src/utils/ransack.js +42 -23
- package/build/utils/ransack.js +44 -22
- package/package.json +1 -1
- package/src/configuration.js +2 -2
- package/src/database/pool/async-tracked-multi-connection.js +7 -2
- package/src/database/pool/single-multi-use.js +38 -0
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +155 -35
- package/src/frontend-model-controller.js +209 -57
- package/src/frontend-models/query.js +55 -33
- package/src/utils/ransack.js +44 -22
package/build/utils/ransack.js
CHANGED
|
@@ -4,6 +4,28 @@ import * as inflection from "inflection"
|
|
|
4
4
|
import {isPlainObject} from "is-plain-object"
|
|
5
5
|
import {resolveFrontendModelClass} from "../frontend-models/model-registry.js"
|
|
6
6
|
|
|
7
|
+
/** Error raised when a Ransack descriptor is malformed. */
|
|
8
|
+
export class RansackQueryError extends Error {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a Ransack query error.
|
|
11
|
+
* @param {string} message - Error message.
|
|
12
|
+
*/
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message)
|
|
15
|
+
|
|
16
|
+
this.name = "RansackQueryError"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds a Ransack query error.
|
|
22
|
+
* @param {string} message - Error message.
|
|
23
|
+
* @returns {RansackQueryError} - Ransack query error.
|
|
24
|
+
*/
|
|
25
|
+
function ransackQueryError(message) {
|
|
26
|
+
return new RansackQueryError(message)
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
/**
|
|
8
30
|
* RansackPredicate type.
|
|
9
31
|
* @typedef {"cont" | "end" | "eq" | "gt" | "gteq" | "in" | "lt" | "lteq" | "not_eq" | "not_in" | "null" | "start"} RansackPredicate
|
|
@@ -76,7 +98,7 @@ export function normalizeRansackParams(modelClass, params) {
|
|
|
76
98
|
*/
|
|
77
99
|
export function normalizeRansackGroup(modelClass, params) {
|
|
78
100
|
if (!isPlainObject(params)) {
|
|
79
|
-
throw
|
|
101
|
+
throw ransackQueryError(`ransack params must be a plain object, got: ${typeof params}`)
|
|
80
102
|
}
|
|
81
103
|
|
|
82
104
|
/**
|
|
@@ -124,7 +146,7 @@ function normalizeSimpleRansackCondition({key, modelClass, rawValue}) {
|
|
|
124
146
|
const parsedKey = parseRansackKey(key)
|
|
125
147
|
|
|
126
148
|
if (!parsedKey) {
|
|
127
|
-
throw
|
|
149
|
+
throw ransackQueryError(`Unsupported ransack predicate in key: ${key}`)
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
const value = normalizeRansackValue({
|
|
@@ -159,17 +181,17 @@ function normalizeAdvancedRansackConditions({modelClass, value}) {
|
|
|
159
181
|
|
|
160
182
|
for (const entry of normalizeRansackCollection(value, "conditions")) {
|
|
161
183
|
if (!isPlainObject(entry)) {
|
|
162
|
-
throw
|
|
184
|
+
throw ransackQueryError(`Ransack condition entries must be plain objects, got: ${typeof entry}`)
|
|
163
185
|
}
|
|
164
186
|
|
|
165
187
|
const predicateValue = entry.p
|
|
166
188
|
|
|
167
189
|
if (typeof predicateValue !== "string") {
|
|
168
|
-
throw
|
|
190
|
+
throw ransackQueryError("Ransack condition predicate must be a string")
|
|
169
191
|
}
|
|
170
192
|
|
|
171
193
|
if (!supportedPredicates.includes(predicateValue)) {
|
|
172
|
-
throw
|
|
194
|
+
throw ransackQueryError(`Unsupported ransack predicate in condition: ${predicateValue}`)
|
|
173
195
|
}
|
|
174
196
|
|
|
175
197
|
const predicate = /** @type {RansackPredicate} */ (predicateValue)
|
|
@@ -206,7 +228,7 @@ function normalizeAdvancedRansackGroups({modelClass, value}) {
|
|
|
206
228
|
|
|
207
229
|
for (const entry of normalizeRansackCollection(value, "groupings")) {
|
|
208
230
|
if (!isPlainObject(entry)) {
|
|
209
|
-
throw
|
|
231
|
+
throw ransackQueryError(`Ransack grouping entries must be plain objects, got: ${typeof entry}`)
|
|
210
232
|
}
|
|
211
233
|
|
|
212
234
|
groupings.push(normalizeRansackGroup(modelClass, entry))
|
|
@@ -225,7 +247,7 @@ function normalizeRansackCombinator(value, defaultValue) {
|
|
|
225
247
|
if (value === undefined || value === null || value === "") return defaultValue
|
|
226
248
|
if (value === "and" || value === "or") return value
|
|
227
249
|
|
|
228
|
-
throw
|
|
250
|
+
throw ransackQueryError(`Invalid ransack combinator: ${String(value)}`)
|
|
229
251
|
}
|
|
230
252
|
|
|
231
253
|
/**
|
|
@@ -253,7 +275,7 @@ function normalizeRansackCollection(value, name) {
|
|
|
253
275
|
.map((key) => value[key])
|
|
254
276
|
}
|
|
255
277
|
|
|
256
|
-
throw
|
|
278
|
+
throw ransackQueryError(`Ransack ${name} must be an array or object, got: ${typeof value}`)
|
|
257
279
|
}
|
|
258
280
|
|
|
259
281
|
/**
|
|
@@ -292,7 +314,7 @@ function resolveRansackAttributesFromAdvancedValue({modelClass, value}) {
|
|
|
292
314
|
}
|
|
293
315
|
|
|
294
316
|
if (attributes.length < 1) {
|
|
295
|
-
throw
|
|
317
|
+
throw ransackQueryError("Ransack condition must include at least one attribute")
|
|
296
318
|
}
|
|
297
319
|
|
|
298
320
|
return attributes
|
|
@@ -314,7 +336,7 @@ function normalizeAdvancedAttributeValues(value) {
|
|
|
314
336
|
return normalizeRansackCollection(value, "attributes").map((entry) => normalizeAdvancedAttributeValue(entry))
|
|
315
337
|
}
|
|
316
338
|
|
|
317
|
-
throw
|
|
339
|
+
throw ransackQueryError(`Ransack condition attributes must be strings, arrays, or objects, got: ${typeof value}`)
|
|
318
340
|
}
|
|
319
341
|
|
|
320
342
|
/**
|
|
@@ -329,7 +351,7 @@ function normalizeAdvancedAttributeValue(value) {
|
|
|
329
351
|
return value.name
|
|
330
352
|
}
|
|
331
353
|
|
|
332
|
-
throw
|
|
354
|
+
throw ransackQueryError(`Ransack condition attribute entries must be strings or {name}, got: ${typeof value}`)
|
|
333
355
|
}
|
|
334
356
|
|
|
335
357
|
/**
|
|
@@ -346,7 +368,7 @@ function resolveRansackAttributes({modelClass, value}) {
|
|
|
346
368
|
const attributeName = resolveAttributeName({modelClass: targetModelClass, value: resolvedPath.attributeValue})
|
|
347
369
|
|
|
348
370
|
if (!attributeName) {
|
|
349
|
-
throw
|
|
371
|
+
throw ransackQueryError(`Unknown ransack attribute "${resolvedPath.attributeValue}" for ${targetModelClass.name}`)
|
|
350
372
|
}
|
|
351
373
|
|
|
352
374
|
return {
|
|
@@ -370,7 +392,7 @@ function modelClassAtPath({modelClass, path}) {
|
|
|
370
392
|
const relationship = relationshipEntries(currentModelClass)[relationshipName]
|
|
371
393
|
|
|
372
394
|
if (!relationship) {
|
|
373
|
-
throw
|
|
395
|
+
throw ransackQueryError(`Unknown ransack relationship "${relationshipName}" for ${currentModelClass.name}`)
|
|
374
396
|
}
|
|
375
397
|
|
|
376
398
|
currentModelClass = relationship.targetModelClass
|
|
@@ -412,7 +434,7 @@ function resolveRansackPath({modelClass, value}) {
|
|
|
412
434
|
}
|
|
413
435
|
|
|
414
436
|
if (remainingValue.length < 1) {
|
|
415
|
-
throw
|
|
437
|
+
throw ransackQueryError(`Invalid ransack key path: ${value}`)
|
|
416
438
|
}
|
|
417
439
|
|
|
418
440
|
return {
|
|
@@ -648,7 +670,7 @@ function parseRansackKey(key) {
|
|
|
648
670
|
const pathValue = key.slice(0, key.length - suffix.length)
|
|
649
671
|
|
|
650
672
|
if (pathValue.length < 1) {
|
|
651
|
-
throw
|
|
673
|
+
throw ransackQueryError(`Invalid ransack key: ${key}`)
|
|
652
674
|
}
|
|
653
675
|
|
|
654
676
|
return {
|
|
@@ -665,7 +687,7 @@ function parseRansackKey(key) {
|
|
|
665
687
|
const pathValue = key.slice(0, key.length - camelSuffix.length)
|
|
666
688
|
|
|
667
689
|
if (pathValue.length < 1) {
|
|
668
|
-
throw
|
|
690
|
+
throw ransackQueryError(`Invalid ransack key: ${key}`)
|
|
669
691
|
}
|
|
670
692
|
|
|
671
693
|
return {
|
|
@@ -750,7 +772,7 @@ function normalizeRansackBoolean(value) {
|
|
|
750
772
|
if (ransackFalseValues.has(value)) return false
|
|
751
773
|
if (ransackValueIsBlank(value)) return null
|
|
752
774
|
|
|
753
|
-
throw
|
|
775
|
+
throw ransackQueryError(`Invalid ransack boolean value: ${String(value)}`)
|
|
754
776
|
}
|
|
755
777
|
|
|
756
778
|
/**
|
|
@@ -789,10 +811,10 @@ function normalizeRansackArray(value) {
|
|
|
789
811
|
*/
|
|
790
812
|
|
|
791
813
|
/**
|
|
792
|
-
* Parses
|
|
793
|
-
* @param {RansackModelClass} modelClass - Model class for attribute
|
|
814
|
+
* Parses a ransack `s` sort string against model attributes.
|
|
815
|
+
* @param {RansackModelClass} modelClass - Model class for attribute lookup.
|
|
794
816
|
* @param {string} sortString - Ransack sort string (e.g., "name asc" or "name asc, createdAt desc").
|
|
795
|
-
* @returns {RansackSort[]} -
|
|
817
|
+
* @returns {RansackSort[]} - Parsed sort definitions.
|
|
796
818
|
*/
|
|
797
819
|
export function parseRansackSort(modelClass, sortString) {
|
|
798
820
|
const segments = sortString.split(",").map((segment) => segment.trim()).filter((segment) => segment.length > 0)
|
|
@@ -808,13 +830,13 @@ export function parseRansackSort(modelClass, sortString) {
|
|
|
808
830
|
const directionCandidate = parts.length > 1 ? parts[1].toLowerCase() : "asc"
|
|
809
831
|
|
|
810
832
|
if (directionCandidate !== "asc" && directionCandidate !== "desc") {
|
|
811
|
-
throw
|
|
833
|
+
throw ransackQueryError(`Invalid ransack sort direction "${directionCandidate}" in: ${segment}`)
|
|
812
834
|
}
|
|
813
835
|
|
|
814
836
|
const resolvedAttribute = resolveAttributeName({modelClass, value: columnCandidate})
|
|
815
837
|
|
|
816
838
|
if (!resolvedAttribute) {
|
|
817
|
-
throw
|
|
839
|
+
throw ransackQueryError(`Unknown ransack sort attribute "${columnCandidate}" for ${modelClass.name}`)
|
|
818
840
|
}
|
|
819
841
|
|
|
820
842
|
sorts.push({attribute: resolvedAttribute, direction: directionCandidate})
|
package/package.json
CHANGED
package/src/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.
|
|
@@ -29,7 +29,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
29
29
|
/** @type {Map<string, string> | null} */
|
|
30
30
|
_resourceMethodReturnTypes = null
|
|
31
31
|
|
|
32
|
-
/** @type {Map<string, string
|
|
32
|
+
/** @type {Map<string, Array<{name: string | null, type: string}>> | null} */
|
|
33
33
|
_resourceMethodParameterTypes = null
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -288,7 +288,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
288
288
|
}
|
|
289
289
|
const collectionCommands = modelConfig.collectionCommands
|
|
290
290
|
const memberCommands = modelConfig.memberCommands
|
|
291
|
-
const
|
|
291
|
+
const declaredCommandMetadata = modelConfig.commandMetadata || {}
|
|
292
|
+
const commandMetadata = await this.commandMetadataWithResourceJsDoc({
|
|
293
|
+
commandMetadata: declaredCommandMetadata,
|
|
294
|
+
commandNames: [...Object.keys(collectionCommands), ...Object.keys(memberCommands)],
|
|
295
|
+
resourceClass
|
|
296
|
+
})
|
|
292
297
|
const builtInCollectionCommandsAreDefault = builtInCollectionCommands.create === "create" && builtInCollectionCommands.index === "index"
|
|
293
298
|
const builtInMemberCommandsAreDefault = builtInMemberCommands.attach === "attach"
|
|
294
299
|
&& builtInMemberCommands.destroy === "destroy"
|
|
@@ -472,12 +477,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
472
477
|
fileContent += ` * @returns {Promise<${signature.returnType}>} - Command response.\n`
|
|
473
478
|
fileContent += " */\n"
|
|
474
479
|
fileContent += ` static async ${methodName}(${signature.parameters}) {\n`
|
|
475
|
-
fileContent +=
|
|
480
|
+
fileContent += ` return /** @type {${signature.returnType}} */ (await this.executeCustomCommand({\n`
|
|
476
481
|
fileContent += ` commandName: ${JSON.stringify(collectionCommands[methodName])},\n`
|
|
477
482
|
fileContent += ` commandType: ${JSON.stringify(collectionCommands[methodName])},\n`
|
|
478
483
|
fileContent += ` payload: ${className}.normalizeCustomCommandPayloadArguments(${signature.payloadArguments}),\n`
|
|
479
484
|
fileContent += " resourcePath: this.resourcePath()\n"
|
|
480
|
-
fileContent += " })\n"
|
|
485
|
+
fileContent += " }))\n"
|
|
481
486
|
fileContent += " }\n"
|
|
482
487
|
}
|
|
483
488
|
|
|
@@ -491,13 +496,13 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
491
496
|
fileContent += ` * @returns {Promise<${signature.returnType}>} - Command response.\n`
|
|
492
497
|
fileContent += " */\n"
|
|
493
498
|
fileContent += ` async ${methodName}(${signature.parameters}) {\n`
|
|
494
|
-
fileContent += ` return await ${className}.executeCustomCommand({\n`
|
|
499
|
+
fileContent += ` return /** @type {${signature.returnType}} */ (await ${className}.executeCustomCommand({\n`
|
|
495
500
|
fileContent += ` commandName: ${JSON.stringify(memberCommands[methodName])},\n`
|
|
496
501
|
fileContent += ` commandType: ${JSON.stringify(memberCommands[methodName])},\n`
|
|
497
502
|
fileContent += " memberId: this.primaryKeyValue(),\n"
|
|
498
503
|
fileContent += ` payload: ${className}.normalizeCustomCommandPayloadArguments(${signature.payloadArguments}),\n`
|
|
499
504
|
fileContent += ` resourcePath: ${className}.resourcePath()\n`
|
|
500
|
-
fileContent += " })\n"
|
|
505
|
+
fileContent += " }))\n"
|
|
501
506
|
fileContent += " }\n"
|
|
502
507
|
}
|
|
503
508
|
|
|
@@ -1406,11 +1411,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1406
1411
|
* @returns {string} - A frontend-resolvable attribute type.
|
|
1407
1412
|
*/
|
|
1408
1413
|
frontendResolvableAttributeJsDocType(jsDocType) {
|
|
1409
|
-
const safeTypeIdentifiers =
|
|
1410
|
-
"Array", "Date", "Exclude", "Extract", "FrontendModelAttributeValue", "FrontendModelTransportValue",
|
|
1411
|
-
"Map", "NonNullable", "Omit", "Partial", "Pick", "Promise", "Readonly", "ReadonlyArray", "Record",
|
|
1412
|
-
"Required", "ReturnType", "Set"
|
|
1413
|
-
])
|
|
1414
|
+
const safeTypeIdentifiers = this.frontendResolvableTypeIdentifiers()
|
|
1414
1415
|
const referencedIdentifiers = jsDocType.match(/[A-Z][A-Za-z0-9_$]*/g) || []
|
|
1415
1416
|
|
|
1416
1417
|
if (referencedIdentifiers.some((identifier) => !safeTypeIdentifiers.has(identifier))) {
|
|
@@ -1420,6 +1421,43 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1420
1421
|
return jsDocType
|
|
1421
1422
|
}
|
|
1422
1423
|
|
|
1424
|
+
/**
|
|
1425
|
+
* Capitalized identifiers a generated frontend model can resolve on its own
|
|
1426
|
+
* (primitives are lower-case and matched separately), so only framework-owned
|
|
1427
|
+
* and builtin generic types are listed.
|
|
1428
|
+
* @returns {Set<string>} - Frontend-resolvable type identifiers.
|
|
1429
|
+
*/
|
|
1430
|
+
frontendResolvableTypeIdentifiers() {
|
|
1431
|
+
return new Set([
|
|
1432
|
+
"Array", "Date", "Exclude", "Extract", "FrontendModelAttributeValue", "FrontendModelTransportValue",
|
|
1433
|
+
"Map", "NonNullable", "Omit", "Partial", "Pick", "Promise", "Readonly", "ReadonlyArray", "Record",
|
|
1434
|
+
"Required", "ReturnType", "Set"
|
|
1435
|
+
])
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Rewrites a custom-command param/return JSDoc type so it resolves in the generated
|
|
1440
|
+
* frontend model: each model-class (or otherwise non-frontend-resolvable) identifier
|
|
1441
|
+
* becomes `any` in place, keeping the surrounding object fields typed. A command-result
|
|
1442
|
+
* field holding a model arrives as a serialized transport value, so the consumer hydrates
|
|
1443
|
+
* it with `Model.instantiateFromResponse(...)`. The word boundary avoids matching the
|
|
1444
|
+
* capitalized middle of a camelCase property name (e.g. `adjustedTotalCents`).
|
|
1445
|
+
* @param {string} jsDocType - Resolved (Promise-unwrapped) JSDoc type.
|
|
1446
|
+
* @returns {string} - A frontend-resolvable JSDoc type.
|
|
1447
|
+
*/
|
|
1448
|
+
frontendResolvableCommandJsDocType(jsDocType) {
|
|
1449
|
+
const safeTypeIdentifiers = this.frontendResolvableTypeIdentifiers()
|
|
1450
|
+
|
|
1451
|
+
return jsDocType
|
|
1452
|
+
// A type that reaches into a backend source file via `import("...")` (optionally
|
|
1453
|
+
// `.Member` and `[]`) can't resolve from the generated frontend model and would type
|
|
1454
|
+
// a serialized result as a backend model instance, so collapse it to `any`.
|
|
1455
|
+
.replace(/import\(\s*["'][^"']*["']\s*\)(\s*\.\s*[A-Za-z_$][\w$]*)*(\s*\[\s*\])*/g, "any")
|
|
1456
|
+
// Remaining capitalized identifiers are model classes or otherwise non-resolvable
|
|
1457
|
+
// types; downgrade each in place so sibling scalar fields keep their real types.
|
|
1458
|
+
.replace(/\b[A-Z][A-Za-z0-9_$]*/g, (identifier) => safeTypeIdentifiers.has(identifier) ? identifier : "any")
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1423
1461
|
/**
|
|
1424
1462
|
* Builds the JSDoc param block, parameter list, payload-argument expression, and
|
|
1425
1463
|
* return type for a custom command method. With declared `args` each becomes a
|
|
@@ -1453,6 +1491,69 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1453
1491
|
}
|
|
1454
1492
|
}
|
|
1455
1493
|
|
|
1494
|
+
/**
|
|
1495
|
+
* Enriches custom-command metadata by deriving a command's typed args and return
|
|
1496
|
+
* type from the backend resource method's `@param`/`@returns` JSDoc when they are
|
|
1497
|
+
* not already declared in `resourceConfig`. Precedence: explicit `resourceConfig`
|
|
1498
|
+
* `{args, returnType}` wins, then the derived backend-method JSDoc, then the generic
|
|
1499
|
+
* default. Model-class identifiers in the derived types are downgraded to `any`
|
|
1500
|
+
* because the frontend receives a serialized record, not a model instance, which the
|
|
1501
|
+
* consumer hydrates with `Model.instantiateFromResponse(...)`.
|
|
1502
|
+
* @param {object} args - Arguments.
|
|
1503
|
+
* @param {Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>} args.commandMetadata - Declared per-command metadata.
|
|
1504
|
+
* @param {string[]} args.commandNames - Command method names to resolve.
|
|
1505
|
+
* @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null | undefined} args.resourceClass - Resource class.
|
|
1506
|
+
* @returns {Promise<Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>>} - Enriched metadata.
|
|
1507
|
+
*/
|
|
1508
|
+
async commandMetadataWithResourceJsDoc({commandMetadata, commandNames, resourceClass}) {
|
|
1509
|
+
if (!resourceClass) return commandMetadata
|
|
1510
|
+
|
|
1511
|
+
/** @type {Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>} */
|
|
1512
|
+
const enriched = {...commandMetadata}
|
|
1513
|
+
|
|
1514
|
+
for (const commandName of commandNames) {
|
|
1515
|
+
const declared = commandMetadata[commandName] || {args: [], returnType: null}
|
|
1516
|
+
const sourceClassName = this.methodOwnerClassName({methodName: commandName, targetClass: resourceClass})
|
|
1517
|
+
|
|
1518
|
+
if (!sourceClassName) {
|
|
1519
|
+
enriched[commandName] = declared
|
|
1520
|
+
|
|
1521
|
+
continue
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
let returnType = declared.returnType
|
|
1525
|
+
|
|
1526
|
+
if (!returnType) {
|
|
1527
|
+
const jsDocReturnType = await this.resourceMethodReturnType({methodName: commandName, sourceClassName})
|
|
1528
|
+
|
|
1529
|
+
if (jsDocReturnType) {
|
|
1530
|
+
returnType = this.frontendResolvableCommandJsDocType(this.unwrappedPromiseJsDocType({jsDocType: jsDocReturnType}))
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
let args = declared.args
|
|
1535
|
+
|
|
1536
|
+
if (!args || args.length === 0) {
|
|
1537
|
+
const jsDocParameters = await this.resourceMethodParameters({methodName: commandName, sourceClassName})
|
|
1538
|
+
// Skip object-property tags (`@param {string} args.message`); only the
|
|
1539
|
+
// top-level parameters map to method arguments, otherwise the shared
|
|
1540
|
+
// `@param {object} args` + property style would emit `name(args, args)`.
|
|
1541
|
+
const topLevelParameters = (jsDocParameters || []).filter((parameter) => typeof parameter.name === "string" && !parameter.name.includes("."))
|
|
1542
|
+
|
|
1543
|
+
if (topLevelParameters.length > 0) {
|
|
1544
|
+
args = topLevelParameters.map((parameter) => ({
|
|
1545
|
+
name: /** @type {string} */ (parameter.name),
|
|
1546
|
+
type: this.frontendResolvableCommandJsDocType(parameter.type)
|
|
1547
|
+
}))
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
enriched[commandName] = {args: args || [], returnType: returnType || null}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
return enriched
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1456
1557
|
/**
|
|
1457
1558
|
* Runs unwrapped promise js doc type.
|
|
1458
1559
|
* @param {object} args - Arguments.
|
|
@@ -1534,26 +1635,39 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1534
1635
|
* @returns {Promise<string | null>} - JSDoc parameter type when documented.
|
|
1535
1636
|
*/
|
|
1536
1637
|
async resourceMethodParameterType({methodName, parameterIndex, sourceClassName}) {
|
|
1537
|
-
const
|
|
1538
|
-
const parameterTypesKey = `${sourceClassName}.${methodName}`
|
|
1638
|
+
const parameters = await this.resourceMethodParameters({methodName, sourceClassName})
|
|
1539
1639
|
|
|
1540
|
-
if (!
|
|
1640
|
+
if (!parameters) return null
|
|
1541
1641
|
|
|
1542
|
-
const
|
|
1642
|
+
const parameter = parameters[parameterIndex]
|
|
1543
1643
|
|
|
1544
|
-
if (
|
|
1545
|
-
|
|
1644
|
+
if (parameter === undefined) return null
|
|
1645
|
+
|
|
1646
|
+
if (parameter.type.length < 1) {
|
|
1647
|
+
throw new Error(`Expected non-empty JSDoc parameter type for ${sourceClassName}.${methodName} parameter ${parameterIndex}`)
|
|
1546
1648
|
}
|
|
1547
1649
|
|
|
1548
|
-
|
|
1650
|
+
return parameter.type
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Runs resource method parameters.
|
|
1655
|
+
* @param {{methodName: string, sourceClassName: string}} args - Arguments.
|
|
1656
|
+
* @returns {Promise<Array<{name: string | null, type: string}> | null>} - JSDoc parameters (name + type) when documented.
|
|
1657
|
+
*/
|
|
1658
|
+
async resourceMethodParameters({methodName, sourceClassName}) {
|
|
1659
|
+
const resourceMethodParameterTypes = await this.resourceMethodParameterTypes()
|
|
1660
|
+
const parameterTypesKey = `${sourceClassName}.${methodName}`
|
|
1661
|
+
|
|
1662
|
+
if (!resourceMethodParameterTypes.has(parameterTypesKey)) return null
|
|
1549
1663
|
|
|
1550
|
-
|
|
1664
|
+
const parameters = resourceMethodParameterTypes.get(parameterTypesKey)
|
|
1551
1665
|
|
|
1552
|
-
if (
|
|
1553
|
-
throw new Error(`Expected
|
|
1666
|
+
if (!parameters) {
|
|
1667
|
+
throw new Error(`Expected JSDoc parameters for ${parameterTypesKey}`)
|
|
1554
1668
|
}
|
|
1555
1669
|
|
|
1556
|
-
return
|
|
1670
|
+
return parameters
|
|
1557
1671
|
}
|
|
1558
1672
|
|
|
1559
1673
|
/**
|
|
@@ -1579,7 +1693,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1579
1693
|
|
|
1580
1694
|
/**
|
|
1581
1695
|
* Runs resource method parameter types.
|
|
1582
|
-
* @returns {Promise<Map<string, string
|
|
1696
|
+
* @returns {Promise<Map<string, Array<{name: string | null, type: string}>>>} - Resource method parameters keyed by ClassName.methodName.
|
|
1583
1697
|
*/
|
|
1584
1698
|
async resourceMethodParameterTypes() {
|
|
1585
1699
|
if (this._resourceMethodParameterTypes) return this._resourceMethodParameterTypes
|
|
@@ -1678,7 +1792,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1678
1792
|
|
|
1679
1793
|
/**
|
|
1680
1794
|
* Adds resource method parameter types from source.
|
|
1681
|
-
* @param {{parameterTypes: Map<string, string
|
|
1795
|
+
* @param {{parameterTypes: Map<string, Array<{name: string | null, type: string}>>, sourceText: string}} args - Arguments.
|
|
1682
1796
|
* @returns {void}
|
|
1683
1797
|
*/
|
|
1684
1798
|
addResourceMethodParameterTypesFromSource({parameterTypes, sourceText}) {
|
|
@@ -1710,10 +1824,10 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1710
1824
|
if (!methodMatch) continue
|
|
1711
1825
|
|
|
1712
1826
|
const methodName = methodMatch[1]
|
|
1713
|
-
const
|
|
1827
|
+
const jsDocParameters = this.jsDocParameters(jsDocMatch[1])
|
|
1714
1828
|
|
|
1715
|
-
if (
|
|
1716
|
-
parameterTypes.set(`${className}.${methodName}`,
|
|
1829
|
+
if (jsDocParameters.length > 0) {
|
|
1830
|
+
parameterTypes.set(`${className}.${methodName}`, jsDocParameters)
|
|
1717
1831
|
}
|
|
1718
1832
|
}
|
|
1719
1833
|
|
|
@@ -1748,12 +1862,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1748
1862
|
}
|
|
1749
1863
|
|
|
1750
1864
|
/**
|
|
1751
|
-
* Runs js doc
|
|
1865
|
+
* Runs js doc parameters.
|
|
1752
1866
|
* @param {string} jsDocText - JSDoc text inside comment markers.
|
|
1753
|
-
* @returns {string
|
|
1867
|
+
* @returns {Array<{name: string | null, type: string}>} - JSDoc parameters (name + type) in declaration order.
|
|
1754
1868
|
*/
|
|
1755
|
-
|
|
1756
|
-
const
|
|
1869
|
+
jsDocParameters(jsDocText) {
|
|
1870
|
+
const parameters = []
|
|
1757
1871
|
const paramRegex = /@param\s*\{/g
|
|
1758
1872
|
let _paramMatch
|
|
1759
1873
|
|
|
@@ -1765,17 +1879,23 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1765
1879
|
throw new Error(`Could not parse JSDoc parameter type from: ${jsDocText}`)
|
|
1766
1880
|
}
|
|
1767
1881
|
|
|
1768
|
-
const
|
|
1882
|
+
const type = jsDocText.slice(typeOpenIndex + 1, typeCloseIndex).trim()
|
|
1769
1883
|
|
|
1770
|
-
if (
|
|
1884
|
+
if (type.length < 1) {
|
|
1771
1885
|
throw new Error(`Expected non-empty JSDoc parameter type in: ${jsDocText}`)
|
|
1772
1886
|
}
|
|
1773
1887
|
|
|
1774
|
-
|
|
1888
|
+
// After the closing brace the parameter name follows (optionally bracketed
|
|
1889
|
+
// for `@param {type} [name]`). Capture the leading name token — including any
|
|
1890
|
+
// dotted path so object-property tags like `@param {string} args.message` stay
|
|
1891
|
+
// distinguishable from the top-level `@param {object} args` parameter.
|
|
1892
|
+
const nameMatch = jsDocText.slice(typeCloseIndex + 1).match(/^\s*\[?\s*([A-Za-z_$][\w$.]*)/)
|
|
1893
|
+
|
|
1894
|
+
parameters.push({name: nameMatch ? nameMatch[1] : null, type})
|
|
1775
1895
|
paramRegex.lastIndex = typeCloseIndex + 1
|
|
1776
1896
|
}
|
|
1777
1897
|
|
|
1778
|
-
return
|
|
1898
|
+
return parameters
|
|
1779
1899
|
}
|
|
1780
1900
|
|
|
1781
1901
|
/**
|