velocious 1.0.450 → 1.0.451
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 +1 -1
- package/build/configuration-types.js +3 -1
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +785 -47
- package/build/frontend-models/base.js +1 -1
- package/build/src/configuration-types.d.ts +12 -2
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +4 -2
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +271 -13
- 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 +666 -49
- package/build/src/frontend-models/base.d.ts +1 -1
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +2 -2
- package/package.json +1 -1
- package/src/configuration-types.js +3 -1
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +785 -47
- package/src/frontend-models/base.js +1 -1
|
@@ -11,18 +11,26 @@ import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigura
|
|
|
11
11
|
* @property {string} [columnType] - Column type.
|
|
12
12
|
* @property {string} [sqlType] - SQL type.
|
|
13
13
|
* @property {string} [dataType] - Data type.
|
|
14
|
+
* @property {string} [jsDocType] - Exact JSDoc type.
|
|
14
15
|
* @property {string} [name] - Attribute name when configured as an array entry.
|
|
15
16
|
* @property {boolean} [null] - Whether null is allowed.
|
|
17
|
+
* @property {boolean} [selectedByDefault] - Whether the attribute is selected by default.
|
|
16
18
|
* @property {() => string} [getType] - Returns column type.
|
|
17
19
|
* @property {() => boolean} [getNull] - Returns whether null is allowed.
|
|
18
20
|
*/
|
|
19
21
|
/**
|
|
20
22
|
* Permit spec returned by frontend-model resources during generation.
|
|
21
|
-
* @typedef {Array<string | Record<string,
|
|
23
|
+
* @typedef {Array<string | Record<string, FrontendModelGeneratorPermitSpec>>} FrontendModelGeneratorPermitSpec
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
/** Node CLI command that generates frontend model classes from backend project resource config. */
|
|
25
27
|
export default class DbGenerateFrontendModels extends BaseCommand {
|
|
28
|
+
/** @type {Map<string, string> | null} */
|
|
29
|
+
_resourceMethodReturnTypes = null
|
|
30
|
+
|
|
31
|
+
/** @type {Map<string, string[]> | null} */
|
|
32
|
+
_resourceMethodParameterTypes = null
|
|
33
|
+
|
|
26
34
|
/**
|
|
27
35
|
* Runs execute.
|
|
28
36
|
* @returns {Promise<void>} - Resolves when files are generated.
|
|
@@ -97,7 +105,9 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
97
105
|
throw new Error(`Invalid frontend model resource definition for '${className}'`)
|
|
98
106
|
}
|
|
99
107
|
|
|
100
|
-
|
|
108
|
+
const resourceClass = frontendModelResourceClassFromDefinition(resources[modelClassName])
|
|
109
|
+
|
|
110
|
+
this.validateModelConfig({availableFrontendModelClassNames, className, modelConfig, resourceClass})
|
|
101
111
|
|
|
102
112
|
if (generatedModelNames.has(className)) {
|
|
103
113
|
throw new Error(`Duplicate frontend model definition for '${className}'`)
|
|
@@ -105,12 +115,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
105
115
|
|
|
106
116
|
generatedModelNames.add(className)
|
|
107
117
|
|
|
108
|
-
const fileContent = this.buildModelFileContent({
|
|
118
|
+
const fileContent = await this.buildModelFileContent({
|
|
109
119
|
className,
|
|
110
120
|
importPath,
|
|
111
|
-
modelClass: configuration.getModelClasses()[className],
|
|
121
|
+
modelClass: resourceClass?.ModelClass || configuration.getModelClasses()[className],
|
|
112
122
|
modelConfig,
|
|
113
|
-
resourceClass
|
|
123
|
+
resourceClass
|
|
114
124
|
})
|
|
115
125
|
|
|
116
126
|
await fs.writeFile(filePath, fileContent)
|
|
@@ -236,10 +246,10 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
236
246
|
* @param {typeof import("../../../../../database/record/index.js").default | undefined} args.modelClass - Backend model class.
|
|
237
247
|
* @param {import("../../../../../configuration-types.js").NormalizedFrontendModelResourceConfiguration} args.modelConfig - Model configuration.
|
|
238
248
|
* @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} [args.resourceClass] - Resource class.
|
|
239
|
-
* @returns {string} - Generated file content.
|
|
249
|
+
* @returns {Promise<string>} - Generated file content.
|
|
240
250
|
*/
|
|
241
|
-
buildModelFileContent({className, importPath, modelClass, modelConfig, resourceClass}) {
|
|
242
|
-
const attributes = this.attributeDefinitionsForModel({modelClass, modelConfig})
|
|
251
|
+
async buildModelFileContent({className, importPath, modelClass, modelConfig, resourceClass}) {
|
|
252
|
+
const attributes = await this.attributeDefinitionsForModel({className, modelClass, modelConfig, resourceClass})
|
|
243
253
|
const relationships = this.relationshipsForModel({className, modelConfig, resourceClass})
|
|
244
254
|
const attachments = modelConfig.attachments && typeof modelConfig.attachments === "object"
|
|
245
255
|
? modelConfig.attachments
|
|
@@ -311,8 +321,8 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
311
321
|
}
|
|
312
322
|
fileContent += " */\n"
|
|
313
323
|
}
|
|
314
|
-
fileContent += this.writeAttributesTypedef({attributes, attributesTypeName, modelClass, nestedWriteTypes, permittedParams: permittedCreateParams, typeName: createAttributesTypeName})
|
|
315
|
-
fileContent += this.writeAttributesTypedef({attributes, attributesTypeName, modelClass, nestedWriteTypes, permittedParams: permittedUpdateParams, typeName: updateAttributesTypeName})
|
|
324
|
+
fileContent += await this.writeAttributesTypedef({attributes, attributesTypeName, modelClass, nestedWriteTypes, permittedParams: permittedCreateParams, resourceClass, typeName: createAttributesTypeName})
|
|
325
|
+
fileContent += await this.writeAttributesTypedef({attributes, attributesTypeName, modelClass, nestedWriteTypes, permittedParams: permittedUpdateParams, resourceClass, typeName: updateAttributesTypeName})
|
|
316
326
|
fileContent += "/**\n"
|
|
317
327
|
fileContent += ` * Frontend model for ${className}.\n`
|
|
318
328
|
fileContent += ` * @augments {FrontendModelBase<${attributesTypeName}, ${createAttributesTypeName}, ${updateAttributesTypeName}>}\n`
|
|
@@ -418,17 +428,24 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
418
428
|
for (const attribute of attributes) {
|
|
419
429
|
const camelizedAttribute = inflection.camelize(attribute.name, true)
|
|
420
430
|
const camelizedAttributeUpper = inflection.camelize(attribute.name)
|
|
431
|
+
const attributeType = `${attributesTypeName}[${JSON.stringify(attribute.name)}]`
|
|
432
|
+
const setterAttributeType = await this.frontendWriteAttributeType({
|
|
433
|
+
attribute,
|
|
434
|
+
attributeName: attribute.name,
|
|
435
|
+
attributesTypeName,
|
|
436
|
+
resourceClass
|
|
437
|
+
})
|
|
421
438
|
|
|
422
439
|
fileContent += "\n"
|
|
423
|
-
fileContent += ` /** @returns {${
|
|
424
|
-
fileContent += ` ${camelizedAttribute}() { return /** @type {${
|
|
440
|
+
fileContent += ` /** @returns {${attributeType}} - Attribute value. */\n`
|
|
441
|
+
fileContent += ` ${camelizedAttribute}() { return /** @type {${attributeType}} */ (this.readAttribute(${JSON.stringify(attribute.name)})) }\n`
|
|
425
442
|
|
|
426
443
|
fileContent += "\n"
|
|
427
444
|
fileContent += " /**\n"
|
|
428
|
-
fileContent += ` * @param {${
|
|
429
|
-
fileContent += ` * @returns {${
|
|
445
|
+
fileContent += ` * @param {${setterAttributeType}} newValue - New attribute value.\n`
|
|
446
|
+
fileContent += ` * @returns {${setterAttributeType}} - Assigned value.\n`
|
|
430
447
|
fileContent += " */\n"
|
|
431
|
-
fileContent += ` set${camelizedAttributeUpper}(newValue) { return /** @type {${
|
|
448
|
+
fileContent += ` set${camelizedAttributeUpper}(newValue) { return /** @type {${setterAttributeType}} */ (this.setAttribute(${JSON.stringify(attribute.name)}, newValue)) }\n`
|
|
432
449
|
}
|
|
433
450
|
|
|
434
451
|
for (const methodName of Object.keys(collectionCommands)) {
|
|
@@ -597,11 +614,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
597
614
|
* @param {string} args.attributesTypeName - Generated read attributes typedef name.
|
|
598
615
|
* @param {typeof import("../../../../../database/record/index.js").default | undefined} args.modelClass - Backend model class.
|
|
599
616
|
* @param {Array<{attributes: Array<{name: string, type: string}>, relationshipName: string, typeName: string}>} args.nestedWriteTypes - Nested write typedefs.
|
|
600
|
-
* @param {
|
|
617
|
+
* @param {FrontendModelGeneratorPermitSpec} args.permittedParams - Resource permitted params spec.
|
|
618
|
+
* @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null | undefined} args.resourceClass - Resource class.
|
|
601
619
|
* @param {string} args.typeName - Typedef name.
|
|
602
|
-
* @returns {string} - Generated typedef source.
|
|
620
|
+
* @returns {Promise<string>} - Generated typedef source.
|
|
603
621
|
*/
|
|
604
|
-
writeAttributesTypedef({attributes, attributesTypeName, modelClass, nestedWriteTypes, permittedParams, typeName}) {
|
|
622
|
+
async writeAttributesTypedef({attributes, attributesTypeName, modelClass, nestedWriteTypes, permittedParams, resourceClass, typeName}) {
|
|
605
623
|
const attributeLines = []
|
|
606
624
|
|
|
607
625
|
let output = "/**\n"
|
|
@@ -618,8 +636,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
618
636
|
|
|
619
637
|
emittedAttributeNames.add(attributeName)
|
|
620
638
|
|
|
621
|
-
const
|
|
622
|
-
|
|
639
|
+
const type = await this.frontendWriteAttributeType({
|
|
640
|
+
attribute: attributesByName.get(attributeName),
|
|
641
|
+
attributeName,
|
|
642
|
+
attributesTypeName,
|
|
643
|
+
resourceClass
|
|
644
|
+
})
|
|
623
645
|
|
|
624
646
|
attributeLines.push(` * @property {${type}} [${attributeName}] - Permitted ${attributeName} value.\n`)
|
|
625
647
|
} else if (entry && typeof entry == "object" && !Array.isArray(entry)) {
|
|
@@ -644,6 +666,58 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
644
666
|
return output
|
|
645
667
|
}
|
|
646
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Runs frontend write attribute type.
|
|
671
|
+
* @param {{attribute: {jsDocType: string, name: string} | undefined, attributeName: string, attributesTypeName: string, resourceClass: import("../../../../../configuration-types.js").FrontendModelResourceClassType | null | undefined}} args - Arguments.
|
|
672
|
+
* @returns {Promise<string>} - JSDoc type for the permitted write field.
|
|
673
|
+
*/
|
|
674
|
+
async frontendWriteAttributeType({attribute, attributeName, attributesTypeName, resourceClass}) {
|
|
675
|
+
const setterParameterType = await this.frontendWriteAttributeSetterParameterType({attributeName, resourceClass})
|
|
676
|
+
|
|
677
|
+
if (setterParameterType) return `${setterParameterType} | null`
|
|
678
|
+
|
|
679
|
+
if (!attribute) return "FrontendModelAttributeValue"
|
|
680
|
+
|
|
681
|
+
if (attribute.jsDocType.trim() === "null") return "FrontendModelAttributeValue"
|
|
682
|
+
|
|
683
|
+
return `${attributesTypeName}[${JSON.stringify(attribute.name)}] | null`
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Runs frontend write attribute setter parameter type.
|
|
688
|
+
* @param {{attributeName: string, resourceClass: import("../../../../../configuration-types.js").FrontendModelResourceClassType | null | undefined}} args - Arguments.
|
|
689
|
+
* @returns {Promise<string | null>} - Setter value parameter type when it is useful for generation.
|
|
690
|
+
*/
|
|
691
|
+
async frontendWriteAttributeSetterParameterType({attributeName, resourceClass}) {
|
|
692
|
+
if (!resourceClass?.name) return null
|
|
693
|
+
|
|
694
|
+
const methodName = `set${inflection.camelize(attributeName)}Attribute`
|
|
695
|
+
const parameterType = await this.resourceMethodParameterType({
|
|
696
|
+
methodName,
|
|
697
|
+
parameterIndex: 1,
|
|
698
|
+
sourceClassName: resourceClass.name
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
if (!parameterType) return null
|
|
702
|
+
if (this.isBroadGeneratedType(parameterType)) return null
|
|
703
|
+
|
|
704
|
+
return parameterType
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Runs is broad generated type.
|
|
709
|
+
* @param {string} jsDocType - JSDoc type.
|
|
710
|
+
* @returns {boolean} - Whether the type is too broad to improve generated write typing.
|
|
711
|
+
*/
|
|
712
|
+
isBroadGeneratedType(jsDocType) {
|
|
713
|
+
const normalizedType = jsDocType.trim()
|
|
714
|
+
|
|
715
|
+
return normalizedType === "?"
|
|
716
|
+
|| normalizedType === "any"
|
|
717
|
+
|| normalizedType === "object"
|
|
718
|
+
|| normalizedType === "unknown"
|
|
719
|
+
}
|
|
720
|
+
|
|
647
721
|
/**
|
|
648
722
|
* Resolves a permitted write attribute to the generated frontend attribute name.
|
|
649
723
|
* @param {{attributeName: string, attributesByName: Map<string, {jsDocType: string, name: string}>, modelClass: typeof import("../../../../../database/record/index.js").default | undefined}} args - Arguments.
|
|
@@ -894,64 +968,172 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
894
968
|
/**
|
|
895
969
|
* Runs attribute definitions for model.
|
|
896
970
|
* @param {object} args - Arguments.
|
|
971
|
+
* @param {string} args.className - Frontend model class name.
|
|
897
972
|
* @param {typeof import("../../../../../database/record/index.js").default | undefined} args.modelClass - Backend model class.
|
|
898
973
|
* @param {import("../../../../../configuration-types.js").NormalizedFrontendModelResourceConfiguration} args.modelConfig - Model configuration.
|
|
899
|
-
* @
|
|
974
|
+
* @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} [args.resourceClass] - Resource class.
|
|
975
|
+
* @returns {Promise<Array<{jsDocType: string, name: string}>>} - Attribute definitions.
|
|
900
976
|
*/
|
|
901
|
-
attributeDefinitionsForModel({modelClass, modelConfig}) {
|
|
977
|
+
async attributeDefinitionsForModel({className, modelClass, modelConfig, resourceClass}) {
|
|
902
978
|
let attributes = modelConfig.attributes
|
|
903
979
|
|
|
904
980
|
// Auto-derive attributes from model columns when not explicitly defined
|
|
905
981
|
if ((!attributes || (Array.isArray(attributes) && attributes.length === 0)) && modelClass) {
|
|
906
|
-
|
|
907
|
-
const columns = modelClass.getColumns()
|
|
982
|
+
const columns = modelClass.getColumns()
|
|
908
983
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}
|
|
912
|
-
} catch {
|
|
913
|
-
// Model may not be initialized yet
|
|
984
|
+
if (Array.isArray(columns)) {
|
|
985
|
+
attributes = columns.map((column) => inflection.camelize(column.getName(), true))
|
|
914
986
|
}
|
|
915
987
|
}
|
|
916
988
|
|
|
917
989
|
if (Array.isArray(attributes)) {
|
|
918
|
-
|
|
990
|
+
const attributeDefinitions = []
|
|
991
|
+
|
|
992
|
+
for (const attributeDefinition of attributes) {
|
|
919
993
|
/** @type {FrontendAttributeConfig | null} */
|
|
920
|
-
let
|
|
994
|
+
let configuredAttributeConfig = null
|
|
921
995
|
let attributeName
|
|
922
996
|
|
|
923
997
|
if (typeof attributeDefinition == "string") {
|
|
924
998
|
attributeName = attributeDefinition
|
|
925
|
-
attributeConfig = this.frontendAttributeConfigForModelAttribute({attributeName, modelClass})
|
|
926
999
|
} else if (attributeDefinition && typeof attributeDefinition == "object" && !Array.isArray(attributeDefinition)) {
|
|
927
|
-
|
|
928
|
-
attributeName =
|
|
1000
|
+
configuredAttributeConfig = /** @type {FrontendAttributeConfig} */ (attributeDefinition)
|
|
1001
|
+
attributeName = configuredAttributeConfig.name
|
|
929
1002
|
}
|
|
930
1003
|
|
|
931
1004
|
if (typeof attributeName != "string" || attributeName.length < 1) {
|
|
932
1005
|
throw new Error(`Expected frontend model attribute array entries to be strings or objects with a name, got: ${JSON.stringify(attributeDefinition)}`)
|
|
933
1006
|
}
|
|
934
1007
|
|
|
935
|
-
|
|
936
|
-
|
|
1008
|
+
const attributeConfig = await this.resolvedFrontendAttributeConfig({
|
|
1009
|
+
attributeName,
|
|
1010
|
+
className,
|
|
1011
|
+
configuredAttributeConfig,
|
|
1012
|
+
modelClass,
|
|
1013
|
+
resourceClass
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
const frontendAttributeConfig = this.frontendAttributeConfigForGeneratedAttribute({
|
|
1017
|
+
attributeConfig,
|
|
1018
|
+
attributeName,
|
|
1019
|
+
modelClass
|
|
1020
|
+
})
|
|
1021
|
+
|
|
1022
|
+
attributeDefinitions.push({
|
|
1023
|
+
jsDocType: this.jsDocTypeForFrontendAttribute({attributeConfig: frontendAttributeConfig}),
|
|
937
1024
|
name: attributeName
|
|
938
|
-
}
|
|
939
|
-
}
|
|
1025
|
+
})
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return attributeDefinitions
|
|
940
1029
|
}
|
|
941
1030
|
|
|
942
1031
|
if (!attributes || typeof attributes !== "object") {
|
|
943
1032
|
throw new Error(`Expected 'attributes' as array or object but got: ${attributes}`)
|
|
944
1033
|
}
|
|
945
1034
|
|
|
946
|
-
|
|
1035
|
+
const attributeDefinitions = []
|
|
1036
|
+
|
|
1037
|
+
for (const attributeName of Object.keys(attributes)) {
|
|
947
1038
|
const attributeConfig = attributes[attributeName]
|
|
948
|
-
const
|
|
1039
|
+
const configuredAttributeConfig = attributeConfig && typeof attributeConfig === "object"
|
|
1040
|
+
? /** @type {FrontendAttributeConfig} */ (attributeConfig)
|
|
1041
|
+
: null
|
|
1042
|
+
const normalizedAttributeConfig = await this.resolvedFrontendAttributeConfig({
|
|
1043
|
+
attributeName,
|
|
1044
|
+
className,
|
|
1045
|
+
configuredAttributeConfig,
|
|
1046
|
+
modelClass,
|
|
1047
|
+
resourceClass
|
|
1048
|
+
})
|
|
1049
|
+
const frontendAttributeConfig = this.frontendAttributeConfigForGeneratedAttribute({
|
|
1050
|
+
attributeConfig: normalizedAttributeConfig,
|
|
1051
|
+
attributeName,
|
|
1052
|
+
modelClass
|
|
1053
|
+
})
|
|
949
1054
|
|
|
950
|
-
|
|
951
|
-
jsDocType: this.jsDocTypeForFrontendAttribute({attributeConfig:
|
|
1055
|
+
attributeDefinitions.push({
|
|
1056
|
+
jsDocType: this.jsDocTypeForFrontendAttribute({attributeConfig: frontendAttributeConfig}),
|
|
952
1057
|
name: attributeName
|
|
953
|
-
}
|
|
954
|
-
}
|
|
1058
|
+
})
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return attributeDefinitions
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Runs frontend attribute config for generated attribute.
|
|
1066
|
+
* @param {{attributeConfig: FrontendAttributeConfig, attributeName: string, modelClass: typeof import("../../../../../database/record/index.js").default | undefined}} args - Arguments.
|
|
1067
|
+
* @returns {FrontendAttributeConfig} - Attribute config used for generated JSDoc.
|
|
1068
|
+
*/
|
|
1069
|
+
frontendAttributeConfigForGeneratedAttribute({attributeConfig, attributeName, modelClass}) {
|
|
1070
|
+
if (!this.frontendAttributeIsModelPrimaryKey({attributeName, modelClass})) return attributeConfig
|
|
1071
|
+
|
|
1072
|
+
return {...attributeConfig, null: false}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Runs frontend attribute is model primary key.
|
|
1077
|
+
* @param {{attributeName: string, modelClass: typeof import("../../../../../database/record/index.js").default | undefined}} args - Arguments.
|
|
1078
|
+
* @returns {boolean} - Whether the attribute is the model primary key.
|
|
1079
|
+
*/
|
|
1080
|
+
frontendAttributeIsModelPrimaryKey({attributeName, modelClass}) {
|
|
1081
|
+
if (!modelClass) return false
|
|
1082
|
+
|
|
1083
|
+
const primaryKey = modelClass.primaryKey()
|
|
1084
|
+
|
|
1085
|
+
if (typeof primaryKey != "string" || primaryKey.length < 1) return false
|
|
1086
|
+
if (attributeName === primaryKey) return true
|
|
1087
|
+
|
|
1088
|
+
return modelClass.resolveAttributeName(primaryKey) === attributeName
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Resolves frontend attribute config from explicit metadata, resource methods, model columns, translated columns, or model accessor JSDoc.
|
|
1093
|
+
* @param {object} args - Arguments.
|
|
1094
|
+
* @param {string} args.attributeName - Frontend attribute name.
|
|
1095
|
+
* @param {string} args.className - Frontend model class name.
|
|
1096
|
+
* @param {FrontendAttributeConfig | null} args.configuredAttributeConfig - Resource-provided attribute config.
|
|
1097
|
+
* @param {typeof import("../../../../../database/record/index.js").default | undefined} args.modelClass - Backend model class.
|
|
1098
|
+
* @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null | undefined} args.resourceClass - Resource class.
|
|
1099
|
+
* @returns {Promise<FrontendAttributeConfig>} - Resolved frontend attribute config.
|
|
1100
|
+
*/
|
|
1101
|
+
async resolvedFrontendAttributeConfig({attributeName, className, configuredAttributeConfig, modelClass, resourceClass}) {
|
|
1102
|
+
const inferredResourceConfig = await this.frontendAttributeConfigForResourceAttribute({attributeName, resourceClass})
|
|
1103
|
+
const inferredColumnConfig = inferredResourceConfig
|
|
1104
|
+
? null
|
|
1105
|
+
: this.frontendAttributeConfigForModelAttribute({attributeName, modelClass})
|
|
1106
|
+
const inferredTranslatedConfig = inferredResourceConfig || inferredColumnConfig
|
|
1107
|
+
? null
|
|
1108
|
+
: this.frontendAttributeConfigForTranslatedAttribute({attributeName, modelClass, resourceClass})
|
|
1109
|
+
const inferredModelAccessorConfig = inferredResourceConfig || inferredColumnConfig || inferredTranslatedConfig
|
|
1110
|
+
? null
|
|
1111
|
+
: await this.frontendAttributeConfigForModelAccessor({attributeName, modelClass})
|
|
1112
|
+
const inferredConfig = inferredResourceConfig || inferredColumnConfig || inferredTranslatedConfig || inferredModelAccessorConfig
|
|
1113
|
+
|
|
1114
|
+
if (configuredAttributeConfig && this.frontendAttributeConfigHasType(configuredAttributeConfig)) {
|
|
1115
|
+
return inferredConfig
|
|
1116
|
+
? {...inferredConfig, ...configuredAttributeConfig}
|
|
1117
|
+
: configuredAttributeConfig
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (inferredConfig) {
|
|
1121
|
+
return configuredAttributeConfig
|
|
1122
|
+
? {...inferredConfig, ...configuredAttributeConfig}
|
|
1123
|
+
: inferredConfig
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
throw new Error(`Could not infer JSDoc type for frontend model attribute '${className}#${attributeName}'. Add a backend model column, translation table column, explicit resource metadata, or a @returns JSDoc type on ${resourceClass?.name || "the resource"}.${attributeName}Attribute().`)
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Runs frontend attribute config has type.
|
|
1131
|
+
* @param {FrontendAttributeConfig | null | undefined} attributeConfig - Attribute config.
|
|
1132
|
+
* @returns {boolean} - Whether the config declares a type source.
|
|
1133
|
+
*/
|
|
1134
|
+
frontendAttributeConfigHasType(attributeConfig) {
|
|
1135
|
+
return typeof this.frontendAttributeTypeValue(attributeConfig) == "string"
|
|
1136
|
+
|| typeof attributeConfig?.jsDocType == "string"
|
|
955
1137
|
}
|
|
956
1138
|
|
|
957
1139
|
/**
|
|
@@ -961,6 +1143,10 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
961
1143
|
* @returns {string} - JSDoc type.
|
|
962
1144
|
*/
|
|
963
1145
|
jsDocTypeForFrontendAttribute({attributeConfig}) {
|
|
1146
|
+
if (attributeConfig && typeof attributeConfig.jsDocType == "string" && attributeConfig.jsDocType.length > 0) {
|
|
1147
|
+
return attributeConfig.jsDocType
|
|
1148
|
+
}
|
|
1149
|
+
|
|
964
1150
|
const jsDocType = this.jsDocTypeForFrontendAttributeBaseType(attributeConfig)
|
|
965
1151
|
|
|
966
1152
|
if (!this.frontendAttributeCanBeNull(attributeConfig)) {
|
|
@@ -986,7 +1172,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
986
1172
|
return "boolean"
|
|
987
1173
|
} else if (type == "json" || type == "jsonb") {
|
|
988
1174
|
return "FrontendModelTransportValue"
|
|
989
|
-
} else if (type && ["blob", "char", "nvarchar", "varchar", "text", "longtext", "uuid", "character varying"].includes(type)) {
|
|
1175
|
+
} else if (type && ["blob", "char", "nvarchar", "varchar", "text", "longtext", "mediumtext", "tinytext", "uuid", "character varying"].includes(type)) {
|
|
990
1176
|
return "string"
|
|
991
1177
|
} else if (type && ["bit", "bigint", "decimal", "double", "double precision", "float", "int", "integer", "numeric", "real", "smallint", "tinyint"].includes(type)) {
|
|
992
1178
|
return "number"
|
|
@@ -1037,6 +1223,517 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1037
1223
|
return typeValue
|
|
1038
1224
|
}
|
|
1039
1225
|
|
|
1226
|
+
/**
|
|
1227
|
+
* Runs frontend attribute config for resource attribute.
|
|
1228
|
+
* @param {object} args - Arguments.
|
|
1229
|
+
* @param {string} args.attributeName - Frontend model attribute name.
|
|
1230
|
+
* @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null | undefined} args.resourceClass - Resource class.
|
|
1231
|
+
* @returns {Promise<FrontendAttributeConfig | null>} - Attribute config inferred from resource method JSDoc.
|
|
1232
|
+
*/
|
|
1233
|
+
async frontendAttributeConfigForResourceAttribute({attributeName, resourceClass}) {
|
|
1234
|
+
if (!resourceClass) return null
|
|
1235
|
+
|
|
1236
|
+
const methodName = `${attributeName}Attribute`
|
|
1237
|
+
const ownerClassName = this.methodOwnerClassName({methodName, targetClass: resourceClass})
|
|
1238
|
+
|
|
1239
|
+
if (!ownerClassName) return null
|
|
1240
|
+
|
|
1241
|
+
const jsDocType = await this.resourceMethodReturnType({
|
|
1242
|
+
methodName,
|
|
1243
|
+
sourceClassName: ownerClassName
|
|
1244
|
+
})
|
|
1245
|
+
|
|
1246
|
+
return jsDocType ? {jsDocType: this.unwrappedPromiseJsDocType({jsDocType})} : null
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Runs frontend attribute config for translated attribute.
|
|
1251
|
+
* @param {object} args - Arguments.
|
|
1252
|
+
* @param {string} args.attributeName - Frontend model attribute name.
|
|
1253
|
+
* @param {typeof import("../../../../../database/record/index.js").default | undefined} args.modelClass - Backend model class.
|
|
1254
|
+
* @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null | undefined} args.resourceClass - Resource class.
|
|
1255
|
+
* @returns {FrontendAttributeConfig | null} - Attribute config inferred from translated attribute columns.
|
|
1256
|
+
*/
|
|
1257
|
+
frontendAttributeConfigForTranslatedAttribute({attributeName, modelClass, resourceClass}) {
|
|
1258
|
+
if (!modelClass) return null
|
|
1259
|
+
if (!this.frontendAttributeIsTranslated({attributeName, modelClass, resourceClass})) return null
|
|
1260
|
+
|
|
1261
|
+
const TranslationClass = modelClass.getTranslationClass()
|
|
1262
|
+
const columnName = inflection.underscore(attributeName)
|
|
1263
|
+
|
|
1264
|
+
let column
|
|
1265
|
+
|
|
1266
|
+
try {
|
|
1267
|
+
column = TranslationClass.getColumnsHash()[columnName]
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
if (error instanceof Error && (error.message.includes("hasn't been initialized yet") || error.message.includes("used before initialization"))) return null
|
|
1270
|
+
|
|
1271
|
+
throw error
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
return column ? this.frontendAttributeConfigForColumn({column}) : null
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Runs frontend attribute is translated.
|
|
1279
|
+
* @param {object} args - Arguments.
|
|
1280
|
+
* @param {string} args.attributeName - Frontend model attribute name.
|
|
1281
|
+
* @param {typeof import("../../../../../database/record/index.js").default} args.modelClass - Backend model class.
|
|
1282
|
+
* @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null | undefined} args.resourceClass - Resource class.
|
|
1283
|
+
* @returns {boolean} - Whether the frontend attribute is translated.
|
|
1284
|
+
*/
|
|
1285
|
+
frontendAttributeIsTranslated({attributeName, modelClass, resourceClass}) {
|
|
1286
|
+
if (resourceClass) {
|
|
1287
|
+
const translatedAttributes = resourceClass.translatedAttributes
|
|
1288
|
+
|
|
1289
|
+
if (Array.isArray(translatedAttributes) && translatedAttributes.includes(attributeName)) return true
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const translations = modelClass._translations
|
|
1293
|
+
|
|
1294
|
+
return Boolean(translations && typeof translations == "object" && Object.prototype.hasOwnProperty.call(translations, attributeName))
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Runs frontend attribute config for model accessor.
|
|
1299
|
+
* @param {object} args - Arguments.
|
|
1300
|
+
* @param {string} args.attributeName - Frontend model attribute name.
|
|
1301
|
+
* @param {typeof import("../../../../../database/record/index.js").default | undefined} args.modelClass - Backend model class.
|
|
1302
|
+
* @returns {Promise<FrontendAttributeConfig | null>} - Attribute config inferred from model accessor JSDoc.
|
|
1303
|
+
*/
|
|
1304
|
+
async frontendAttributeConfigForModelAccessor({attributeName, modelClass}) {
|
|
1305
|
+
if (!modelClass) return null
|
|
1306
|
+
|
|
1307
|
+
const ownerClassName = this.methodOwnerClassName({methodName: attributeName, targetClass: modelClass})
|
|
1308
|
+
|
|
1309
|
+
if (!ownerClassName) return null
|
|
1310
|
+
|
|
1311
|
+
const jsDocType = await this.resourceMethodReturnType({
|
|
1312
|
+
methodName: attributeName,
|
|
1313
|
+
sourceClassName: ownerClassName
|
|
1314
|
+
})
|
|
1315
|
+
|
|
1316
|
+
return jsDocType ? {jsDocType} : null
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Runs unwrapped promise js doc type.
|
|
1321
|
+
* @param {object} args - Arguments.
|
|
1322
|
+
* @param {string} args.jsDocType - JSDoc type to normalize.
|
|
1323
|
+
* @returns {string} - The resolved value type for serialized frontend attributes.
|
|
1324
|
+
*/
|
|
1325
|
+
unwrappedPromiseJsDocType({jsDocType}) {
|
|
1326
|
+
const promisePrefix = "Promise<"
|
|
1327
|
+
|
|
1328
|
+
if (!jsDocType.startsWith(promisePrefix)) return jsDocType
|
|
1329
|
+
|
|
1330
|
+
if (!jsDocType.endsWith(">")) {
|
|
1331
|
+
throw new Error(`Expected Promise JSDoc type to end with '>': ${jsDocType}`)
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const resolvedType = jsDocType.slice(promisePrefix.length, -1).trim()
|
|
1335
|
+
|
|
1336
|
+
if (resolvedType.length < 1) {
|
|
1337
|
+
throw new Error(`Expected Promise JSDoc type to contain a resolved type: ${jsDocType}`)
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
return resolvedType
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Runs method owner class name.
|
|
1345
|
+
* @param {object} args - Arguments.
|
|
1346
|
+
* @param {string} args.methodName - Method name.
|
|
1347
|
+
* @param {typeof import("../../../../../database/record/index.js").default | import("../../../../../configuration-types.js").FrontendModelResourceClassType} args.targetClass - Target class.
|
|
1348
|
+
* @returns {string | null} - Class name that declares the method.
|
|
1349
|
+
*/
|
|
1350
|
+
methodOwnerClassName({methodName, targetClass}) {
|
|
1351
|
+
let prototype = targetClass.prototype
|
|
1352
|
+
|
|
1353
|
+
while (prototype && prototype !== Object.prototype) {
|
|
1354
|
+
if (Object.prototype.hasOwnProperty.call(prototype, methodName)) {
|
|
1355
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, methodName)
|
|
1356
|
+
|
|
1357
|
+
if (typeof descriptor?.value != "function") return null
|
|
1358
|
+
|
|
1359
|
+
const constructorName = prototype.constructor?.name
|
|
1360
|
+
|
|
1361
|
+
if (typeof constructorName == "string" && constructorName.length > 0) return constructorName
|
|
1362
|
+
|
|
1363
|
+
return null
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
prototype = Object.getPrototypeOf(prototype)
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
return null
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Runs resource method return type.
|
|
1374
|
+
* @param {object} args - Arguments.
|
|
1375
|
+
* @param {string} args.methodName - Method name.
|
|
1376
|
+
* @param {string} args.sourceClassName - Source class name.
|
|
1377
|
+
* @returns {Promise<string | null>} - JSDoc return type when documented.
|
|
1378
|
+
*/
|
|
1379
|
+
async resourceMethodReturnType({methodName, sourceClassName}) {
|
|
1380
|
+
const resourceMethodReturnTypes = await this.resourceMethodReturnTypes()
|
|
1381
|
+
const returnTypeKey = `${sourceClassName}.${methodName}`
|
|
1382
|
+
|
|
1383
|
+
if (!resourceMethodReturnTypes.has(returnTypeKey)) return null
|
|
1384
|
+
|
|
1385
|
+
const returnType = resourceMethodReturnTypes.get(returnTypeKey)
|
|
1386
|
+
|
|
1387
|
+
if (typeof returnType != "string" || returnType.length < 1) {
|
|
1388
|
+
throw new Error(`Expected non-empty JSDoc return type for ${returnTypeKey}`)
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
return returnType
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* Runs resource method parameter type.
|
|
1396
|
+
* @param {{methodName: string, parameterIndex: number, sourceClassName: string}} args - Arguments.
|
|
1397
|
+
* @returns {Promise<string | null>} - JSDoc parameter type when documented.
|
|
1398
|
+
*/
|
|
1399
|
+
async resourceMethodParameterType({methodName, parameterIndex, sourceClassName}) {
|
|
1400
|
+
const resourceMethodParameterTypes = await this.resourceMethodParameterTypes()
|
|
1401
|
+
const parameterTypesKey = `${sourceClassName}.${methodName}`
|
|
1402
|
+
|
|
1403
|
+
if (!resourceMethodParameterTypes.has(parameterTypesKey)) return null
|
|
1404
|
+
|
|
1405
|
+
const parameterTypes = resourceMethodParameterTypes.get(parameterTypesKey)
|
|
1406
|
+
|
|
1407
|
+
if (!parameterTypes) {
|
|
1408
|
+
throw new Error(`Expected JSDoc parameter types for ${parameterTypesKey}`)
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
const parameterType = parameterTypes[parameterIndex]
|
|
1412
|
+
|
|
1413
|
+
if (parameterType === undefined) return null
|
|
1414
|
+
|
|
1415
|
+
if (parameterType.length < 1) {
|
|
1416
|
+
throw new Error(`Expected non-empty JSDoc parameter type for ${parameterTypesKey} parameter ${parameterIndex}`)
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
return parameterType
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Runs resource method return types.
|
|
1424
|
+
* @returns {Promise<Map<string, string>>} - Resource method return types keyed by ClassName.methodName.
|
|
1425
|
+
*/
|
|
1426
|
+
async resourceMethodReturnTypes() {
|
|
1427
|
+
if (this._resourceMethodReturnTypes) return this._resourceMethodReturnTypes
|
|
1428
|
+
|
|
1429
|
+
const sourceFiles = await this.frontendModelJsDocSourceFiles()
|
|
1430
|
+
const returnTypes = new Map()
|
|
1431
|
+
|
|
1432
|
+
for (const sourceFile of sourceFiles) {
|
|
1433
|
+
const sourceText = await fs.readFile(sourceFile, "utf8")
|
|
1434
|
+
|
|
1435
|
+
this.addResourceMethodReturnTypesFromSource({returnTypes, sourceText})
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
this._resourceMethodReturnTypes = returnTypes
|
|
1439
|
+
|
|
1440
|
+
return returnTypes
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* Runs resource method parameter types.
|
|
1445
|
+
* @returns {Promise<Map<string, string[]>>} - Resource method parameter types keyed by ClassName.methodName.
|
|
1446
|
+
*/
|
|
1447
|
+
async resourceMethodParameterTypes() {
|
|
1448
|
+
if (this._resourceMethodParameterTypes) return this._resourceMethodParameterTypes
|
|
1449
|
+
|
|
1450
|
+
const sourceFiles = await this.frontendModelJsDocSourceFiles()
|
|
1451
|
+
const parameterTypes = new Map()
|
|
1452
|
+
|
|
1453
|
+
for (const sourceFile of sourceFiles) {
|
|
1454
|
+
const sourceText = await fs.readFile(sourceFile, "utf8")
|
|
1455
|
+
|
|
1456
|
+
this.addResourceMethodParameterTypesFromSource({parameterTypes, sourceText})
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
this._resourceMethodParameterTypes = parameterTypes
|
|
1460
|
+
|
|
1461
|
+
return parameterTypes
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Runs frontend model JSDoc source files.
|
|
1466
|
+
* @returns {Promise<string[]>} - JavaScript source files that can define frontend-model resources and model accessors.
|
|
1467
|
+
*/
|
|
1468
|
+
async frontendModelJsDocSourceFiles() {
|
|
1469
|
+
const sourceFiles = []
|
|
1470
|
+
|
|
1471
|
+
for (const sourceDirectory of this.frontendModelJsDocSourceDirectories()) {
|
|
1472
|
+
sourceFiles.push(...await this.javascriptFilesInDirectory(sourceDirectory))
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
return sourceFiles
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* Runs frontend model JSDoc source directories.
|
|
1480
|
+
* @returns {string[]} - Source directories to scan for generated frontend-model JSDoc.
|
|
1481
|
+
*/
|
|
1482
|
+
frontendModelJsDocSourceDirectories() {
|
|
1483
|
+
const sourceDirectories = new Set([path.join(this.directory(), "src")])
|
|
1484
|
+
|
|
1485
|
+
for (const backendProject of this.getConfiguration().getBackendProjects()) {
|
|
1486
|
+
if (typeof backendProject.path == "string" && backendProject.path.length > 0) {
|
|
1487
|
+
sourceDirectories.add(path.join(backendProject.path, "src"))
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
return Array.from(sourceDirectories)
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* Adds resource method return types from source.
|
|
1496
|
+
* @param {object} args - Arguments.
|
|
1497
|
+
* @param {Map<string, string>} args.returnTypes - Mutable return types map.
|
|
1498
|
+
* @param {string} args.sourceText - Source text.
|
|
1499
|
+
* @returns {void}
|
|
1500
|
+
*/
|
|
1501
|
+
addResourceMethodReturnTypesFromSource({returnTypes, sourceText}) {
|
|
1502
|
+
const classRegex = /class\s+([A-Za-z_$][\w$]*)\s+(?:extends\s+[^{]+)?\{/g
|
|
1503
|
+
let classMatch
|
|
1504
|
+
|
|
1505
|
+
while ((classMatch = classRegex.exec(sourceText))) {
|
|
1506
|
+
const className = classMatch[1]
|
|
1507
|
+
const classBodyStart = classRegex.lastIndex
|
|
1508
|
+
const classBodyEnd = this.matchingBraceIndex({openIndex: classBodyStart - 1, sourceText})
|
|
1509
|
+
|
|
1510
|
+
if (classBodyEnd == null) {
|
|
1511
|
+
throw new Error(`Could not find closing brace for resource class '${className}' while reading frontend attribute JSDoc`)
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
const classBody = sourceText.slice(classBodyStart, classBodyEnd)
|
|
1515
|
+
const jsDocRegex = /\/\*\*([\s\S]*?)\*\//g
|
|
1516
|
+
let jsDocMatch
|
|
1517
|
+
|
|
1518
|
+
while ((jsDocMatch = jsDocRegex.exec(classBody))) {
|
|
1519
|
+
const sourceAfterJsDoc = classBody.slice(jsDocRegex.lastIndex)
|
|
1520
|
+
const methodMatch = sourceAfterJsDoc.match(/^\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(/)
|
|
1521
|
+
|
|
1522
|
+
if (!methodMatch) continue
|
|
1523
|
+
|
|
1524
|
+
const methodName = methodMatch[1]
|
|
1525
|
+
|
|
1526
|
+
const returnType = this.jsDocReturnType(jsDocMatch[1])
|
|
1527
|
+
|
|
1528
|
+
if (returnType) {
|
|
1529
|
+
returnTypes.set(`${className}.${methodName}`, returnType)
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
classRegex.lastIndex = classBodyEnd + 1
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* Adds resource method parameter types from source.
|
|
1539
|
+
* @param {{parameterTypes: Map<string, string[]>, sourceText: string}} args - Arguments.
|
|
1540
|
+
* @returns {void}
|
|
1541
|
+
*/
|
|
1542
|
+
addResourceMethodParameterTypesFromSource({parameterTypes, sourceText}) {
|
|
1543
|
+
const classRegex = /class\s+([A-Za-z_$][\w$]*)\s+(?:extends\s+[^{]+)?\{/g
|
|
1544
|
+
let classMatch
|
|
1545
|
+
|
|
1546
|
+
while ((classMatch = classRegex.exec(sourceText))) {
|
|
1547
|
+
const className = classMatch[1]
|
|
1548
|
+
const classBodyStart = classRegex.lastIndex
|
|
1549
|
+
const classBodyEnd = this.matchingBraceIndex({openIndex: classBodyStart - 1, sourceText})
|
|
1550
|
+
|
|
1551
|
+
if (classBodyEnd == null) {
|
|
1552
|
+
throw new Error(`Could not find closing brace for resource class '${className}' while reading frontend attribute JSDoc`)
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const classBody = sourceText.slice(classBodyStart, classBodyEnd)
|
|
1556
|
+
const jsDocRegex = /\/\*\*([\s\S]*?)\*\//g
|
|
1557
|
+
let jsDocMatch
|
|
1558
|
+
|
|
1559
|
+
while ((jsDocMatch = jsDocRegex.exec(classBody))) {
|
|
1560
|
+
const sourceAfterJsDoc = classBody.slice(jsDocRegex.lastIndex)
|
|
1561
|
+
const methodMatch = sourceAfterJsDoc.match(/^\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(/)
|
|
1562
|
+
|
|
1563
|
+
if (!methodMatch) continue
|
|
1564
|
+
|
|
1565
|
+
const methodName = methodMatch[1]
|
|
1566
|
+
const jsDocParameterTypes = this.jsDocParameterTypes(jsDocMatch[1])
|
|
1567
|
+
|
|
1568
|
+
if (jsDocParameterTypes.length > 0) {
|
|
1569
|
+
parameterTypes.set(`${className}.${methodName}`, jsDocParameterTypes)
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
classRegex.lastIndex = classBodyEnd + 1
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Runs js doc return type.
|
|
1579
|
+
* @param {string} jsDocText - JSDoc text inside comment markers.
|
|
1580
|
+
* @returns {string | null} - JSDoc return type when present.
|
|
1581
|
+
*/
|
|
1582
|
+
jsDocReturnType(jsDocText) {
|
|
1583
|
+
const returnsMatch = jsDocText.match(/@returns?\s*\{/)
|
|
1584
|
+
|
|
1585
|
+
if (!returnsMatch || returnsMatch.index == null) return null
|
|
1586
|
+
|
|
1587
|
+
const typeOpenIndex = returnsMatch.index + returnsMatch[0].length - 1
|
|
1588
|
+
const typeCloseIndex = this.matchingBraceIndex({openIndex: typeOpenIndex, sourceText: jsDocText})
|
|
1589
|
+
|
|
1590
|
+
if (typeCloseIndex == null) {
|
|
1591
|
+
throw new Error(`Could not parse JSDoc return type from: ${jsDocText}`)
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
const returnType = jsDocText.slice(typeOpenIndex + 1, typeCloseIndex).trim()
|
|
1595
|
+
|
|
1596
|
+
if (returnType.length < 1) {
|
|
1597
|
+
throw new Error(`Expected non-empty JSDoc return type in: ${jsDocText}`)
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
return returnType
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* Runs js doc parameter types.
|
|
1605
|
+
* @param {string} jsDocText - JSDoc text inside comment markers.
|
|
1606
|
+
* @returns {string[]} - JSDoc parameter types in declaration order.
|
|
1607
|
+
*/
|
|
1608
|
+
jsDocParameterTypes(jsDocText) {
|
|
1609
|
+
const parameterTypes = []
|
|
1610
|
+
const paramRegex = /@param\s*\{/g
|
|
1611
|
+
let _paramMatch
|
|
1612
|
+
|
|
1613
|
+
while ((_paramMatch = paramRegex.exec(jsDocText))) {
|
|
1614
|
+
const typeOpenIndex = paramRegex.lastIndex - 1
|
|
1615
|
+
const typeCloseIndex = this.matchingBraceIndex({openIndex: typeOpenIndex, sourceText: jsDocText})
|
|
1616
|
+
|
|
1617
|
+
if (typeCloseIndex == null) {
|
|
1618
|
+
throw new Error(`Could not parse JSDoc parameter type from: ${jsDocText}`)
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const parameterType = jsDocText.slice(typeOpenIndex + 1, typeCloseIndex).trim()
|
|
1622
|
+
|
|
1623
|
+
if (parameterType.length < 1) {
|
|
1624
|
+
throw new Error(`Expected non-empty JSDoc parameter type in: ${jsDocText}`)
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
parameterTypes.push(parameterType)
|
|
1628
|
+
paramRegex.lastIndex = typeCloseIndex + 1
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
return parameterTypes
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* Runs javascript files in directory.
|
|
1636
|
+
* @param {string} directory - Directory path.
|
|
1637
|
+
* @returns {Promise<string[]>} - JavaScript source file paths.
|
|
1638
|
+
*/
|
|
1639
|
+
async javascriptFilesInDirectory(directory) {
|
|
1640
|
+
let entries
|
|
1641
|
+
|
|
1642
|
+
try {
|
|
1643
|
+
entries = await fs.readdir(directory, {withFileTypes: true})
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
if (error && typeof error == "object" && "code" in error && error.code === "ENOENT") return []
|
|
1646
|
+
|
|
1647
|
+
throw error
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const filePaths = []
|
|
1651
|
+
|
|
1652
|
+
for (const entry of entries) {
|
|
1653
|
+
const entryPath = path.join(directory, entry.name)
|
|
1654
|
+
|
|
1655
|
+
if (entry.isDirectory()) {
|
|
1656
|
+
filePaths.push(...await this.javascriptFilesInDirectory(entryPath))
|
|
1657
|
+
} else if (entry.isFile() && /\.(mjs|js|jsx|ts)$/.test(entry.name)) {
|
|
1658
|
+
filePaths.push(entryPath)
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
return filePaths
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/**
|
|
1666
|
+
* Finds a matching closing brace while respecting JavaScript strings and comments.
|
|
1667
|
+
* @param {object} args - Arguments.
|
|
1668
|
+
* @param {number} args.openIndex - Opening brace index.
|
|
1669
|
+
* @param {string} args.sourceText - Source text.
|
|
1670
|
+
* @returns {number | null} - Closing brace index when found.
|
|
1671
|
+
*/
|
|
1672
|
+
matchingBraceIndex({openIndex, sourceText}) {
|
|
1673
|
+
if (sourceText[openIndex] !== "{") {
|
|
1674
|
+
throw new Error(`Expected opening brace at index ${openIndex}`)
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
let depth = 0
|
|
1678
|
+
let inBlockComment = false
|
|
1679
|
+
let inLineComment = false
|
|
1680
|
+
let inString = ""
|
|
1681
|
+
|
|
1682
|
+
for (let index = openIndex; index < sourceText.length; index++) {
|
|
1683
|
+
const char = sourceText[index]
|
|
1684
|
+
const nextChar = sourceText[index + 1]
|
|
1685
|
+
const previousChar = sourceText[index - 1]
|
|
1686
|
+
|
|
1687
|
+
if (inLineComment) {
|
|
1688
|
+
if (char === "\n") inLineComment = false
|
|
1689
|
+
|
|
1690
|
+
continue
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
if (inBlockComment) {
|
|
1694
|
+
if (char === "*" && nextChar === "/") {
|
|
1695
|
+
inBlockComment = false
|
|
1696
|
+
index++
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
continue
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
if (inString) {
|
|
1703
|
+
if (char === inString && previousChar !== "\\") inString = ""
|
|
1704
|
+
|
|
1705
|
+
continue
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
if (char === "/" && nextChar === "/") {
|
|
1709
|
+
inLineComment = true
|
|
1710
|
+
index++
|
|
1711
|
+
continue
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
if (char === "/" && nextChar === "*") {
|
|
1715
|
+
inBlockComment = true
|
|
1716
|
+
index++
|
|
1717
|
+
continue
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
1721
|
+
inString = char
|
|
1722
|
+
continue
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (char === "{") {
|
|
1726
|
+
depth++
|
|
1727
|
+
} else if (char === "}") {
|
|
1728
|
+
depth--
|
|
1729
|
+
|
|
1730
|
+
if (depth === 0) return index
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
return null
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1040
1737
|
/**
|
|
1041
1738
|
* Runs frontend attribute config for model attribute.
|
|
1042
1739
|
* @param {object} args - Arguments.
|
|
@@ -1049,13 +1746,54 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1049
1746
|
return null
|
|
1050
1747
|
}
|
|
1051
1748
|
|
|
1052
|
-
const
|
|
1749
|
+
const resolvedAttributeName = modelClass.resolveAttributeName(attributeName)
|
|
1750
|
+
|
|
1751
|
+
if (!resolvedAttributeName) return null
|
|
1752
|
+
|
|
1753
|
+
let columnName
|
|
1754
|
+
|
|
1755
|
+
try {
|
|
1756
|
+
columnName = modelClass.getAttributeNameToColumnNameMap()[resolvedAttributeName]
|
|
1757
|
+
} catch (error) {
|
|
1758
|
+
if (error instanceof Error && error.message.includes("used before initialization")) return null
|
|
1759
|
+
|
|
1760
|
+
throw error
|
|
1761
|
+
}
|
|
1053
1762
|
|
|
1054
1763
|
if (!columnName) {
|
|
1055
1764
|
return null
|
|
1056
1765
|
}
|
|
1057
1766
|
|
|
1058
|
-
|
|
1767
|
+
let column
|
|
1768
|
+
|
|
1769
|
+
try {
|
|
1770
|
+
column = modelClass.getColumnsHash()[columnName]
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
if (error instanceof Error && error.message.includes("used before initialization")) return null
|
|
1773
|
+
|
|
1774
|
+
throw error
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
return column ? this.frontendAttributeConfigForColumn({column}) : null
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/**
|
|
1781
|
+
* Runs frontend attribute config for column.
|
|
1782
|
+
* @param {object} args - Arguments.
|
|
1783
|
+
* @param {import("../../../../../database/drivers/base-column.js").default} args.column - Database column.
|
|
1784
|
+
* @returns {FrontendAttributeConfig} - Attribute config inferred from the database column.
|
|
1785
|
+
*/
|
|
1786
|
+
frontendAttributeConfigForColumn({column}) {
|
|
1787
|
+
const type = column.getType()
|
|
1788
|
+
|
|
1789
|
+
if (typeof type != "string" || type.length < 1) {
|
|
1790
|
+
throw new Error(`Expected non-empty column type for frontend model attribute inference, got: ${type}`)
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
return {
|
|
1794
|
+
null: column.getNull(),
|
|
1795
|
+
type
|
|
1796
|
+
}
|
|
1059
1797
|
}
|
|
1060
1798
|
|
|
1061
1799
|
/**
|