velocious 1.0.470 → 1.0.471

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