velocious 1.0.470 → 1.0.472

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.
Files changed (83) hide show
  1. package/README.md +5 -3
  2. package/build/configuration-types.js +1 -0
  3. package/build/configuration.js +27 -1
  4. package/build/controller.js +13 -1
  5. package/build/database/datetime-storage.js +49 -5
  6. package/build/database/record/index.js +14 -4
  7. package/build/environment-handlers/base.js +46 -0
  8. package/build/environment-handlers/node/cli/commands/generate/base-models.js +4 -1
  9. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +368 -43
  10. package/build/environment-handlers/node/cli/commands/generate/generated-file-banner.js +15 -0
  11. package/build/environment-handlers/node.js +84 -8
  12. package/build/frontend-model-controller.js +7 -7
  13. package/build/frontend-models/base.js +75 -9
  14. package/build/frontend-models/transport-serialization.js +41 -8
  15. package/build/frontend-models/websocket-channel.js +18 -1
  16. package/build/frontend-models/websocket-publishers.js +12 -1
  17. package/build/http-server/client/request-runner.js +13 -1
  18. package/build/src/configuration-types.d.ts +5 -0
  19. package/build/src/configuration-types.d.ts.map +1 -1
  20. package/build/src/configuration-types.js +2 -1
  21. package/build/src/configuration.d.ts +14 -1
  22. package/build/src/configuration.d.ts.map +1 -1
  23. package/build/src/configuration.js +25 -2
  24. package/build/src/controller.d.ts +5 -0
  25. package/build/src/controller.d.ts.map +1 -1
  26. package/build/src/controller.js +12 -2
  27. package/build/src/database/datetime-storage.d.ts +12 -4
  28. package/build/src/database/datetime-storage.d.ts.map +1 -1
  29. package/build/src/database/datetime-storage.js +48 -6
  30. package/build/src/database/record/index.d.ts +5 -0
  31. package/build/src/database/record/index.d.ts.map +1 -1
  32. package/build/src/database/record/index.js +13 -5
  33. package/build/src/environment-handlers/base.d.ts +19 -0
  34. package/build/src/environment-handlers/base.d.ts.map +1 -1
  35. package/build/src/environment-handlers/base.js +41 -1
  36. package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
  37. package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +4 -2
  38. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +219 -40
  39. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  40. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +317 -43
  41. package/build/src/environment-handlers/node/cli/commands/generate/generated-file-banner.d.ts +9 -0
  42. package/build/src/environment-handlers/node/cli/commands/generate/generated-file-banner.d.ts.map +1 -0
  43. package/build/src/environment-handlers/node/cli/commands/generate/generated-file-banner.js +13 -0
  44. package/build/src/environment-handlers/node.d.ts +1 -0
  45. package/build/src/environment-handlers/node.d.ts.map +1 -1
  46. package/build/src/environment-handlers/node.js +76 -9
  47. package/build/src/frontend-model-controller.js +8 -8
  48. package/build/src/frontend-models/base.d.ts +4 -0
  49. package/build/src/frontend-models/base.d.ts.map +1 -1
  50. package/build/src/frontend-models/base.js +64 -9
  51. package/build/src/frontend-models/transport-serialization.d.ts +30 -1
  52. package/build/src/frontend-models/transport-serialization.d.ts.map +1 -1
  53. package/build/src/frontend-models/transport-serialization.js +34 -9
  54. package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
  55. package/build/src/frontend-models/websocket-channel.js +16 -2
  56. package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
  57. package/build/src/frontend-models/websocket-publishers.js +12 -2
  58. package/build/src/http-server/client/request-runner.d.ts.map +1 -1
  59. package/build/src/http-server/client/request-runner.js +13 -2
  60. package/build/src/time-zone.d.ts +16 -0
  61. package/build/src/time-zone.d.ts.map +1 -0
  62. package/build/src/time-zone.js +56 -0
  63. package/build/time-zone.js +65 -0
  64. package/build/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +2 -1
  66. package/scripts/test-browser.js +1 -0
  67. package/src/configuration-types.js +1 -0
  68. package/src/configuration.js +27 -1
  69. package/src/controller.js +13 -1
  70. package/src/database/datetime-storage.js +49 -5
  71. package/src/database/record/index.js +14 -4
  72. package/src/environment-handlers/base.js +46 -0
  73. package/src/environment-handlers/node/cli/commands/generate/base-models.js +4 -1
  74. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +368 -43
  75. package/src/environment-handlers/node/cli/commands/generate/generated-file-banner.js +15 -0
  76. package/src/environment-handlers/node.js +84 -8
  77. package/src/frontend-model-controller.js +7 -7
  78. package/src/frontend-models/base.js +75 -9
  79. package/src/frontend-models/transport-serialization.js +41 -8
  80. package/src/frontend-models/websocket-channel.js +18 -1
  81. package/src/frontend-models/websocket-publishers.js +12 -1
  82. package/src/http-server/client/request-runner.js +13 -1
  83. package/src/time-zone.js +65 -0
@@ -1,10 +1,13 @@
1
1
  import BaseCommand from "../../../../../cli/base-command.js"
2
2
  import fs from "fs/promises"
3
+ import generatedFileBanner from "./generated-file-banner.js"
3
4
  import path from "node:path"
4
5
  import * as inflection from "inflection"
5
6
  import {frontendModelResourceIsBuiltIn, frontendModelResourcesWithBuiltInsForBackendProject} from "../../../../../frontend-models/built-in-resources.js"
6
7
  import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition} from "../../../../../frontend-models/resource-definition.js"
7
8
 
9
+ const FRONTEND_MODELS_REGENERATE_COMMAND = "velocious generate:frontend-models"
10
+
8
11
  /**
9
12
  * Attribute metadata used for generated frontend-model JSDoc.
10
13
  * @typedef {object} FrontendAttributeConfig
@@ -23,13 +26,34 @@ import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigura
23
26
  * Permit spec returned by frontend-model resources during generation.
24
27
  * @typedef {Array<string | Record<string, FrontendModelGeneratorPermitSpec>>} FrontendModelGeneratorPermitSpec
25
28
  */
29
+ /**
30
+ * JSDoc import alias extracted from a backend resource source file.
31
+ * @typedef {object} ResourceJsDocImportAlias
32
+ * @property {string} importedName - Exported type name.
33
+ * @property {string} specifier - Import specifier from the source file.
34
+ */
35
+ /**
36
+ * JSDoc return type extracted from a backend resource method.
37
+ * @typedef {object} ResourceMethodReturnType
38
+ * @property {Map<string, ResourceJsDocImportAlias>} importAliases - Import aliases visible in the source file.
39
+ * @property {string | null} sourceFile - Source file that declared the method.
40
+ * @property {string} type - JSDoc return type.
41
+ */
42
+ /**
43
+ * JSDoc parameter type extracted from a backend resource method.
44
+ * @typedef {object} ResourceMethodParameterType
45
+ * @property {Map<string, ResourceJsDocImportAlias>} importAliases - Import aliases visible in the source file.
46
+ * @property {string | null} name - Parameter name.
47
+ * @property {string | null} sourceFile - Source file that declared the method.
48
+ * @property {string} type - JSDoc parameter type.
49
+ */
26
50
 
27
51
  /** Node CLI command that generates frontend model classes from backend project resource config. */
28
52
  export default class DbGenerateFrontendModels extends BaseCommand {
29
- /** @type {Map<string, string> | null} */
53
+ /** @type {Map<string, ResourceMethodReturnType> | null} */
30
54
  _resourceMethodReturnTypes = null
31
55
 
32
- /** @type {Map<string, Array<{name: string | null, type: string}>> | null} */
56
+ /** @type {Map<string, ResourceMethodParameterType[]> | null} */
33
57
  _resourceMethodParameterTypes = null
34
58
 
35
59
  /**
@@ -128,6 +152,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
128
152
 
129
153
  const fileContent = await this.buildModelFileContent({
130
154
  className,
155
+ frontendModelFilePath: filePath,
131
156
  importPath,
132
157
  modelClass: resourceClass ? resourceClass.modelClass() : configuration.getModelClasses()[className],
133
158
  modelConfig,
@@ -253,13 +278,14 @@ export default class DbGenerateFrontendModels extends BaseCommand {
253
278
  * Runs build model file content.
254
279
  * @param {object} args - Method args.
255
280
  * @param {string} args.className - Model class name.
281
+ * @param {string} args.frontendModelFilePath - Generated frontend model file path.
256
282
  * @param {string} args.importPath - Base class import path.
257
283
  * @param {typeof import("../../../../../database/record/index.js").default | undefined} args.modelClass - Backend model class.
258
284
  * @param {import("../../../../../configuration-types.js").NormalizedFrontendModelResourceConfiguration} args.modelConfig - Model configuration.
259
285
  * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} [args.resourceClass] - Resource class.
260
286
  * @returns {Promise<string>} - Generated file content.
261
287
  */
262
- async buildModelFileContent({className, importPath, modelClass, modelConfig, resourceClass}) {
288
+ async buildModelFileContent({className, frontendModelFilePath, importPath, modelClass, modelConfig, resourceClass}) {
263
289
  const attributes = await this.attributeDefinitionsForModel({className, modelClass, modelConfig, resourceClass})
264
290
  const relationships = this.relationshipsForModel({className, modelConfig, resourceClass})
265
291
  const attachments = modelConfig.attachments && typeof modelConfig.attachments === "object"
@@ -292,6 +318,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
292
318
  const commandMetadata = await this.commandMetadataWithResourceJsDoc({
293
319
  commandMetadata: declaredCommandMetadata,
294
320
  commandNames: [...Object.keys(collectionCommands), ...Object.keys(memberCommands)],
321
+ frontendModelFilePath,
295
322
  resourceClass
296
323
  })
297
324
  const builtInCollectionCommandsAreDefault = builtInCollectionCommands.create === "create" && builtInCollectionCommands.index === "index"
@@ -302,7 +329,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
302
329
  && builtInMemberCommands.update === "update"
303
330
  && builtInMemberCommands.url === "url"
304
331
 
305
- let fileContent = ""
332
+ let fileContent = generatedFileBanner(FRONTEND_MODELS_REGENERATE_COMMAND)
306
333
 
307
334
  fileContent += `import FrontendModelBase from "${importPath}"\n`
308
335
 
@@ -604,7 +631,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
604
631
  * @returns {string} - Index file content that imports and re-exports all models.
605
632
  */
606
633
  buildIndexFileContent(generatedFiles) {
607
- let content = ""
634
+ let content = generatedFileBanner(FRONTEND_MODELS_REGENERATE_COMMAND)
608
635
 
609
636
  for (const {className, fileName} of generatedFiles) {
610
637
  content += `export {default as ${className}} from "./${fileName}"\n`
@@ -619,9 +646,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
619
646
  * @returns {string} - Setup file content with side-effect imports for model registration.
620
647
  */
621
648
  buildSetupFileContent(generatedFiles) {
622
- let content = "// This file is auto-generated by Velocious. Do not edit manually.\n"
623
-
624
- content += "// Run `velocious g:frontend-models` to regenerate.\n"
649
+ let content = generatedFileBanner(FRONTEND_MODELS_REGENERATE_COMMAND)
625
650
 
626
651
  for (const {fileName} of generatedFiles) {
627
652
  content += `import "./${fileName}"\n`
@@ -633,7 +658,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
633
658
  /**
634
659
  * Runs write attributes typedef.
635
660
  * @param {object} args - Arguments.
636
- * @param {Array<{jsDocType: string, name: string}>} args.attributes - Generated read attributes.
661
+ * @param {Array<{jsDocType: string, name: string, writeJsDocType: string}>} args.attributes - Generated read attributes.
637
662
  * @param {string} args.attributesTypeName - Generated read attributes typedef name.
638
663
  * @param {typeof import("../../../../../database/record/index.js").default | undefined} args.modelClass - Backend model class.
639
664
  * @param {Array<{attributes: Array<{name: string, type: string}>, relationshipName: string, typeName: string}>} args.nestedWriteTypes - Nested write typedefs.
@@ -691,7 +716,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
691
716
 
692
717
  /**
693
718
  * Runs frontend write attribute type.
694
- * @param {{attribute: {jsDocType: string, name: string} | undefined, attributeName: string, attributesTypeName: string, resourceClass: import("../../../../../configuration-types.js").FrontendModelResourceClassType | null | undefined}} args - Arguments.
719
+ * @param {{attribute: {jsDocType: string, name: string, writeJsDocType: string} | undefined, attributeName: string, attributesTypeName: string, resourceClass: import("../../../../../configuration-types.js").FrontendModelResourceClassType | null | undefined}} args - Arguments.
695
720
  * @returns {Promise<string>} - JSDoc type for the permitted write field.
696
721
  */
697
722
  async frontendWriteAttributeType({attribute, attributeName, attributesTypeName, resourceClass}) {
@@ -703,6 +728,8 @@ export default class DbGenerateFrontendModels extends BaseCommand {
703
728
 
704
729
  if (attribute.jsDocType.trim() === "null") return "FrontendModelAttributeValue"
705
730
 
731
+ if (attribute.writeJsDocType !== attribute.jsDocType) return attribute.writeJsDocType
732
+
706
733
  return `${attributesTypeName}[${JSON.stringify(attribute.name)}] | null`
707
734
  }
708
735
 
@@ -823,7 +850,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
823
850
 
824
851
  return {
825
852
  name: resolvedAttributeName,
826
- type: attributeConfig ? this.jsDocTypeForFrontendAttribute({attributeConfig}) : "FrontendModelAttributeValue"
853
+ type: attributeConfig ? this.jsDocTypeForFrontendWriteAttribute({attributeConfig}) : "FrontendModelAttributeValue"
827
854
  }
828
855
  })
829
856
  }
@@ -979,7 +1006,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
979
1006
  * @param {typeof import("../../../../../database/record/index.js").default | undefined} args.modelClass - Backend model class.
980
1007
  * @param {import("../../../../../configuration-types.js").NormalizedFrontendModelResourceConfiguration} args.modelConfig - Model configuration.
981
1008
  * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} [args.resourceClass] - Resource class.
982
- * @returns {Promise<Array<{jsDocType: string, name: string}>>} - Attribute definitions.
1009
+ * @returns {Promise<Array<{jsDocType: string, name: string, writeJsDocType: string}>>} - Attribute definitions.
983
1010
  */
984
1011
  async attributeDefinitionsForModel({className, modelClass, modelConfig, resourceClass}) {
985
1012
  let attributes = modelConfig.attributes
@@ -1028,7 +1055,8 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1028
1055
 
1029
1056
  attributeDefinitions.push({
1030
1057
  jsDocType: this.jsDocTypeForFrontendAttribute({attributeConfig: frontendAttributeConfig}),
1031
- name: attributeName
1058
+ name: attributeName,
1059
+ writeJsDocType: this.jsDocTypeForFrontendWriteAttribute({attributeConfig: frontendAttributeConfig})
1032
1060
  })
1033
1061
  }
1034
1062
 
@@ -1061,7 +1089,8 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1061
1089
 
1062
1090
  attributeDefinitions.push({
1063
1091
  jsDocType: this.jsDocTypeForFrontendAttribute({attributeConfig: frontendAttributeConfig}),
1064
- name: attributeName
1092
+ name: attributeName,
1093
+ writeJsDocType: this.jsDocTypeForFrontendWriteAttribute({attributeConfig: frontendAttributeConfig})
1065
1094
  })
1066
1095
  }
1067
1096
 
@@ -1236,6 +1265,39 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1236
1265
  return `${jsDocType} | null`
1237
1266
  }
1238
1267
 
1268
+ /**
1269
+ * Runs js doc type for frontend write attribute.
1270
+ * @param {object} args - Arguments.
1271
+ * @param {FrontendAttributeConfig | null | undefined} args.attributeConfig - Attribute configuration value.
1272
+ * @returns {string} - JSDoc type accepted by create/update payloads.
1273
+ */
1274
+ jsDocTypeForFrontendWriteAttribute({attributeConfig}) {
1275
+ if (attributeConfig && typeof attributeConfig.jsDocType == "string" && attributeConfig.jsDocType.length > 0) {
1276
+ return attributeConfig.jsDocType
1277
+ }
1278
+
1279
+ const jsDocType = this.jsDocTypeForFrontendWriteAttributeBaseType(attributeConfig)
1280
+
1281
+ if (!this.frontendAttributeCanBeNull(attributeConfig)) {
1282
+ return jsDocType
1283
+ }
1284
+
1285
+ return `${jsDocType} | null`
1286
+ }
1287
+
1288
+ /**
1289
+ * Runs js doc type for frontend write attribute base type.
1290
+ * @param {FrontendAttributeConfig | null | undefined} attributeConfig - Attribute configuration value.
1291
+ * @returns {string} - Non-nullable JSDoc type accepted by create/update payloads.
1292
+ */
1293
+ jsDocTypeForFrontendWriteAttributeBaseType(attributeConfig) {
1294
+ const readType = this.jsDocTypeForFrontendAttributeBaseType(attributeConfig)
1295
+
1296
+ if (!this.frontendAttributeTypeIsTemporal(attributeConfig)) return readType
1297
+
1298
+ return `${readType} | string`
1299
+ }
1300
+
1239
1301
  /**
1240
1302
  * Runs js doc type for frontend attribute base type.
1241
1303
  * @param {FrontendAttributeConfig | null | undefined} attributeConfig - Attribute configuration value.
@@ -1256,13 +1318,26 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1256
1318
  return "string"
1257
1319
  } else if (type && ["bit", "bigint", "decimal", "double", "double precision", "float", "int", "integer", "numeric", "real", "smallint", "tinyint"].includes(type)) {
1258
1320
  return "number"
1259
- } else if (type && ["date", "datetime", "timestamp", "timestamp without time zone", "timestamptz"].includes(type)) {
1321
+ } else if (this.frontendAttributeTypeIsTemporal(attributeConfig)) {
1260
1322
  return "Date"
1261
1323
  } else {
1262
1324
  return "FrontendModelAttributeValue"
1263
1325
  }
1264
1326
  }
1265
1327
 
1328
+ /**
1329
+ * Runs frontend attribute type is temporal.
1330
+ * @param {FrontendAttributeConfig | null | undefined} attributeConfig - Attribute configuration value.
1331
+ * @returns {boolean} - Whether the attribute represents a date/time value.
1332
+ */
1333
+ frontendAttributeTypeIsTemporal(attributeConfig) {
1334
+ if (!attributeConfig || typeof attributeConfig !== "object") return false
1335
+
1336
+ const type = this.frontendAttributeTypeValue(attributeConfig)
1337
+
1338
+ return type ? ["date", "datetime", "timestamp", "timestamp without time zone", "timestamptz"].includes(type) : false
1339
+ }
1340
+
1266
1341
  /**
1267
1342
  * Runs frontend attribute can be null.
1268
1343
  * @param {FrontendAttributeConfig | null | undefined} attributeConfig - Attribute configuration value.
@@ -1437,25 +1512,209 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1437
1512
 
1438
1513
  /**
1439
1514
  * 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.
1515
+ * frontend model: backend model imports are mapped to generated frontend model
1516
+ * imports, and otherwise non-frontend-resolvable identifiers become `any` in place
1517
+ * so sibling scalar fields keep their real types. The word boundary avoids matching
1518
+ * the capitalized middle of a camelCase property name (e.g. `adjustedTotalCents`).
1519
+ * @param {object} args - Arguments.
1520
+ * @param {string | null} args.frontendModelFilePath - Generated frontend model file path.
1521
+ * @param {Map<string, ResourceJsDocImportAlias>} args.importAliases - Import aliases visible to the source method.
1522
+ * @param {string} args.jsDocType - Resolved (Promise-unwrapped) JSDoc type.
1523
+ * @param {string | null} args.sourceFile - Source file that declared the method.
1446
1524
  * @returns {string} - A frontend-resolvable JSDoc type.
1447
1525
  */
1448
- frontendResolvableCommandJsDocType(jsDocType) {
1526
+ frontendResolvableCommandJsDocType({frontendModelFilePath, importAliases, jsDocType, sourceFile}) {
1449
1527
  const safeTypeIdentifiers = this.frontendResolvableTypeIdentifiers()
1528
+ /** @type {string[]} */
1529
+ const preservedImports = []
1530
+ /**
1531
+ * Stores an import expression behind a lowercase placeholder while generic
1532
+ * identifier cleanup runs.
1533
+ * @param {string} importExpression - Import expression to preserve.
1534
+ * @returns {string} Placeholder inserted into the type string.
1535
+ */
1536
+ const preserveImportExpression = (importExpression) => {
1537
+ const placeholder = `__velocious_import_placeholder_${preservedImports.length}__`
1538
+
1539
+ preservedImports.push(importExpression)
1540
+
1541
+ return placeholder
1542
+ }
1543
+
1544
+ this.assertNoBackendLocalCommandTypeExpressions(jsDocType)
1545
+
1546
+ const withRewrittenInlineImports = jsDocType
1547
+ // A type that reaches into a backend source file via `import("...")`
1548
+ // (optionally `.Member` and `[]`) is frontend-resolvable only when it
1549
+ // points at a generated model file; other backend-local imports collapse
1550
+ // to `any` so helper/service implementation details do not leak.
1551
+ .replace(/import\(\s*["']([^"']*)["']\s*\)((?:\s*\.\s*[A-Za-z_$][\w$]*)*)((?:\s*\[\s*\])*)/g, (_match, specifier, memberChain, arraySuffix) => {
1552
+ const rewrittenSpecifier = this.frontendResolvableJsDocImportSpecifier({
1553
+ frontendModelFilePath,
1554
+ sourceFile,
1555
+ specifier
1556
+ })
1450
1557
 
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")
1558
+ if (!rewrittenSpecifier) return "any"
1559
+
1560
+ return preserveImportExpression(`import(${JSON.stringify(rewrittenSpecifier)})${memberChain.replace(/\s+/g, "")}${arraySuffix.replace(/\s+/g, "")}`)
1561
+ })
1562
+
1563
+ let withRewrittenAliases = withRewrittenInlineImports
1564
+
1565
+ for (const [aliasName, importAlias] of importAliases) {
1566
+ const rewrittenSpecifier = this.frontendResolvableJsDocImportSpecifier({
1567
+ frontendModelFilePath,
1568
+ sourceFile,
1569
+ specifier: importAlias.specifier
1570
+ })
1571
+
1572
+ if (!rewrittenSpecifier) continue
1573
+
1574
+ const aliasRegex = new RegExp(`\\b${this.escapeRegExp(aliasName)}\\b`, "g")
1575
+
1576
+ withRewrittenAliases = withRewrittenAliases.replace(aliasRegex, preserveImportExpression(`import(${JSON.stringify(rewrittenSpecifier)}).${importAlias.importedName}`))
1577
+ }
1578
+
1579
+ const sanitized = withRewrittenAliases
1456
1580
  // Remaining capitalized identifiers are model classes or otherwise non-resolvable
1457
1581
  // types; downgrade each in place so sibling scalar fields keep their real types.
1458
1582
  .replace(/\b[A-Z][A-Za-z0-9_$]*/g, (identifier) => safeTypeIdentifiers.has(identifier) ? identifier : "any")
1583
+
1584
+ return preservedImports.reduce(
1585
+ (type, importExpression, index) => type.replaceAll(`__velocious_import_placeholder_${index}__`, importExpression),
1586
+ sanitized
1587
+ )
1588
+ }
1589
+
1590
+ /**
1591
+ * Raises when a command JSDoc type references a backend-local helper expression.
1592
+ * @param {string} jsDocType - Command JSDoc type.
1593
+ * @returns {void} No return value.
1594
+ */
1595
+ assertNoBackendLocalCommandTypeExpressions(jsDocType) {
1596
+ const localReturnTypeMatch = jsDocType.match(/\b(?:Awaited\s*<\s*)?ReturnType\s*<\s*typeof\s+[A-Za-z_$][\w$]*\s*>\s*>?/)
1597
+
1598
+ if (!localReturnTypeMatch) return
1599
+
1600
+ throw new Error(`Custom command JSDoc type cannot use backend-local ReturnType expressions in generated frontend models: ${localReturnTypeMatch[0]}. Move the payload shape to a shared typedef and return that type from the command method.`)
1601
+ }
1602
+
1603
+ /**
1604
+ * Runs frontend resolvable js doc import specifier.
1605
+ * @param {object} args - Arguments.
1606
+ * @param {string | null} args.frontendModelFilePath - Generated frontend model file path.
1607
+ * @param {string | null} args.sourceFile - Source file that declared the JSDoc type.
1608
+ * @param {string} args.specifier - Source-file import specifier.
1609
+ * @returns {string | null} - Rewritten frontend-model import specifier, or null when backend-local.
1610
+ */
1611
+ frontendResolvableJsDocImportSpecifier({frontendModelFilePath, sourceFile, specifier}) {
1612
+ if (!sourceFile || !frontendModelFilePath) return null
1613
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) return specifier
1614
+
1615
+ const importedPath = path.resolve(path.dirname(sourceFile), specifier)
1616
+ const modelImportSpecifier = this.frontendModelImportSpecifierForBackendModelPath({
1617
+ frontendModelFilePath,
1618
+ importedPath
1619
+ })
1620
+
1621
+ if (modelImportSpecifier) return modelImportSpecifier
1622
+
1623
+ if (this.filePathIsWithinAnyDirectory({directories: this.frontendModelJsDocSourceDirectories(), filePath: importedPath})) {
1624
+ return null
1625
+ }
1626
+
1627
+ return this.relativeImportSpecifier({fromFile: frontendModelFilePath, toFile: importedPath})
1628
+ }
1629
+
1630
+ /**
1631
+ * Runs frontend model import specifier for backend model path.
1632
+ * @param {object} args - Arguments.
1633
+ * @param {string} args.frontendModelFilePath - Generated frontend model file path.
1634
+ * @param {string} args.importedPath - Source-file import path resolved from JSDoc.
1635
+ * @returns {string | null} - Generated frontend-model import specifier, or null when the path is not a registered model file.
1636
+ */
1637
+ frontendModelImportSpecifierForBackendModelPath({frontendModelFilePath, importedPath}) {
1638
+ const frontendModelsDirectory = path.dirname(frontendModelFilePath)
1639
+ const importedModelPath = importedPath.endsWith(".js") ? importedPath : `${importedPath}.js`
1640
+
1641
+ for (const modelFileName of this.generatedFrontendModelFileNames()) {
1642
+ for (const sourceDirectory of this.frontendModelJsDocSourceDirectories()) {
1643
+ const modelsDirectory = path.join(sourceDirectory, "models")
1644
+ const candidateModelPath = path.join(modelsDirectory, modelFileName)
1645
+
1646
+ if (path.resolve(candidateModelPath) !== path.resolve(importedModelPath)) continue
1647
+
1648
+ return this.relativeImportSpecifier({
1649
+ fromFile: frontendModelFilePath,
1650
+ toFile: path.join(frontendModelsDirectory, modelFileName)
1651
+ })
1652
+ }
1653
+ }
1654
+
1655
+ return null
1656
+ }
1657
+
1658
+ /**
1659
+ * Runs generated frontend model file names.
1660
+ * @returns {Set<string>} - Frontend model filenames that this generation run can emit.
1661
+ */
1662
+ generatedFrontendModelFileNames() {
1663
+ /** @type {Set<string>} */
1664
+ const fileNames = new Set()
1665
+
1666
+ for (const backendProject of this.getConfiguration().getBackendProjects()) {
1667
+ const resources = this.resourcesForBackendProject(backendProject)
1668
+
1669
+ for (const resourceModelName of Object.keys(resources)) {
1670
+ const className = inflection.camelize(resourceModelName.replaceAll("-", "_"))
1671
+
1672
+ fileNames.add(`${inflection.dasherize(inflection.underscore(className))}.js`)
1673
+ }
1674
+ }
1675
+
1676
+ return fileNames
1677
+ }
1678
+
1679
+ /**
1680
+ * Runs relative import specifier.
1681
+ * @param {object} args - Arguments.
1682
+ * @param {string} args.fromFile - Source file that will contain the import expression.
1683
+ * @param {string} args.toFile - File being imported.
1684
+ * @returns {string} - Relative import specifier.
1685
+ */
1686
+ relativeImportSpecifier({fromFile, toFile}) {
1687
+ let relativeSpecifier = path.relative(path.dirname(fromFile), toFile).split(path.sep).join("/")
1688
+
1689
+ if (!relativeSpecifier.startsWith(".")) {
1690
+ relativeSpecifier = `./${relativeSpecifier}`
1691
+ }
1692
+
1693
+ return relativeSpecifier
1694
+ }
1695
+
1696
+ /**
1697
+ * Runs file path is within any directory.
1698
+ * @param {object} args - Arguments.
1699
+ * @param {string[]} args.directories - Candidate parent directories.
1700
+ * @param {string} args.filePath - File path to test.
1701
+ * @returns {boolean} - Whether the file path is under one candidate directory.
1702
+ */
1703
+ filePathIsWithinAnyDirectory({directories, filePath}) {
1704
+ return directories.some((directory) => {
1705
+ const relativePath = path.relative(path.resolve(directory), path.resolve(filePath))
1706
+
1707
+ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
1708
+ })
1709
+ }
1710
+
1711
+ /**
1712
+ * Escapes text for use inside a RegExp.
1713
+ * @param {string} value - Value to escape.
1714
+ * @returns {string} - RegExp-safe value.
1715
+ */
1716
+ escapeRegExp(value) {
1717
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
1459
1718
  }
1460
1719
 
1461
1720
  /**
@@ -1625,10 +1884,11 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1625
1884
  * @param {object} args - Arguments.
1626
1885
  * @param {Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>} args.commandMetadata - Declared per-command metadata.
1627
1886
  * @param {string[]} args.commandNames - Command method names to resolve.
1887
+ * @param {string} args.frontendModelFilePath - Generated frontend model file path.
1628
1888
  * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null | undefined} args.resourceClass - Resource class.
1629
1889
  * @returns {Promise<Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>>} - Enriched metadata.
1630
1890
  */
1631
- async commandMetadataWithResourceJsDoc({commandMetadata, commandNames, resourceClass}) {
1891
+ async commandMetadataWithResourceJsDoc({commandMetadata, commandNames, frontendModelFilePath, resourceClass}) {
1632
1892
  if (!resourceClass) return commandMetadata
1633
1893
 
1634
1894
  /** @type {Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>} */
@@ -1647,10 +1907,15 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1647
1907
  let returnType = declared.returnType
1648
1908
 
1649
1909
  if (!returnType) {
1650
- const jsDocReturnType = await this.resourceMethodReturnType({methodName: commandName, sourceClassName})
1910
+ const jsDocReturnType = await this.resourceMethodReturnTypeDefinition({methodName: commandName, sourceClassName})
1651
1911
 
1652
1912
  if (jsDocReturnType) {
1653
- returnType = this.frontendResolvableCommandJsDocType(this.unwrappedPromiseJsDocType({jsDocType: jsDocReturnType}))
1913
+ returnType = this.frontendResolvableCommandJsDocType({
1914
+ frontendModelFilePath,
1915
+ importAliases: jsDocReturnType.importAliases,
1916
+ jsDocType: this.unwrappedPromiseJsDocType({jsDocType: jsDocReturnType.type}),
1917
+ sourceFile: jsDocReturnType.sourceFile
1918
+ })
1654
1919
  }
1655
1920
  }
1656
1921
 
@@ -1666,7 +1931,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1666
1931
  if (topLevelParameters.length > 0) {
1667
1932
  args = topLevelParameters.map((parameter) => ({
1668
1933
  name: /** @type {string} */ (parameter.name),
1669
- type: this.frontendResolvableCommandJsDocType(parameter.type)
1934
+ type: this.frontendResolvableCommandJsDocType({
1935
+ frontendModelFilePath,
1936
+ importAliases: parameter.importAliases,
1937
+ jsDocType: parameter.type,
1938
+ sourceFile: parameter.sourceFile
1939
+ })
1670
1940
  }))
1671
1941
  }
1672
1942
  }
@@ -1738,6 +2008,19 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1738
2008
  * @returns {Promise<string | null>} - JSDoc return type when documented.
1739
2009
  */
1740
2010
  async resourceMethodReturnType({methodName, sourceClassName}) {
2011
+ const returnType = await this.resourceMethodReturnTypeDefinition({methodName, sourceClassName})
2012
+
2013
+ return returnType ? returnType.type : null
2014
+ }
2015
+
2016
+ /**
2017
+ * Runs resource method return type definition.
2018
+ * @param {object} args - Arguments.
2019
+ * @param {string} args.methodName - Method name.
2020
+ * @param {string} args.sourceClassName - Source class name.
2021
+ * @returns {Promise<ResourceMethodReturnType | null>} - JSDoc return type definition when documented.
2022
+ */
2023
+ async resourceMethodReturnTypeDefinition({methodName, sourceClassName}) {
1741
2024
  const resourceMethodReturnTypes = await this.resourceMethodReturnTypes()
1742
2025
  const returnTypeKey = `${sourceClassName}.${methodName}`
1743
2026
 
@@ -1745,7 +2028,11 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1745
2028
 
1746
2029
  const returnType = resourceMethodReturnTypes.get(returnTypeKey)
1747
2030
 
1748
- if (typeof returnType != "string" || returnType.length < 1) {
2031
+ if (!returnType) {
2032
+ throw new Error(`Expected JSDoc return type for ${returnTypeKey}`)
2033
+ }
2034
+
2035
+ if (typeof returnType.type != "string" || returnType.type.length < 1) {
1749
2036
  throw new Error(`Expected non-empty JSDoc return type for ${returnTypeKey}`)
1750
2037
  }
1751
2038
 
@@ -1776,7 +2063,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1776
2063
  /**
1777
2064
  * Runs resource method parameters.
1778
2065
  * @param {{methodName: string, sourceClassName: string}} args - Arguments.
1779
- * @returns {Promise<Array<{name: string | null, type: string}> | null>} - JSDoc parameters (name + type) when documented.
2066
+ * @returns {Promise<ResourceMethodParameterType[] | null>} - JSDoc parameters (name + type) when documented.
1780
2067
  */
1781
2068
  async resourceMethodParameters({methodName, sourceClassName}) {
1782
2069
  const resourceMethodParameterTypes = await this.resourceMethodParameterTypes()
@@ -1795,7 +2082,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1795
2082
 
1796
2083
  /**
1797
2084
  * Runs resource method return types.
1798
- * @returns {Promise<Map<string, string>>} - Resource method return types keyed by ClassName.methodName.
2085
+ * @returns {Promise<Map<string, ResourceMethodReturnType>>} - Resource method return types keyed by ClassName.methodName.
1799
2086
  */
1800
2087
  async resourceMethodReturnTypes() {
1801
2088
  if (this._resourceMethodReturnTypes) return this._resourceMethodReturnTypes
@@ -1806,7 +2093,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1806
2093
  for (const sourceFile of sourceFiles) {
1807
2094
  const sourceText = await fs.readFile(sourceFile, "utf8")
1808
2095
 
1809
- this.addResourceMethodReturnTypesFromSource({returnTypes, sourceText})
2096
+ this.addResourceMethodReturnTypesFromSource({returnTypes, sourceFile, sourceText})
1810
2097
  }
1811
2098
 
1812
2099
  this._resourceMethodReturnTypes = returnTypes
@@ -1816,7 +2103,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1816
2103
 
1817
2104
  /**
1818
2105
  * Runs resource method parameter types.
1819
- * @returns {Promise<Map<string, Array<{name: string | null, type: string}>>>} - Resource method parameters keyed by ClassName.methodName.
2106
+ * @returns {Promise<Map<string, ResourceMethodParameterType[]>>} - Resource method parameters keyed by ClassName.methodName.
1820
2107
  */
1821
2108
  async resourceMethodParameterTypes() {
1822
2109
  if (this._resourceMethodParameterTypes) return this._resourceMethodParameterTypes
@@ -1827,7 +2114,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1827
2114
  for (const sourceFile of sourceFiles) {
1828
2115
  const sourceText = await fs.readFile(sourceFile, "utf8")
1829
2116
 
1830
- this.addResourceMethodParameterTypesFromSource({parameterTypes, sourceText})
2117
+ this.addResourceMethodParameterTypesFromSource({parameterTypes, sourceFile, sourceText})
1831
2118
  }
1832
2119
 
1833
2120
  this._resourceMethodParameterTypes = parameterTypes
@@ -1868,12 +2155,14 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1868
2155
  /**
1869
2156
  * Adds resource method return types from source.
1870
2157
  * @param {object} args - Arguments.
1871
- * @param {Map<string, string>} args.returnTypes - Mutable return types map.
2158
+ * @param {Map<string, ResourceMethodReturnType>} args.returnTypes - Mutable return types map.
2159
+ * @param {string | null} [args.sourceFile] - Source file path.
1872
2160
  * @param {string} args.sourceText - Source text.
1873
2161
  * @returns {void}
1874
2162
  */
1875
- addResourceMethodReturnTypesFromSource({returnTypes, sourceText}) {
2163
+ addResourceMethodReturnTypesFromSource({returnTypes, sourceFile = null, sourceText}) {
1876
2164
  const classRegex = /class\s+([A-Za-z_$][\w$]*)\s+(?:extends\s+[^{]+)?\{/g
2165
+ const importAliases = this.jsDocImportAliasesFromSource(sourceText)
1877
2166
  let classMatch
1878
2167
 
1879
2168
  while ((classMatch = classRegex.exec(sourceText))) {
@@ -1905,7 +2194,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1905
2194
  const returnType = this.jsDocReturnType(jsDocMatch[1])
1906
2195
 
1907
2196
  if (returnType) {
1908
- returnTypes.set(`${className}.${methodName}`, returnType)
2197
+ returnTypes.set(`${className}.${methodName}`, {importAliases, sourceFile, type: returnType})
1909
2198
  }
1910
2199
  }
1911
2200
 
@@ -1915,11 +2204,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1915
2204
 
1916
2205
  /**
1917
2206
  * Adds resource method parameter types from source.
1918
- * @param {{parameterTypes: Map<string, Array<{name: string | null, type: string}>>, sourceText: string}} args - Arguments.
2207
+ * @param {{parameterTypes: Map<string, ResourceMethodParameterType[]>, sourceFile?: string | null, sourceText: string}} args - Arguments.
1919
2208
  * @returns {void}
1920
2209
  */
1921
- addResourceMethodParameterTypesFromSource({parameterTypes, sourceText}) {
2210
+ addResourceMethodParameterTypesFromSource({parameterTypes, sourceFile = null, sourceText}) {
1922
2211
  const classRegex = /class\s+([A-Za-z_$][\w$]*)\s+(?:extends\s+[^{]+)?\{/g
2212
+ const importAliases = this.jsDocImportAliasesFromSource(sourceText)
1923
2213
  let classMatch
1924
2214
 
1925
2215
  while ((classMatch = classRegex.exec(sourceText))) {
@@ -1950,7 +2240,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1950
2240
  const jsDocParameters = this.jsDocParameters(jsDocMatch[1])
1951
2241
 
1952
2242
  if (jsDocParameters.length > 0) {
1953
- parameterTypes.set(`${className}.${methodName}`, jsDocParameters)
2243
+ parameterTypes.set(`${className}.${methodName}`, jsDocParameters.map((parameter) => ({...parameter, importAliases, sourceFile})))
1954
2244
  }
1955
2245
  }
1956
2246
 
@@ -1958,6 +2248,41 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1958
2248
  }
1959
2249
  }
1960
2250
 
2251
+ /**
2252
+ * Runs JSDoc import aliases from source.
2253
+ * @param {string} sourceText - Source text.
2254
+ * @returns {Map<string, ResourceJsDocImportAlias>} - Import aliases keyed by local name.
2255
+ */
2256
+ jsDocImportAliasesFromSource(sourceText) {
2257
+ const importAliases = new Map()
2258
+ const importRegex = /@import\s*\{\s*([^}]+?)\s*\}\s*from\s*["']([^"']+)["']/g
2259
+ let importMatch
2260
+
2261
+ while ((importMatch = importRegex.exec(sourceText))) {
2262
+ const importList = importMatch[1]
2263
+ const specifier = importMatch[2]
2264
+
2265
+ for (const rawImportEntry of importList.split(",")) {
2266
+ const importEntry = rawImportEntry.trim()
2267
+
2268
+ if (importEntry.length < 1) continue
2269
+
2270
+ const entryMatch = importEntry.match(/^(default|[A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/)
2271
+
2272
+ if (!entryMatch) {
2273
+ throw new Error(`Could not parse JSDoc @import entry: ${importEntry}`)
2274
+ }
2275
+
2276
+ const importedName = entryMatch[1]
2277
+ const aliasName = entryMatch[2] || importedName
2278
+
2279
+ importAliases.set(aliasName, {importedName, specifier})
2280
+ }
2281
+ }
2282
+
2283
+ return importAliases
2284
+ }
2285
+
1961
2286
  /**
1962
2287
  * Runs js doc return type.
1963
2288
  * @param {string} jsDocText - JSDoc text inside comment markers.