d365fo-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/bridge/bridgeAdapter.d.ts +319 -0
- package/dist/bridge/bridgeAdapter.d.ts.map +1 -0
- package/dist/bridge/bridgeAdapter.js +2003 -0
- package/dist/bridge/bridgeAdapter.js.map +1 -0
- package/dist/bridge/bridgeClient.d.ts +255 -0
- package/dist/bridge/bridgeClient.d.ts.map +1 -0
- package/dist/bridge/bridgeClient.js +741 -0
- package/dist/bridge/bridgeClient.js.map +1 -0
- package/dist/bridge/bridgeTypes.d.ts +561 -0
- package/dist/bridge/bridgeTypes.d.ts.map +1 -0
- package/dist/bridge/bridgeTypes.js +6 -0
- package/dist/bridge/bridgeTypes.js.map +1 -0
- package/dist/bridge/debouncedRefresh.d.ts +16 -0
- package/dist/bridge/debouncedRefresh.d.ts.map +1 -0
- package/dist/bridge/debouncedRefresh.js +77 -0
- package/dist/bridge/debouncedRefresh.js.map +1 -0
- package/dist/bridge/index.d.ts +21 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +20 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/cli/commands/config.d.ts +8 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +77 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +177 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/indexCmd.d.ts +8 -0
- package/dist/cli/commands/indexCmd.d.ts.map +1 -0
- package/dist/cli/commands/indexCmd.js +78 -0
- package/dist/cli/commands/indexCmd.js.map +1 -0
- package/dist/cli/commands/instance.d.ts +4 -0
- package/dist/cli/commands/instance.d.ts.map +1 -0
- package/dist/cli/commands/instance.js +183 -0
- package/dist/cli/commands/instance.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +4 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +246 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/start.d.ts +2 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +49 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/update.d.ts +4 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +55 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/context.d.ts +26 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/context.js +38 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/envFile.d.ts +23 -0
- package/dist/cli/envFile.d.ts.map +1 -0
- package/dist/cli/envFile.js +79 -0
- package/dist/cli/envFile.js.map +1 -0
- package/dist/cli/exec.d.ts +13 -0
- package/dist/cli/exec.d.ts.map +1 -0
- package/dist/cli/exec.js +47 -0
- package/dist/cli/exec.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +122 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/instances.d.ts +19 -0
- package/dist/cli/instances.d.ts.map +1 -0
- package/dist/cli/instances.js +60 -0
- package/dist/cli/instances.js.map +1 -0
- package/dist/cli/mcpJson.d.ts +15 -0
- package/dist/cli/mcpJson.d.ts.map +1 -0
- package/dist/cli/mcpJson.js +44 -0
- package/dist/cli/mcpJson.js.map +1 -0
- package/dist/cli/settingsPrompt.d.ts +20 -0
- package/dist/cli/settingsPrompt.d.ts.map +1 -0
- package/dist/cli/settingsPrompt.js +150 -0
- package/dist/cli/settingsPrompt.js.map +1 -0
- package/dist/cli/settingsStore.d.ts +47 -0
- package/dist/cli/settingsStore.d.ts.map +1 -0
- package/dist/cli/settingsStore.js +144 -0
- package/dist/cli/settingsStore.js.map +1 -0
- package/dist/cli/target.d.ts +23 -0
- package/dist/cli/target.d.ts.map +1 -0
- package/dist/cli/target.js +62 -0
- package/dist/cli/target.js.map +1 -0
- package/dist/cli/ui.d.ts +26 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +48 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/cli/xppConfig.d.ts +35 -0
- package/dist/cli/xppConfig.d.ts.map +1 -0
- package/dist/cli/xppConfig.js +97 -0
- package/dist/cli/xppConfig.js.map +1 -0
- package/dist/config/configFile.d.ts +50 -0
- package/dist/config/configFile.d.ts.map +1 -0
- package/dist/config/configFile.js +152 -0
- package/dist/config/configFile.js.map +1 -0
- package/dist/config/settings.d.ts +58 -0
- package/dist/config/settings.d.ts.map +1 -0
- package/dist/config/settings.js +641 -0
- package/dist/config/settings.js.map +1 -0
- package/dist/database/download.d.ts +27 -0
- package/dist/database/download.d.ts.map +1 -0
- package/dist/database/download.js +241 -0
- package/dist/database/download.js.map +1 -0
- package/dist/eval/audit/knowledgeAudit.d.ts +66 -0
- package/dist/eval/audit/knowledgeAudit.d.ts.map +1 -0
- package/dist/eval/audit/knowledgeAudit.js +117 -0
- package/dist/eval/audit/knowledgeAudit.js.map +1 -0
- package/dist/eval/audit/knowledgeAuditCli.d.ts +17 -0
- package/dist/eval/audit/knowledgeAuditCli.d.ts.map +1 -0
- package/dist/eval/audit/knowledgeAuditCli.js +83 -0
- package/dist/eval/audit/knowledgeAuditCli.js.map +1 -0
- package/dist/eval/audit/knowledgeRefs.d.ts +37 -0
- package/dist/eval/audit/knowledgeRefs.d.ts.map +1 -0
- package/dist/eval/audit/knowledgeRefs.js +150 -0
- package/dist/eval/audit/knowledgeRefs.js.map +1 -0
- package/dist/eval/audit/symbolLookup.d.ts +23 -0
- package/dist/eval/audit/symbolLookup.d.ts.map +1 -0
- package/dist/eval/audit/symbolLookup.js +80 -0
- package/dist/eval/audit/symbolLookup.js.map +1 -0
- package/dist/eval/coverage/coverage.d.ts +65 -0
- package/dist/eval/coverage/coverage.d.ts.map +1 -0
- package/dist/eval/coverage/coverage.js +136 -0
- package/dist/eval/coverage/coverage.js.map +1 -0
- package/dist/eval/coverage/coverageCli.d.ts +13 -0
- package/dist/eval/coverage/coverageCli.d.ts.map +1 -0
- package/dist/eval/coverage/coverageCli.js +106 -0
- package/dist/eval/coverage/coverageCli.js.map +1 -0
- package/dist/eval/coverage/sources.d.ts +17 -0
- package/dist/eval/coverage/sources.d.ts.map +1 -0
- package/dist/eval/coverage/sources.js +40 -0
- package/dist/eval/coverage/sources.js.map +1 -0
- package/dist/eval/coverage/taxonomy.d.ts +59 -0
- package/dist/eval/coverage/taxonomy.d.ts.map +1 -0
- package/dist/eval/coverage/taxonomy.js +400 -0
- package/dist/eval/coverage/taxonomy.js.map +1 -0
- package/dist/eval/improver/caseMining.d.ts +55 -0
- package/dist/eval/improver/caseMining.d.ts.map +1 -0
- package/dist/eval/improver/caseMining.js +64 -0
- package/dist/eval/improver/caseMining.js.map +1 -0
- package/dist/eval/improver/caseMiningCli.d.ts +10 -0
- package/dist/eval/improver/caseMiningCli.d.ts.map +1 -0
- package/dist/eval/improver/caseMiningCli.js +50 -0
- package/dist/eval/improver/caseMiningCli.js.map +1 -0
- package/dist/eval/improver/cli.d.ts +11 -0
- package/dist/eval/improver/cli.d.ts.map +1 -0
- package/dist/eval/improver/cli.js +73 -0
- package/dist/eval/improver/cli.js.map +1 -0
- package/dist/eval/improver/cluster.d.ts +47 -0
- package/dist/eval/improver/cluster.d.ts.map +1 -0
- package/dist/eval/improver/cluster.js +0 -0
- package/dist/eval/improver/cluster.js.map +1 -0
- package/dist/eval/improver/corpusIO.d.ts +26 -0
- package/dist/eval/improver/corpusIO.d.ts.map +1 -0
- package/dist/eval/improver/corpusIO.js +50 -0
- package/dist/eval/improver/corpusIO.js.map +1 -0
- package/dist/eval/improver/fixBrief.d.ts +34 -0
- package/dist/eval/improver/fixBrief.d.ts.map +1 -0
- package/dist/eval/improver/fixBrief.js +69 -0
- package/dist/eval/improver/fixBrief.js.map +1 -0
- package/dist/eval/improver/fixBriefCli.d.ts +11 -0
- package/dist/eval/improver/fixBriefCli.d.ts.map +1 -0
- package/dist/eval/improver/fixBriefCli.js +46 -0
- package/dist/eval/improver/fixBriefCli.js.map +1 -0
- package/dist/eval/improver/flakeDetection.d.ts +50 -0
- package/dist/eval/improver/flakeDetection.d.ts.map +1 -0
- package/dist/eval/improver/flakeDetection.js +0 -0
- package/dist/eval/improver/flakeDetection.js.map +1 -0
- package/dist/eval/improver/flakeDetectionCli.d.ts +8 -0
- package/dist/eval/improver/flakeDetectionCli.d.ts.map +1 -0
- package/dist/eval/improver/flakeDetectionCli.js +27 -0
- package/dist/eval/improver/flakeDetectionCli.js.map +1 -0
- package/dist/eval/improver/heldout.d.ts +44 -0
- package/dist/eval/improver/heldout.d.ts.map +1 -0
- package/dist/eval/improver/heldout.js +52 -0
- package/dist/eval/improver/heldout.js.map +1 -0
- package/dist/eval/improver/knowledgeFeedback.d.ts +56 -0
- package/dist/eval/improver/knowledgeFeedback.d.ts.map +1 -0
- package/dist/eval/improver/knowledgeFeedback.js +88 -0
- package/dist/eval/improver/knowledgeFeedback.js.map +1 -0
- package/dist/eval/improver/knowledgeFeedbackCli.d.ts +8 -0
- package/dist/eval/improver/knowledgeFeedbackCli.d.ts.map +1 -0
- package/dist/eval/improver/knowledgeFeedbackCli.js +28 -0
- package/dist/eval/improver/knowledgeFeedbackCli.js.map +1 -0
- package/dist/eval/improver/report.d.ts +36 -0
- package/dist/eval/improver/report.d.ts.map +1 -0
- package/dist/eval/improver/report.js +61 -0
- package/dist/eval/improver/report.js.map +1 -0
- package/dist/eval/improver/reportCli.d.ts +7 -0
- package/dist/eval/improver/reportCli.d.ts.map +1 -0
- package/dist/eval/improver/reportCli.js +27 -0
- package/dist/eval/improver/reportCli.js.map +1 -0
- package/dist/eval/oracle/actualArtifactResolution.d.ts +49 -0
- package/dist/eval/oracle/actualArtifactResolution.d.ts.map +1 -0
- package/dist/eval/oracle/actualArtifactResolution.js +73 -0
- package/dist/eval/oracle/actualArtifactResolution.js.map +1 -0
- package/dist/eval/oracle/cli.d.ts +30 -0
- package/dist/eval/oracle/cli.d.ts.map +1 -0
- package/dist/eval/oracle/cli.js +179 -0
- package/dist/eval/oracle/cli.js.map +1 -0
- package/dist/eval/oracle/diff.d.ts +26 -0
- package/dist/eval/oracle/diff.d.ts.map +1 -0
- package/dist/eval/oracle/diff.js +51 -0
- package/dist/eval/oracle/diff.js.map +1 -0
- package/dist/eval/oracle/index.d.ts +81 -0
- package/dist/eval/oracle/index.d.ts.map +1 -0
- package/dist/eval/oracle/index.js +55 -0
- package/dist/eval/oracle/index.js.map +1 -0
- package/dist/eval/oracle/normalize.d.ts +85 -0
- package/dist/eval/oracle/normalize.d.ts.map +1 -0
- package/dist/eval/oracle/normalize.js +321 -0
- package/dist/eval/oracle/normalize.js.map +1 -0
- package/dist/eval/oracle/score.d.ts +26 -0
- package/dist/eval/oracle/score.d.ts.map +1 -0
- package/dist/eval/oracle/score.js +15 -0
- package/dist/eval/oracle/score.js.map +1 -0
- package/dist/eval/oracle/systest.d.ts +30 -0
- package/dist/eval/oracle/systest.d.ts.map +1 -0
- package/dist/eval/oracle/systest.js +98 -0
- package/dist/eval/oracle/systest.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +753 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/customFilters.d.ts +10 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/customFilters.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/customFilters.js +38 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/customFilters.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/fields.d.ts +21 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/fields.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/fields.js +168 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/fields.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/panels.d.ts +9 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/panels.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/panels.js +47 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/panels.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/toolbar.d.ts +16 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/toolbar.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/toolbar.js +124 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/toolbar.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/workspaceSections.d.ts +12 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/workspaceSections.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/workspaceSections.js +36 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/workspaceSections.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/common.d.ts +11 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/common.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/common.js +37 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/common.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsMaster.d.ts +8 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsMaster.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsMaster.js +86 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsMaster.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsTransaction.d.ts +7 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsTransaction.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsTransaction.js +69 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsTransaction.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/dialog.d.ts +14 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/dialog.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/dialog.js +187 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/dialog.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/factBox.d.ts +10 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/factBox.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/factBox.js +54 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/factBox.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/listPage.d.ts +7 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/listPage.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/listPage.js +33 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/listPage.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/lookup.d.ts +10 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/lookup.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/lookup.js +95 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/lookup.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleDetails.d.ts +16 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleDetails.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleDetails.js +158 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleDetails.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleList.d.ts +7 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleList.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleList.js +34 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleList.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleListDetails.d.ts +9 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleListDetails.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleListDetails.js +99 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleListDetails.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/tableOfContents.d.ts +8 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/tableOfContents.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/tableOfContents.js +95 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/tableOfContents.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/task.d.ts +9 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/task.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/task.js +42 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/task.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/wizard.d.ts +7 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/wizard.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/wizard.js +42 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/wizard.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/workspace.d.ts +17 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/workspace.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/workspace.js +157 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/workspace.js.map +1 -0
- package/dist/knowledge/formPatterns/crossCheck.d.ts +44 -0
- package/dist/knowledge/formPatterns/crossCheck.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/crossCheck.js +109 -0
- package/dist/knowledge/formPatterns/crossCheck.js.map +1 -0
- package/dist/knowledge/formPatterns/index.d.ts +30 -0
- package/dist/knowledge/formPatterns/index.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/index.js +158 -0
- package/dist/knowledge/formPatterns/index.js.map +1 -0
- package/dist/knowledge/formPatterns/methodStubs.d.ts +45 -0
- package/dist/knowledge/formPatterns/methodStubs.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/methodStubs.js +257 -0
- package/dist/knowledge/formPatterns/methodStubs.js.map +1 -0
- package/dist/knowledge/formPatterns/types.d.ts +115 -0
- package/dist/knowledge/formPatterns/types.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/types.js +44 -0
- package/dist/knowledge/formPatterns/types.js.map +1 -0
- package/dist/metadata/enhancedParser.d.ts +91 -0
- package/dist/metadata/enhancedParser.d.ts.map +1 -0
- package/dist/metadata/enhancedParser.js +379 -0
- package/dist/metadata/enhancedParser.js.map +1 -0
- package/dist/metadata/formPatternMiner.d.ts +78 -0
- package/dist/metadata/formPatternMiner.d.ts.map +1 -0
- package/dist/metadata/formPatternMiner.js +220 -0
- package/dist/metadata/formPatternMiner.js.map +1 -0
- package/dist/metadata/labelParser.d.ts +71 -0
- package/dist/metadata/labelParser.d.ts.map +1 -0
- package/dist/metadata/labelParser.js +258 -0
- package/dist/metadata/labelParser.js.map +1 -0
- package/dist/metadata/symbolCountsWorker.d.ts +16 -0
- package/dist/metadata/symbolCountsWorker.d.ts.map +1 -0
- package/dist/metadata/symbolCountsWorker.js +42 -0
- package/dist/metadata/symbolCountsWorker.js.map +1 -0
- package/dist/metadata/symbolIndex.d.ts +474 -0
- package/dist/metadata/symbolIndex.d.ts.map +1 -0
- package/dist/metadata/symbolIndex.js +3152 -0
- package/dist/metadata/symbolIndex.js.map +1 -0
- package/dist/metadata/types.d.ts +267 -0
- package/dist/metadata/types.d.ts.map +1 -0
- package/dist/metadata/types.js +5 -0
- package/dist/metadata/types.js.map +1 -0
- package/dist/metadata/xmlParser.d.ts +232 -0
- package/dist/metadata/xmlParser.d.ts.map +1 -0
- package/dist/metadata/xmlParser.js +1021 -0
- package/dist/metadata/xmlParser.js.map +1 -0
- package/dist/metadata/xppDeclaration.d.ts +116 -0
- package/dist/metadata/xppDeclaration.d.ts.map +1 -0
- package/dist/metadata/xppDeclaration.js +350 -0
- package/dist/metadata/xppDeclaration.js.map +1 -0
- package/dist/middleware/apiKeyAuth.d.ts +7 -0
- package/dist/middleware/apiKeyAuth.d.ts.map +1 -0
- package/dist/middleware/apiKeyAuth.js +70 -0
- package/dist/middleware/apiKeyAuth.js.map +1 -0
- package/dist/middleware/rateLimiter.d.ts +7 -0
- package/dist/middleware/rateLimiter.d.ts.map +1 -0
- package/dist/middleware/rateLimiter.js +78 -0
- package/dist/middleware/rateLimiter.js.map +1 -0
- package/dist/prompts/codeReview.d.ts +8 -0
- package/dist/prompts/codeReview.d.ts.map +1 -0
- package/dist/prompts/codeReview.js +619 -0
- package/dist/prompts/codeReview.js.map +1 -0
- package/dist/prompts/index.d.ts +6 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +6 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/systemInstructions.d.ts +33 -0
- package/dist/prompts/systemInstructions.d.ts.map +1 -0
- package/dist/prompts/systemInstructions.js +154 -0
- package/dist/prompts/systemInstructions.js.map +1 -0
- package/dist/resources/classResource.d.ts +17 -0
- package/dist/resources/classResource.d.ts.map +1 -0
- package/dist/resources/classResource.js +33 -0
- package/dist/resources/classResource.js.map +1 -0
- package/dist/resources/index.d.ts +20 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +154 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/server/mcpServer.d.ts +11 -0
- package/dist/server/mcpServer.d.ts.map +1 -0
- package/dist/server/mcpServer.js +185 -0
- package/dist/server/mcpServer.js.map +1 -0
- package/dist/server/serverMode.d.ts +53 -0
- package/dist/server/serverMode.d.ts.map +1 -0
- package/dist/server/serverMode.js +72 -0
- package/dist/server/serverMode.js.map +1 -0
- package/dist/server/toolAnnotations.d.ts +28 -0
- package/dist/server/toolAnnotations.d.ts.map +1 -0
- package/dist/server/toolAnnotations.js +70 -0
- package/dist/server/toolAnnotations.js.map +1 -0
- package/dist/server/toolSchemas/analyzeCode.d.ts +71 -0
- package/dist/server/toolSchemas/analyzeCode.d.ts.map +1 -0
- package/dist/server/toolSchemas/analyzeCode.js +50 -0
- package/dist/server/toolSchemas/analyzeCode.js.map +1 -0
- package/dist/server/toolSchemas/batchGetInfo.d.ts +32 -0
- package/dist/server/toolSchemas/batchGetInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/batchGetInfo.js +35 -0
- package/dist/server/toolSchemas/batchGetInfo.js.map +1 -0
- package/dist/server/toolSchemas/buildD365foProject.d.ts +44 -0
- package/dist/server/toolSchemas/buildD365foProject.d.ts.map +1 -0
- package/dist/server/toolSchemas/buildD365foProject.js +47 -0
- package/dist/server/toolSchemas/buildD365foProject.js.map +1 -0
- package/dist/server/toolSchemas/d365foFile.d.ts +99 -0
- package/dist/server/toolSchemas/d365foFile.d.ts.map +1 -0
- package/dist/server/toolSchemas/d365foFile.js +163 -0
- package/dist/server/toolSchemas/d365foFile.js.map +1 -0
- package/dist/server/toolSchemas/extensionInfo.d.ts +65 -0
- package/dist/server/toolSchemas/extensionInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/extensionInfo.js +73 -0
- package/dist/server/toolSchemas/extensionInfo.js.map +1 -0
- package/dist/server/toolSchemas/findReferences.d.ts +35 -0
- package/dist/server/toolSchemas/findReferences.d.ts.map +1 -0
- package/dist/server/toolSchemas/findReferences.js +35 -0
- package/dist/server/toolSchemas/findReferences.js.map +1 -0
- package/dist/server/toolSchemas/generateObject.d.ts +246 -0
- package/dist/server/toolSchemas/generateObject.d.ts.map +1 -0
- package/dist/server/toolSchemas/generateObject.js +158 -0
- package/dist/server/toolSchemas/generateObject.js.map +1 -0
- package/dist/server/toolSchemas/getKnowledge.d.ts +39 -0
- package/dist/server/toolSchemas/getKnowledge.d.ts.map +1 -0
- package/dist/server/toolSchemas/getKnowledge.js +46 -0
- package/dist/server/toolSchemas/getKnowledge.js.map +1 -0
- package/dist/server/toolSchemas/getMethod.d.ts +30 -0
- package/dist/server/toolSchemas/getMethod.d.ts.map +1 -0
- package/dist/server/toolSchemas/getMethod.js +34 -0
- package/dist/server/toolSchemas/getMethod.js.map +1 -0
- package/dist/server/toolSchemas/getObjectInfo.d.ts +24 -0
- package/dist/server/toolSchemas/getObjectInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/getObjectInfo.js +30 -0
- package/dist/server/toolSchemas/getObjectInfo.js.map +1 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.d.ts +29 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.js +29 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.js.map +1 -0
- package/dist/server/toolSchemas/index.d.ts +1142 -0
- package/dist/server/toolSchemas/index.d.ts.map +1 -0
- package/dist/server/toolSchemas/index.js +60 -0
- package/dist/server/toolSchemas/index.js.map +1 -0
- package/dist/server/toolSchemas/labels.d.ts +172 -0
- package/dist/server/toolSchemas/labels.d.ts.map +1 -0
- package/dist/server/toolSchemas/labels.js +159 -0
- package/dist/server/toolSchemas/labels.js.map +1 -0
- package/dist/server/toolSchemas/objectPatterns.d.ts +93 -0
- package/dist/server/toolSchemas/objectPatterns.d.ts.map +1 -0
- package/dist/server/toolSchemas/objectPatterns.js +104 -0
- package/dist/server/toolSchemas/objectPatterns.js.map +1 -0
- package/dist/server/toolSchemas/prepare.d.ts +49 -0
- package/dist/server/toolSchemas/prepare.d.ts.map +1 -0
- package/dist/server/toolSchemas/prepare.js +57 -0
- package/dist/server/toolSchemas/prepare.js.map +1 -0
- package/dist/server/toolSchemas/reviewWorkspaceChanges.d.ts +20 -0
- package/dist/server/toolSchemas/reviewWorkspaceChanges.d.ts.map +1 -0
- package/dist/server/toolSchemas/reviewWorkspaceChanges.js +19 -0
- package/dist/server/toolSchemas/reviewWorkspaceChanges.js.map +1 -0
- package/dist/server/toolSchemas/runBpCheck.d.ts +36 -0
- package/dist/server/toolSchemas/runBpCheck.d.ts.map +1 -0
- package/dist/server/toolSchemas/runBpCheck.js +21 -0
- package/dist/server/toolSchemas/runBpCheck.js.map +1 -0
- package/dist/server/toolSchemas/runSystestClass.d.ts +32 -0
- package/dist/server/toolSchemas/runSystestClass.d.ts.map +1 -0
- package/dist/server/toolSchemas/runSystestClass.js +20 -0
- package/dist/server/toolSchemas/runSystestClass.js.map +1 -0
- package/dist/server/toolSchemas/search.d.ts +109 -0
- package/dist/server/toolSchemas/search.d.ts.map +1 -0
- package/dist/server/toolSchemas/search.js +123 -0
- package/dist/server/toolSchemas/search.js.map +1 -0
- package/dist/server/toolSchemas/securityInfo.d.ts +45 -0
- package/dist/server/toolSchemas/securityInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/securityInfo.js +39 -0
- package/dist/server/toolSchemas/securityInfo.js.map +1 -0
- package/dist/server/toolSchemas/suggestEdt.d.ts +29 -0
- package/dist/server/toolSchemas/suggestEdt.d.ts.map +1 -0
- package/dist/server/toolSchemas/suggestEdt.js +29 -0
- package/dist/server/toolSchemas/suggestEdt.js.map +1 -0
- package/dist/server/toolSchemas/triggerDbSync.d.ts +47 -0
- package/dist/server/toolSchemas/triggerDbSync.d.ts.map +1 -0
- package/dist/server/toolSchemas/triggerDbSync.js +31 -0
- package/dist/server/toolSchemas/triggerDbSync.js.map +1 -0
- package/dist/server/toolSchemas/undoLastModification.d.ts +20 -0
- package/dist/server/toolSchemas/undoLastModification.d.ts.map +1 -0
- package/dist/server/toolSchemas/undoLastModification.js +17 -0
- package/dist/server/toolSchemas/undoLastModification.js.map +1 -0
- package/dist/server/toolSchemas/updateSymbolIndex.d.ts +19 -0
- package/dist/server/toolSchemas/updateSymbolIndex.d.ts.map +1 -0
- package/dist/server/toolSchemas/updateSymbolIndex.js +18 -0
- package/dist/server/toolSchemas/updateSymbolIndex.js.map +1 -0
- package/dist/server/toolSchemas/validateCode.d.ts +35 -0
- package/dist/server/toolSchemas/validateCode.d.ts.map +1 -0
- package/dist/server/toolSchemas/validateCode.js +38 -0
- package/dist/server/toolSchemas/validateCode.js.map +1 -0
- package/dist/server/toolSchemas/validateObjectNaming.d.ts +33 -0
- package/dist/server/toolSchemas/validateObjectNaming.d.ts.map +1 -0
- package/dist/server/toolSchemas/validateObjectNaming.js +32 -0
- package/dist/server/toolSchemas/validateObjectNaming.js.map +1 -0
- package/dist/server/toolSchemas/verifyD365foProject.d.ts +50 -0
- package/dist/server/toolSchemas/verifyD365foProject.d.ts.map +1 -0
- package/dist/server/toolSchemas/verifyD365foProject.js +46 -0
- package/dist/server/toolSchemas/verifyD365foProject.js.map +1 -0
- package/dist/server/transport.d.ts +30 -0
- package/dist/server/transport.d.ts.map +1 -0
- package/dist/server/transport.js +391 -0
- package/dist/server/transport.js.map +1 -0
- package/dist/tools/analyzeCode.d.ts +17 -0
- package/dist/tools/analyzeCode.d.ts.map +1 -0
- package/dist/tools/analyzeCode.js +63 -0
- package/dist/tools/analyzeCode.js.map +1 -0
- package/dist/tools/analyzeCompleteness.d.ts +20 -0
- package/dist/tools/analyzeCompleteness.d.ts.map +1 -0
- package/dist/tools/analyzeCompleteness.js +103 -0
- package/dist/tools/analyzeCompleteness.js.map +1 -0
- package/dist/tools/analyzeExtensionPoints.d.ts +21 -0
- package/dist/tools/analyzeExtensionPoints.d.ts.map +1 -0
- package/dist/tools/analyzeExtensionPoints.js +343 -0
- package/dist/tools/analyzeExtensionPoints.js.map +1 -0
- package/dist/tools/analyzePatterns.d.ts +20 -0
- package/dist/tools/analyzePatterns.d.ts.map +1 -0
- package/dist/tools/analyzePatterns.js +71 -0
- package/dist/tools/analyzePatterns.js.map +1 -0
- package/dist/tools/apiUsagePatterns.d.ts +20 -0
- package/dist/tools/apiUsagePatterns.d.ts.map +1 -0
- package/dist/tools/apiUsagePatterns.js +136 -0
- package/dist/tools/apiUsagePatterns.js.map +1 -0
- package/dist/tools/batchGetInfo.d.ts +50 -0
- package/dist/tools/batchGetInfo.d.ts.map +1 -0
- package/dist/tools/batchGetInfo.js +52 -0
- package/dist/tools/batchGetInfo.js.map +1 -0
- package/dist/tools/batchSearch.d.ts +64 -0
- package/dist/tools/batchSearch.d.ts.map +1 -0
- package/dist/tools/batchSearch.js +265 -0
- package/dist/tools/batchSearch.js.map +1 -0
- package/dist/tools/buildProject.d.ts +28 -0
- package/dist/tools/buildProject.d.ts.map +1 -0
- package/dist/tools/buildProject.js +929 -0
- package/dist/tools/buildProject.js.map +1 -0
- package/dist/tools/classInfo.d.ts +8 -0
- package/dist/tools/classInfo.d.ts.map +1 -0
- package/dist/tools/classInfo.js +235 -0
- package/dist/tools/classInfo.js.map +1 -0
- package/dist/tools/codeGen.d.ts +19 -0
- package/dist/tools/codeGen.d.ts.map +1 -0
- package/dist/tools/codeGen.js +1956 -0
- package/dist/tools/codeGen.js.map +1 -0
- package/dist/tools/completion.d.ts +8 -0
- package/dist/tools/completion.d.ts.map +1 -0
- package/dist/tools/completion.js +272 -0
- package/dist/tools/completion.js.map +1 -0
- package/dist/tools/configKeyInfo.d.ts +22 -0
- package/dist/tools/configKeyInfo.d.ts.map +1 -0
- package/dist/tools/configKeyInfo.js +74 -0
- package/dist/tools/configKeyInfo.js.map +1 -0
- package/dist/tools/createD365File.d.ts +479 -0
- package/dist/tools/createD365File.d.ts.map +1 -0
- package/dist/tools/createD365File.js +4185 -0
- package/dist/tools/createD365File.js.map +1 -0
- package/dist/tools/createLabel.d.ts +51 -0
- package/dist/tools/createLabel.d.ts.map +1 -0
- package/dist/tools/createLabel.js +761 -0
- package/dist/tools/createLabel.js.map +1 -0
- package/dist/tools/d365foErrorHelp.d.ts +29 -0
- package/dist/tools/d365foErrorHelp.d.ts.map +1 -0
- package/dist/tools/d365foErrorHelp.js +389 -0
- package/dist/tools/d365foErrorHelp.js.map +1 -0
- package/dist/tools/d365foFile.d.ts +20 -0
- package/dist/tools/d365foFile.d.ts.map +1 -0
- package/dist/tools/d365foFile.js +58 -0
- package/dist/tools/d365foFile.js.map +1 -0
- package/dist/tools/d365foFileOpSpecs.d.ts +41 -0
- package/dist/tools/d365foFileOpSpecs.d.ts.map +1 -0
- package/dist/tools/d365foFileOpSpecs.js +336 -0
- package/dist/tools/d365foFileOpSpecs.js.map +1 -0
- package/dist/tools/dataEntityInfo.d.ts +23 -0
- package/dist/tools/dataEntityInfo.d.ts.map +1 -0
- package/dist/tools/dataEntityInfo.js +45 -0
- package/dist/tools/dataEntityInfo.js.map +1 -0
- package/dist/tools/dataEntityXml.d.ts +31 -0
- package/dist/tools/dataEntityXml.d.ts.map +1 -0
- package/dist/tools/dataEntityXml.js +131 -0
- package/dist/tools/dataEntityXml.js.map +1 -0
- package/dist/tools/dbSync.d.ts +8 -0
- package/dist/tools/dbSync.d.ts.map +1 -0
- package/dist/tools/dbSync.js +285 -0
- package/dist/tools/dbSync.js.map +1 -0
- package/dist/tools/edtInfo.d.ts +23 -0
- package/dist/tools/edtInfo.d.ts.map +1 -0
- package/dist/tools/edtInfo.js +122 -0
- package/dist/tools/edtInfo.js.map +1 -0
- package/dist/tools/enumInfo.d.ts +17 -0
- package/dist/tools/enumInfo.d.ts.map +1 -0
- package/dist/tools/enumInfo.js +44 -0
- package/dist/tools/enumInfo.js.map +1 -0
- package/dist/tools/extensionInfo.d.ts +26 -0
- package/dist/tools/extensionInfo.d.ts.map +1 -0
- package/dist/tools/extensionInfo.js +73 -0
- package/dist/tools/extensionInfo.js.map +1 -0
- package/dist/tools/extensionSearch.d.ts +20 -0
- package/dist/tools/extensionSearch.d.ts.map +1 -0
- package/dist/tools/extensionSearch.js +72 -0
- package/dist/tools/extensionSearch.js.map +1 -0
- package/dist/tools/extensionStrategyAdvisor.d.ts +15 -0
- package/dist/tools/extensionStrategyAdvisor.d.ts.map +1 -0
- package/dist/tools/extensionStrategyAdvisor.js +519 -0
- package/dist/tools/extensionStrategyAdvisor.js.map +1 -0
- package/dist/tools/findCocExtensions.d.ts +20 -0
- package/dist/tools/findCocExtensions.d.ts.map +1 -0
- package/dist/tools/findCocExtensions.js +285 -0
- package/dist/tools/findCocExtensions.js.map +1 -0
- package/dist/tools/findEventHandlers.d.ts +21 -0
- package/dist/tools/findEventHandlers.d.ts.map +1 -0
- package/dist/tools/findEventHandlers.js +196 -0
- package/dist/tools/findEventHandlers.js.map +1 -0
- package/dist/tools/findReferences.d.ts +30 -0
- package/dist/tools/findReferences.d.ts.map +1 -0
- package/dist/tools/findReferences.js +551 -0
- package/dist/tools/findReferences.js.map +1 -0
- package/dist/tools/formInfo.d.ts +13 -0
- package/dist/tools/formInfo.d.ts.map +1 -0
- package/dist/tools/formInfo.js +407 -0
- package/dist/tools/formInfo.js.map +1 -0
- package/dist/tools/formPattern.d.ts +19 -0
- package/dist/tools/formPattern.d.ts.map +1 -0
- package/dist/tools/formPattern.js +52 -0
- package/dist/tools/formPattern.js.map +1 -0
- package/dist/tools/generateD365Xml.d.ts +12 -0
- package/dist/tools/generateD365Xml.d.ts.map +1 -0
- package/dist/tools/generateD365Xml.js +1454 -0
- package/dist/tools/generateD365Xml.js.map +1 -0
- package/dist/tools/generateFindMethods.d.ts +69 -0
- package/dist/tools/generateFindMethods.d.ts.map +1 -0
- package/dist/tools/generateFindMethods.js +207 -0
- package/dist/tools/generateFindMethods.js.map +1 -0
- package/dist/tools/generateMetadata.d.ts +37 -0
- package/dist/tools/generateMetadata.d.ts.map +1 -0
- package/dist/tools/generateMetadata.js +213 -0
- package/dist/tools/generateMetadata.js.map +1 -0
- package/dist/tools/generateObject.d.ts +34 -0
- package/dist/tools/generateObject.d.ts.map +1 -0
- package/dist/tools/generateObject.js +47 -0
- package/dist/tools/generateObject.js.map +1 -0
- package/dist/tools/generateRelationXpp.d.ts +45 -0
- package/dist/tools/generateRelationXpp.d.ts.map +1 -0
- package/dist/tools/generateRelationXpp.js +129 -0
- package/dist/tools/generateRelationXpp.js.map +1 -0
- package/dist/tools/generateSmart.d.ts +32 -0
- package/dist/tools/generateSmart.d.ts.map +1 -0
- package/dist/tools/generateSmart.js +52 -0
- package/dist/tools/generateSmart.js.map +1 -0
- package/dist/tools/generateSmartForm.d.ts +82 -0
- package/dist/tools/generateSmartForm.d.ts.map +1 -0
- package/dist/tools/generateSmartForm.js +996 -0
- package/dist/tools/generateSmartForm.js.map +1 -0
- package/dist/tools/generateSmartReport.d.ts +90 -0
- package/dist/tools/generateSmartReport.d.ts.map +1 -0
- package/dist/tools/generateSmartReport.js +1254 -0
- package/dist/tools/generateSmartReport.js.map +1 -0
- package/dist/tools/generateSmartTable.d.ts +138 -0
- package/dist/tools/generateSmartTable.d.ts.map +1 -0
- package/dist/tools/generateSmartTable.js +1305 -0
- package/dist/tools/generateSmartTable.js.map +1 -0
- package/dist/tools/generateTableFields.d.ts +48 -0
- package/dist/tools/generateTableFields.d.ts.map +1 -0
- package/dist/tools/generateTableFields.js +186 -0
- package/dist/tools/generateTableFields.js.map +1 -0
- package/dist/tools/generateTableRelation.d.ts +35 -0
- package/dist/tools/generateTableRelation.d.ts.map +1 -0
- package/dist/tools/generateTableRelation.js +161 -0
- package/dist/tools/generateTableRelation.js.map +1 -0
- package/dist/tools/getFormPatternSpec.d.ts +13 -0
- package/dist/tools/getFormPatternSpec.d.ts.map +1 -0
- package/dist/tools/getFormPatternSpec.js +290 -0
- package/dist/tools/getFormPatternSpec.js.map +1 -0
- package/dist/tools/getFormPatterns.d.ts +68 -0
- package/dist/tools/getFormPatterns.d.ts.map +1 -0
- package/dist/tools/getFormPatterns.js +471 -0
- package/dist/tools/getFormPatterns.js.map +1 -0
- package/dist/tools/getKnowledge.d.ts +27 -0
- package/dist/tools/getKnowledge.d.ts.map +1 -0
- package/dist/tools/getKnowledge.js +50 -0
- package/dist/tools/getKnowledge.js.map +1 -0
- package/dist/tools/getLabelInfo.d.ts +21 -0
- package/dist/tools/getLabelInfo.d.ts.map +1 -0
- package/dist/tools/getLabelInfo.js +138 -0
- package/dist/tools/getLabelInfo.js.map +1 -0
- package/dist/tools/getMethod.d.ts +19 -0
- package/dist/tools/getMethod.d.ts.map +1 -0
- package/dist/tools/getMethod.js +77 -0
- package/dist/tools/getMethod.js.map +1 -0
- package/dist/tools/getMethodSource.d.ts +17 -0
- package/dist/tools/getMethodSource.d.ts.map +1 -0
- package/dist/tools/getMethodSource.js +161 -0
- package/dist/tools/getMethodSource.js.map +1 -0
- package/dist/tools/getObjectInfo.d.ts +17 -0
- package/dist/tools/getObjectInfo.d.ts.map +1 -0
- package/dist/tools/getObjectInfo.js +88 -0
- package/dist/tools/getObjectInfo.js.map +1 -0
- package/dist/tools/getTablePatterns.d.ts +45 -0
- package/dist/tools/getTablePatterns.d.ts.map +1 -0
- package/dist/tools/getTablePatterns.js +235 -0
- package/dist/tools/getTablePatterns.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +11 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/labels.d.ts +25 -0
- package/dist/tools/labels.d.ts.map +1 -0
- package/dist/tools/labels.js +111 -0
- package/dist/tools/labels.js.map +1 -0
- package/dist/tools/macroInfo.d.ts +23 -0
- package/dist/tools/macroInfo.d.ts.map +1 -0
- package/dist/tools/macroInfo.js +62 -0
- package/dist/tools/macroInfo.js.map +1 -0
- package/dist/tools/mapInfo.d.ts +21 -0
- package/dist/tools/mapInfo.d.ts.map +1 -0
- package/dist/tools/mapInfo.js +54 -0
- package/dist/tools/mapInfo.js.map +1 -0
- package/dist/tools/mapXml.d.ts +26 -0
- package/dist/tools/mapXml.d.ts.map +1 -0
- package/dist/tools/mapXml.js +88 -0
- package/dist/tools/mapXml.js.map +1 -0
- package/dist/tools/menuItemInfo.d.ts +20 -0
- package/dist/tools/menuItemInfo.d.ts.map +1 -0
- package/dist/tools/menuItemInfo.js +185 -0
- package/dist/tools/menuItemInfo.js.map +1 -0
- package/dist/tools/methodSignature.d.ts +33 -0
- package/dist/tools/methodSignature.d.ts.map +1 -0
- package/dist/tools/methodSignature.js +348 -0
- package/dist/tools/methodSignature.js.map +1 -0
- package/dist/tools/modifyD365File.d.ts +130 -0
- package/dist/tools/modifyD365File.d.ts.map +1 -0
- package/dist/tools/modifyD365File.js +2179 -0
- package/dist/tools/modifyD365File.js.map +1 -0
- package/dist/tools/objectInfoRegistry.d.ts +36 -0
- package/dist/tools/objectInfoRegistry.d.ts.map +1 -0
- package/dist/tools/objectInfoRegistry.js +98 -0
- package/dist/tools/objectInfoRegistry.js.map +1 -0
- package/dist/tools/objectPatterns.d.ts +16 -0
- package/dist/tools/objectPatterns.d.ts.map +1 -0
- package/dist/tools/objectPatterns.js +84 -0
- package/dist/tools/objectPatterns.js.map +1 -0
- package/dist/tools/prepare.d.ts +18 -0
- package/dist/tools/prepare.d.ts.map +1 -0
- package/dist/tools/prepare.js +47 -0
- package/dist/tools/prepare.js.map +1 -0
- package/dist/tools/prepareChange.d.ts +43 -0
- package/dist/tools/prepareChange.d.ts.map +1 -0
- package/dist/tools/prepareChange.js +327 -0
- package/dist/tools/prepareChange.js.map +1 -0
- package/dist/tools/prepareCreate.d.ts +45 -0
- package/dist/tools/prepareCreate.d.ts.map +1 -0
- package/dist/tools/prepareCreate.js +263 -0
- package/dist/tools/prepareCreate.js.map +1 -0
- package/dist/tools/queryInfo.d.ts +17 -0
- package/dist/tools/queryInfo.d.ts.map +1 -0
- package/dist/tools/queryInfo.js +46 -0
- package/dist/tools/queryInfo.js.map +1 -0
- package/dist/tools/queryViewXml.d.ts +36 -0
- package/dist/tools/queryViewXml.d.ts.map +1 -0
- package/dist/tools/queryViewXml.js +129 -0
- package/dist/tools/queryViewXml.js.map +1 -0
- package/dist/tools/renameLabel.d.ts +29 -0
- package/dist/tools/renameLabel.d.ts.map +1 -0
- package/dist/tools/renameLabel.js +377 -0
- package/dist/tools/renameLabel.js.map +1 -0
- package/dist/tools/repairFormControls.d.ts +21 -0
- package/dist/tools/repairFormControls.d.ts.map +1 -0
- package/dist/tools/repairFormControls.js +151 -0
- package/dist/tools/repairFormControls.js.map +1 -0
- package/dist/tools/reportInfo.d.ts +14 -0
- package/dist/tools/reportInfo.d.ts.map +1 -0
- package/dist/tools/reportInfo.js +302 -0
- package/dist/tools/reportInfo.js.map +1 -0
- package/dist/tools/resolveReferences.d.ts +89 -0
- package/dist/tools/resolveReferences.d.ts.map +1 -0
- package/dist/tools/resolveReferences.js +791 -0
- package/dist/tools/resolveReferences.js.map +1 -0
- package/dist/tools/reviewWorkspaceChanges.d.ts +14 -0
- package/dist/tools/reviewWorkspaceChanges.d.ts.map +1 -0
- package/dist/tools/reviewWorkspaceChanges.js +73 -0
- package/dist/tools/reviewWorkspaceChanges.js.map +1 -0
- package/dist/tools/runBpCheck.d.ts +14 -0
- package/dist/tools/runBpCheck.d.ts.map +1 -0
- package/dist/tools/runBpCheck.js +241 -0
- package/dist/tools/runBpCheck.js.map +1 -0
- package/dist/tools/search.d.ts +20 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +246 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/searchLabels.d.ts +26 -0
- package/dist/tools/searchLabels.d.ts.map +1 -0
- package/dist/tools/searchLabels.js +91 -0
- package/dist/tools/searchLabels.js.map +1 -0
- package/dist/tools/searchUnified.d.ts +28 -0
- package/dist/tools/searchUnified.d.ts.map +1 -0
- package/dist/tools/searchUnified.js +48 -0
- package/dist/tools/searchUnified.js.map +1 -0
- package/dist/tools/securityArtifactInfo.d.ts +14 -0
- package/dist/tools/securityArtifactInfo.d.ts.map +1 -0
- package/dist/tools/securityArtifactInfo.js +192 -0
- package/dist/tools/securityArtifactInfo.js.map +1 -0
- package/dist/tools/securityCoverageInfo.d.ts +21 -0
- package/dist/tools/securityCoverageInfo.d.ts.map +1 -0
- package/dist/tools/securityCoverageInfo.js +248 -0
- package/dist/tools/securityCoverageInfo.js.map +1 -0
- package/dist/tools/securityInfo.d.ts +22 -0
- package/dist/tools/securityInfo.d.ts.map +1 -0
- package/dist/tools/securityInfo.js +43 -0
- package/dist/tools/securityInfo.js.map +1 -0
- package/dist/tools/securityPolicyInfo.d.ts +22 -0
- package/dist/tools/securityPolicyInfo.d.ts.map +1 -0
- package/dist/tools/securityPolicyInfo.js +49 -0
- package/dist/tools/securityPolicyInfo.js.map +1 -0
- package/dist/tools/securityPrivilegeXml.d.ts +21 -0
- package/dist/tools/securityPrivilegeXml.d.ts.map +1 -0
- package/dist/tools/securityPrivilegeXml.js +61 -0
- package/dist/tools/securityPrivilegeXml.js.map +1 -0
- package/dist/tools/serviceInfo.d.ts +25 -0
- package/dist/tools/serviceInfo.d.ts.map +1 -0
- package/dist/tools/serviceInfo.js +93 -0
- package/dist/tools/serviceInfo.js.map +1 -0
- package/dist/tools/suggestEdt.d.ts +15 -0
- package/dist/tools/suggestEdt.d.ts.map +1 -0
- package/dist/tools/suggestEdt.js +348 -0
- package/dist/tools/suggestEdt.js.map +1 -0
- package/dist/tools/suggestImplementation.d.ts +20 -0
- package/dist/tools/suggestImplementation.d.ts.map +1 -0
- package/dist/tools/suggestImplementation.js +144 -0
- package/dist/tools/suggestImplementation.js.map +1 -0
- package/dist/tools/sysTestRunner.d.ts +14 -0
- package/dist/tools/sysTestRunner.d.ts.map +1 -0
- package/dist/tools/sysTestRunner.js +145 -0
- package/dist/tools/sysTestRunner.js.map +1 -0
- package/dist/tools/tableExtensionInfo.d.ts +91 -0
- package/dist/tools/tableExtensionInfo.d.ts.map +1 -0
- package/dist/tools/tableExtensionInfo.js +305 -0
- package/dist/tools/tableExtensionInfo.js.map +1 -0
- package/dist/tools/tableInfo.d.ts +23 -0
- package/dist/tools/tableInfo.d.ts.map +1 -0
- package/dist/tools/tableInfo.js +125 -0
- package/dist/tools/tableInfo.js.map +1 -0
- package/dist/tools/toolHandler.d.ts +4 -0
- package/dist/tools/toolHandler.d.ts.map +1 -0
- package/dist/tools/toolHandler.js +508 -0
- package/dist/tools/toolHandler.js.map +1 -0
- package/dist/tools/undoLastModification.d.ts +15 -0
- package/dist/tools/undoLastModification.d.ts.map +1 -0
- package/dist/tools/undoLastModification.js +147 -0
- package/dist/tools/undoLastModification.js.map +1 -0
- package/dist/tools/updateSymbolIndex.d.ts +15 -0
- package/dist/tools/updateSymbolIndex.d.ts.map +1 -0
- package/dist/tools/updateSymbolIndex.js +520 -0
- package/dist/tools/updateSymbolIndex.js.map +1 -0
- package/dist/tools/validateCode.d.ts +24 -0
- package/dist/tools/validateCode.d.ts.map +1 -0
- package/dist/tools/validateCode.js +201 -0
- package/dist/tools/validateCode.js.map +1 -0
- package/dist/tools/validateFormPattern.d.ts +51 -0
- package/dist/tools/validateFormPattern.d.ts.map +1 -0
- package/dist/tools/validateFormPattern.js +201 -0
- package/dist/tools/validateFormPattern.js.map +1 -0
- package/dist/tools/validateObjectNaming.d.ts +21 -0
- package/dist/tools/validateObjectNaming.d.ts.map +1 -0
- package/dist/tools/validateObjectNaming.js +319 -0
- package/dist/tools/validateObjectNaming.js.map +1 -0
- package/dist/tools/validateXpp.d.ts +79 -0
- package/dist/tools/validateXpp.d.ts.map +1 -0
- package/dist/tools/validateXpp.js +705 -0
- package/dist/tools/validateXpp.js.map +1 -0
- package/dist/tools/verifyD365Project.d.ts +21 -0
- package/dist/tools/verifyD365Project.d.ts.map +1 -0
- package/dist/tools/verifyD365Project.js +299 -0
- package/dist/tools/verifyD365Project.js.map +1 -0
- package/dist/tools/viewInfo.d.ts +17 -0
- package/dist/tools/viewInfo.d.ts.map +1 -0
- package/dist/tools/viewInfo.js +46 -0
- package/dist/tools/viewInfo.js.map +1 -0
- package/dist/tools/xppKnowledge.d.ts +45 -0
- package/dist/tools/xppKnowledge.d.ts.map +1 -0
- package/dist/tools/xppKnowledge.js +2815 -0
- package/dist/tools/xppKnowledge.js.map +1 -0
- package/dist/types/context.d.ts +56 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/context.js +5 -0
- package/dist/types/context.js.map +1 -0
- package/dist/utils/callDedup.d.ts +29 -0
- package/dist/utils/callDedup.d.ts.map +1 -0
- package/dist/utils/callDedup.js +87 -0
- package/dist/utils/callDedup.js.map +1 -0
- package/dist/utils/configManager.d.ts +307 -0
- package/dist/utils/configManager.d.ts.map +1 -0
- package/dist/utils/configManager.js +1169 -0
- package/dist/utils/configManager.js.map +1 -0
- package/dist/utils/d365XmlNormalizer.d.ts +9 -0
- package/dist/utils/d365XmlNormalizer.d.ts.map +1 -0
- package/dist/utils/d365XmlNormalizer.js +24 -0
- package/dist/utils/d365XmlNormalizer.js.map +1 -0
- package/dist/utils/edtExtensionValidator.d.ts +110 -0
- package/dist/utils/edtExtensionValidator.d.ts.map +1 -0
- package/dist/utils/edtExtensionValidator.js +367 -0
- package/dist/utils/edtExtensionValidator.js.map +1 -0
- package/dist/utils/eolUtils.d.ts +15 -0
- package/dist/utils/eolUtils.d.ts.map +1 -0
- package/dist/utils/eolUtils.js +21 -0
- package/dist/utils/eolUtils.js.map +1 -0
- package/dist/utils/extractManifest.d.ts +34 -0
- package/dist/utils/extractManifest.d.ts.map +1 -0
- package/dist/utils/extractManifest.js +43 -0
- package/dist/utils/extractManifest.js.map +1 -0
- package/dist/utils/fieldControlTypes.d.ts +44 -0
- package/dist/utils/fieldControlTypes.d.ts.map +1 -0
- package/dist/utils/fieldControlTypes.js +97 -0
- package/dist/utils/fieldControlTypes.js.map +1 -0
- package/dist/utils/formCloner.d.ts +77 -0
- package/dist/utils/formCloner.d.ts.map +1 -0
- package/dist/utils/formCloner.js +364 -0
- package/dist/utils/formCloner.js.map +1 -0
- package/dist/utils/formControlExpander.d.ts +68 -0
- package/dist/utils/formControlExpander.d.ts.map +1 -0
- package/dist/utils/formControlExpander.js +207 -0
- package/dist/utils/formControlExpander.js.map +1 -0
- package/dist/utils/formControlRepair.d.ts +81 -0
- package/dist/utils/formControlRepair.d.ts.map +1 -0
- package/dist/utils/formControlRepair.js +203 -0
- package/dist/utils/formControlRepair.js.map +1 -0
- package/dist/utils/formExtensionShapeValidator.d.ts +44 -0
- package/dist/utils/formExtensionShapeValidator.d.ts.map +1 -0
- package/dist/utils/formExtensionShapeValidator.js +99 -0
- package/dist/utils/formExtensionShapeValidator.js.map +1 -0
- package/dist/utils/formPatternTemplates.d.ts +77 -0
- package/dist/utils/formPatternTemplates.d.ts.map +1 -0
- package/dist/utils/formPatternTemplates.js +1917 -0
- package/dist/utils/formPatternTemplates.js.map +1 -0
- package/dist/utils/fsExtensionScanner.d.ts +76 -0
- package/dist/utils/fsExtensionScanner.d.ts.map +1 -0
- package/dist/utils/fsExtensionScanner.js +271 -0
- package/dist/utils/fsExtensionScanner.js.map +1 -0
- package/dist/utils/fuzzyMatching.d.ts +41 -0
- package/dist/utils/fuzzyMatching.d.ts.map +1 -0
- package/dist/utils/fuzzyMatching.js +186 -0
- package/dist/utils/fuzzyMatching.js.map +1 -0
- package/dist/utils/indexStaleness.d.ts +30 -0
- package/dist/utils/indexStaleness.d.ts.map +1 -0
- package/dist/utils/indexStaleness.js +104 -0
- package/dist/utils/indexStaleness.js.map +1 -0
- package/dist/utils/loadEnv.d.ts +32 -0
- package/dist/utils/loadEnv.d.ts.map +1 -0
- package/dist/utils/loadEnv.js +83 -0
- package/dist/utils/loadEnv.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/metadataResolver.d.ts +155 -0
- package/dist/utils/metadataResolver.d.ts.map +1 -0
- package/dist/utils/metadataResolver.js +277 -0
- package/dist/utils/metadataResolver.js.map +1 -0
- package/dist/utils/modelClassifier.d.ts +180 -0
- package/dist/utils/modelClassifier.d.ts.map +1 -0
- package/dist/utils/modelClassifier.js +373 -0
- package/dist/utils/modelClassifier.js.map +1 -0
- package/dist/utils/operationLocks.d.ts +26 -0
- package/dist/utils/operationLocks.d.ts.map +1 -0
- package/dist/utils/operationLocks.js +182 -0
- package/dist/utils/operationLocks.js.map +1 -0
- package/dist/utils/packageResolver.d.ts +52 -0
- package/dist/utils/packageResolver.d.ts.map +1 -0
- package/dist/utils/packageResolver.js +174 -0
- package/dist/utils/packageResolver.js.map +1 -0
- package/dist/utils/pathContainment.d.ts +60 -0
- package/dist/utils/pathContainment.d.ts.map +1 -0
- package/dist/utils/pathContainment.js +270 -0
- package/dist/utils/pathContainment.js.map +1 -0
- package/dist/utils/projectUtils.d.ts +14 -0
- package/dist/utils/projectUtils.d.ts.map +1 -0
- package/dist/utils/projectUtils.js +48 -0
- package/dist/utils/projectUtils.js.map +1 -0
- package/dist/utils/provenanceStore.d.ts +54 -0
- package/dist/utils/provenanceStore.d.ts.map +1 -0
- package/dist/utils/provenanceStore.js +203 -0
- package/dist/utils/provenanceStore.js.map +1 -0
- package/dist/utils/richContext.d.ts +45 -0
- package/dist/utils/richContext.d.ts.map +1 -0
- package/dist/utils/richContext.js +316 -0
- package/dist/utils/richContext.js.map +1 -0
- package/dist/utils/smartXmlBuilder.d.ts +187 -0
- package/dist/utils/smartXmlBuilder.d.ts.map +1 -0
- package/dist/utils/smartXmlBuilder.js +504 -0
- package/dist/utils/smartXmlBuilder.js.map +1 -0
- package/dist/utils/stdioSessionInfo.d.ts +53 -0
- package/dist/utils/stdioSessionInfo.d.ts.map +1 -0
- package/dist/utils/stdioSessionInfo.js +47 -0
- package/dist/utils/stdioSessionInfo.js.map +1 -0
- package/dist/utils/suggestionEngine.d.ts +20 -0
- package/dist/utils/suggestionEngine.d.ts.map +1 -0
- package/dist/utils/suggestionEngine.js +145 -0
- package/dist/utils/suggestionEngine.js.map +1 -0
- package/dist/utils/symbolLookup.d.ts +60 -0
- package/dist/utils/symbolLookup.d.ts.map +1 -0
- package/dist/utils/symbolLookup.js +0 -0
- package/dist/utils/symbolLookup.js.map +1 -0
- package/dist/utils/terminalUi.d.ts +85 -0
- package/dist/utils/terminalUi.d.ts.map +1 -0
- package/dist/utils/terminalUi.js +182 -0
- package/dist/utils/terminalUi.js.map +1 -0
- package/dist/utils/toolMetrics.d.ts +32 -0
- package/dist/utils/toolMetrics.d.ts.map +1 -0
- package/dist/utils/toolMetrics.js +89 -0
- package/dist/utils/toolMetrics.js.map +1 -0
- package/dist/utils/toolProgressMessage.d.ts +8 -0
- package/dist/utils/toolProgressMessage.d.ts.map +1 -0
- package/dist/utils/toolProgressMessage.js +182 -0
- package/dist/utils/toolProgressMessage.js.map +1 -0
- package/dist/utils/workspaceDetector.d.ts +51 -0
- package/dist/utils/workspaceDetector.d.ts.map +1 -0
- package/dist/utils/workspaceDetector.js +328 -0
- package/dist/utils/workspaceDetector.js.map +1 -0
- package/dist/utils/xppConfigProvider.d.ts +48 -0
- package/dist/utils/xppConfigProvider.d.ts.map +1 -0
- package/dist/utils/xppConfigProvider.js +121 -0
- package/dist/utils/xppConfigProvider.js.map +1 -0
- package/dist/utils/xppDocGen.d.ts +39 -0
- package/dist/utils/xppDocGen.d.ts.map +1 -0
- package/dist/utils/xppDocGen.js +518 -0
- package/dist/utils/xppDocGen.js.map +1 -0
- package/dist/utils/xppFormat.d.ts +21 -0
- package/dist/utils/xppFormat.d.ts.map +1 -0
- package/dist/utils/xppFormat.js +102 -0
- package/dist/utils/xppFormat.js.map +1 -0
- package/dist/utils/xppSelectLint.d.ts +18 -0
- package/dist/utils/xppSelectLint.d.ts.map +1 -0
- package/dist/utils/xppSelectLint.js +80 -0
- package/dist/utils/xppSelectLint.js.map +1 -0
- package/dist/validation/formPatternValidator.d.ts +53 -0
- package/dist/validation/formPatternValidator.d.ts.map +1 -0
- package/dist/validation/formPatternValidator.js +454 -0
- package/dist/validation/formPatternValidator.js.map +1 -0
- package/dist/workspace/contextRanker.d.ts +58 -0
- package/dist/workspace/contextRanker.d.ts.map +1 -0
- package/dist/workspace/contextRanker.js +216 -0
- package/dist/workspace/contextRanker.js.map +1 -0
- package/dist/workspace/contextSnapshot.d.ts +68 -0
- package/dist/workspace/contextSnapshot.d.ts.map +1 -0
- package/dist/workspace/contextSnapshot.js +193 -0
- package/dist/workspace/contextSnapshot.js.map +1 -0
- package/dist/workspace/hybridSearch.d.ts +39 -0
- package/dist/workspace/hybridSearch.d.ts.map +1 -0
- package/dist/workspace/hybridSearch.js +96 -0
- package/dist/workspace/hybridSearch.js.map +1 -0
- package/dist/workspace/workspaceScanner.d.ts +95 -0
- package/dist/workspace/workspaceScanner.d.ts.map +1 -0
- package/dist/workspace/workspaceScanner.js +247 -0
- package/dist/workspace/workspaceScanner.js.map +1 -0
- package/dist/workspace/workspaceUtils.d.ts +23 -0
- package/dist/workspace/workspaceUtils.d.ts.map +1 -0
- package/dist/workspace/workspaceUtils.js +77 -0
- package/dist/workspace/workspaceUtils.js.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,4185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D365FO File Creator Tool
|
|
3
|
+
* Creates physical XML files in the AOT package structure
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { Parser, Builder } from 'xml2js';
|
|
9
|
+
import { getConfigManager, fallbackPackagePath } from '../utils/configManager.js';
|
|
10
|
+
import { registerCustomModel, resolveObjectPrefix, applyObjectPrefix, getObjectSuffix, applyObjectSuffix, getExtensionNamingStyle } from '../utils/modelClassifier.js';
|
|
11
|
+
import { PackageResolver } from '../utils/packageResolver.js';
|
|
12
|
+
import { ensureXppDocComment, ensureBlankLineBeforeClosingBrace } from '../utils/xppDocGen.js';
|
|
13
|
+
import { reindentXppSource } from '../utils/xppFormat.js';
|
|
14
|
+
import { decodeXmlEntitiesFromXppSource } from './modifyD365File.js';
|
|
15
|
+
import { bridgeValidateAfterWrite, canBridgeCreate, bridgeCreateObject, bridgeCreateSmartTable, bridgeRefreshProvider } from '../bridge/index.js';
|
|
16
|
+
import { enforceGrounding } from '../utils/provenanceStore.js';
|
|
17
|
+
import { gateOnFormPatternErrors, isFormPatternEnforceEnabled } from './validateFormPattern.js';
|
|
18
|
+
import { validateFormExtensionControlShape, buildFormExtensionShapeError } from '../utils/formExtensionShapeValidator.js';
|
|
19
|
+
import { FormPatternTemplates } from '../utils/formPatternTemplates.js';
|
|
20
|
+
import { gateOnReferenceErrors } from './resolveReferences.js';
|
|
21
|
+
import { normalizeD365Xml } from '../utils/d365XmlNormalizer.js';
|
|
22
|
+
import { buildAxSecurityPrivilegeXml } from './securityPrivilegeXml.js';
|
|
23
|
+
import { buildAxDataEntityXml } from './dataEntityXml.js';
|
|
24
|
+
import { resolveEdtBaseType, resolveEdtEnumType, heuristicEdtBaseType, isEnumName, bridgeEdtBaseType } from './generateSmartTable.js';
|
|
25
|
+
import { buildAxQueryXml, buildAxViewXml } from './queryViewXml.js';
|
|
26
|
+
import { buildAxMapXml } from './mapXml.js';
|
|
27
|
+
/**
|
|
28
|
+
* Per-project-file mutex to serialise concurrent addToProject calls.
|
|
29
|
+
* Key = normalised absolute project path, Value = tail of the promise chain.
|
|
30
|
+
* Prevents race conditions when the AI calls create_d365fo_file in parallel
|
|
31
|
+
* for multiple objects that share the same .rnrproj file.
|
|
32
|
+
*/
|
|
33
|
+
const projectFileLocks = new Map();
|
|
34
|
+
async function withProjectFileLock(projectPath, fn) {
|
|
35
|
+
const key = path.resolve(projectPath).toLowerCase();
|
|
36
|
+
const prev = projectFileLocks.get(key) ?? Promise.resolve();
|
|
37
|
+
let resolve;
|
|
38
|
+
const next = new Promise(r => { resolve = r; });
|
|
39
|
+
projectFileLocks.set(key, next);
|
|
40
|
+
try {
|
|
41
|
+
await prev;
|
|
42
|
+
return await fn();
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
resolve();
|
|
46
|
+
if (projectFileLocks.get(key) === next) {
|
|
47
|
+
projectFileLocks.delete(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const CreateD365FileArgsSchema = z.object({
|
|
52
|
+
objectType: z
|
|
53
|
+
.enum([
|
|
54
|
+
'class', 'class-extension', 'table', 'enum', 'form', 'query', 'view', 'data-entity', 'report',
|
|
55
|
+
'edt', 'edt-extension',
|
|
56
|
+
'table-extension', 'form-extension', 'data-entity-extension', 'enum-extension',
|
|
57
|
+
'menu-item-display', 'menu-item-action', 'menu-item-output',
|
|
58
|
+
'menu-item-display-extension', 'menu-item-action-extension', 'menu-item-output-extension',
|
|
59
|
+
'menu', 'menu-extension',
|
|
60
|
+
'security-privilege', 'security-duty', 'security-role',
|
|
61
|
+
'security-duty-extension', 'security-role-extension',
|
|
62
|
+
'business-event', 'tile', 'kpi', 'map',
|
|
63
|
+
])
|
|
64
|
+
.describe('Type of D365FO object to create'),
|
|
65
|
+
objectName: z
|
|
66
|
+
.string()
|
|
67
|
+
.describe('Name of the object (e.g., MyHelperClass, MyCustomTable)'),
|
|
68
|
+
modelName: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe('Model name (e.g., ContosoExtensions). Auto-detected from mcp.json if omitted.'),
|
|
72
|
+
packageName: z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe('Package name (e.g., CustomExtensions, ApplicationSuite). Auto-resolved from model name if omitted.'),
|
|
76
|
+
packagePath: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe('Base package path (default: auto-detected from .mcp.json or well-known locations: C:\\, J:\\, K:\\AosService\\PackagesLocalDirectory)'),
|
|
80
|
+
sourceCode: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('X++ source code for the object (class declaration, methods, etc.)'),
|
|
84
|
+
properties: z
|
|
85
|
+
.record(z.string(), z.any())
|
|
86
|
+
.optional()
|
|
87
|
+
.describe('Additional properties for the object (extends, implements, etc.)'),
|
|
88
|
+
addToProject: z
|
|
89
|
+
.boolean()
|
|
90
|
+
.optional()
|
|
91
|
+
.default(true)
|
|
92
|
+
.describe('Whether to automatically add file to Visual Studio project (default: true — always pass true unless explicitly told not to)'),
|
|
93
|
+
projectPath: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe('Path to .rnrproj file. Required for addToProject to work. If not specified, auto-detected from .mcp.json or workspace context.'),
|
|
97
|
+
solutionPath: z
|
|
98
|
+
.string()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe('Path to active VS solution directory. Used to find .rnrproj when projectPath is not given.'),
|
|
101
|
+
xmlContent: z
|
|
102
|
+
.string()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe('Custom XML content to write verbatim instead of generating a template. ' +
|
|
105
|
+
'Use this in hybrid setups: call generate / generate on Azure ' +
|
|
106
|
+
'to get AI-driven XML, then pass that XML here on the local Windows VM to write the file ' +
|
|
107
|
+
'and add it to the VS2022 project.'),
|
|
108
|
+
overwrite: z
|
|
109
|
+
.boolean()
|
|
110
|
+
.optional()
|
|
111
|
+
.default(false)
|
|
112
|
+
.describe('Allow overwriting an existing file. Use together with xmlContent when you need to completely ' +
|
|
113
|
+
'rewrite an object (e.g. table with corrupted field names). Default: false (returns error if file already exists).'),
|
|
114
|
+
groundingToken: z
|
|
115
|
+
.string()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe('Provenance token returned by prepare(mode="change"). Proves the change was grounded in the indexed codebase. ' +
|
|
118
|
+
'Required for *-extension objectTypes when GROUNDING_ENFORCE=true on the server.'),
|
|
119
|
+
});
|
|
120
|
+
/**
|
|
121
|
+
* Project File Finder
|
|
122
|
+
* Finds .rnrproj files in solution directory or specific paths
|
|
123
|
+
*/
|
|
124
|
+
export class ProjectFileFinder {
|
|
125
|
+
/**
|
|
126
|
+
* Find .rnrproj file in solution directory
|
|
127
|
+
* Recursively searches for .rnrproj files matching the model name (up to 3 levels deep)
|
|
128
|
+
*/
|
|
129
|
+
static async findProjectInSolution(solutionPath, modelName) {
|
|
130
|
+
return ProjectFileFinder.findRecursive(solutionPath, modelName, 0, 3);
|
|
131
|
+
}
|
|
132
|
+
static async findRecursive(dir, modelName, currentDepth, maxDepth) {
|
|
133
|
+
if (currentDepth > maxDepth)
|
|
134
|
+
return null;
|
|
135
|
+
try {
|
|
136
|
+
await fs.access(dir);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
let files;
|
|
142
|
+
try {
|
|
143
|
+
files = await fs.readdir(dir);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
// Check .rnrproj files at this level first
|
|
149
|
+
const projectFiles = files.filter(file => file.endsWith('.rnrproj') &&
|
|
150
|
+
(file.includes(modelName) || file === `${modelName}.rnrproj`));
|
|
151
|
+
if (projectFiles.length > 0) {
|
|
152
|
+
return path.join(dir, projectFiles[0]);
|
|
153
|
+
}
|
|
154
|
+
// Recurse into subdirectories
|
|
155
|
+
for (const file of files) {
|
|
156
|
+
const fullPath = path.join(dir, file);
|
|
157
|
+
try {
|
|
158
|
+
const stat = await fs.stat(fullPath);
|
|
159
|
+
if (stat.isDirectory()) {
|
|
160
|
+
const found = await ProjectFileFinder.findRecursive(fullPath, modelName, currentDepth + 1, maxDepth);
|
|
161
|
+
if (found)
|
|
162
|
+
return found;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Map a D365FO base type name to the XML i:type attribute used in <AxTableField>.
|
|
174
|
+
* If the explicit fieldType is not a known primitive, fall back to name-based heuristics
|
|
175
|
+
* using edtName (same heuristics as SmartXmlBuilder.getAxTableFieldType).
|
|
176
|
+
*/
|
|
177
|
+
function fieldTypeToAxType(fieldType, edtName) {
|
|
178
|
+
const typeMap = {
|
|
179
|
+
String: 'AxTableFieldString',
|
|
180
|
+
Integer: 'AxTableFieldInt',
|
|
181
|
+
Int64: 'AxTableFieldInt64',
|
|
182
|
+
Real: 'AxTableFieldReal',
|
|
183
|
+
Date: 'AxTableFieldDate',
|
|
184
|
+
DateTime: 'AxTableFieldUtcDateTime',
|
|
185
|
+
UtcDateTime: 'AxTableFieldUtcDateTime',
|
|
186
|
+
Enum: 'AxTableFieldEnum',
|
|
187
|
+
GUID: 'AxTableFieldGuid',
|
|
188
|
+
Guid: 'AxTableFieldGuid',
|
|
189
|
+
Container: 'AxTableFieldContainer',
|
|
190
|
+
};
|
|
191
|
+
const explicit = typeMap[fieldType];
|
|
192
|
+
if (explicit)
|
|
193
|
+
return explicit;
|
|
194
|
+
// Fall back to EDT name heuristics (mirrors SmartXmlBuilder.getAxTableFieldType)
|
|
195
|
+
const hint = edtName || fieldType;
|
|
196
|
+
if (hint) {
|
|
197
|
+
const e = hint.toLowerCase();
|
|
198
|
+
if (e === 'recid' || e.endsWith('recid') || e.includes('refrecid'))
|
|
199
|
+
return 'AxTableFieldInt64';
|
|
200
|
+
if (e.includes('utcdatetime') || (e.includes('datetime') && !e.includes('transdate')))
|
|
201
|
+
return 'AxTableFieldUtcDateTime';
|
|
202
|
+
if (e.includes('date') && !e.includes('time') && !e.includes('update'))
|
|
203
|
+
return 'AxTableFieldDate';
|
|
204
|
+
if (e.includes('amount') || e.includes('mst') || e.includes('price') || e.includes('qty')
|
|
205
|
+
|| e.includes('percent') || e === 'real')
|
|
206
|
+
return 'AxTableFieldReal';
|
|
207
|
+
if (e === 'noyesid' || e.endsWith('noyesid') || e === 'noyes')
|
|
208
|
+
return 'AxTableFieldEnum';
|
|
209
|
+
if ((e.endsWith('int') || e.includes('count') || e.includes('level'))
|
|
210
|
+
&& !e.includes('account') && !e.includes('name'))
|
|
211
|
+
return 'AxTableFieldInt';
|
|
212
|
+
}
|
|
213
|
+
return 'AxTableFieldString';
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Normalize the flexible field specs accepted by the tool / XML generators
|
|
217
|
+
* (`{ name, edt?, type?, fieldType?, extendedDataType?, enumType?, mandatory?, label? }`)
|
|
218
|
+
* into the key shape the C# bridge's WriteFieldParam actually deserializes.
|
|
219
|
+
*
|
|
220
|
+
* The bridge only reads JSON keys `type` and `edt` (`[JsonPropertyName]`), not
|
|
221
|
+
* `fieldType`/`extendedDataType` — accept either input spelling and always emit
|
|
222
|
+
* the bridge's keys. `type` may arrive as a base-type keyword ("Integer") or a
|
|
223
|
+
* full i:type ("AxTableFieldInt"); the latter is stripped back to the keyword
|
|
224
|
+
* the bridge's CreateTableField switch understands.
|
|
225
|
+
*/
|
|
226
|
+
export function normalizeFieldSpecsForBridge(fields) {
|
|
227
|
+
return fields.map((f) => {
|
|
228
|
+
let fieldType = f.type ?? f.fieldType;
|
|
229
|
+
if (typeof fieldType === 'string')
|
|
230
|
+
fieldType = fieldType.replace(/^AxTableField/, '');
|
|
231
|
+
const out = { name: f.name };
|
|
232
|
+
if (fieldType != null && fieldType !== '')
|
|
233
|
+
out.type = fieldType;
|
|
234
|
+
if (f.edt != null)
|
|
235
|
+
out.edt = f.edt;
|
|
236
|
+
else if (f.extendedDataType != null)
|
|
237
|
+
out.edt = f.extendedDataType;
|
|
238
|
+
if (f.enumType != null)
|
|
239
|
+
out.enumType = f.enumType;
|
|
240
|
+
if (f.mandatory != null)
|
|
241
|
+
out.mandatory = f.mandatory;
|
|
242
|
+
if (f.label != null)
|
|
243
|
+
out.label = f.label;
|
|
244
|
+
if (f.stringSize != null)
|
|
245
|
+
out.stringSize = f.stringSize;
|
|
246
|
+
return out;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Normalize the flexible index specs accepted by the tool into the key shape the
|
|
251
|
+
* C# bridge's WriteIndexParam actually deserializes: `{ name, fields: string[],
|
|
252
|
+
* alternateKey?, allowDuplicates? }`.
|
|
253
|
+
*
|
|
254
|
+
* Accepts both the bridge's native `{name, fields}` shape and the documented
|
|
255
|
+
* `modify(operation="add-index")` shape `{indexName, indexFields: [{fieldName}]}`.
|
|
256
|
+
* Unrecognized keys are silently ignored by System.Text.Json, so any unmapped
|
|
257
|
+
* shape produces an index with an empty Name/Fields that xppc rejects at build time.
|
|
258
|
+
*/
|
|
259
|
+
export function normalizeIndexSpecsForBridge(indexes) {
|
|
260
|
+
return indexes.map((idx) => {
|
|
261
|
+
const name = idx.name ?? idx.indexName;
|
|
262
|
+
const rawFields = (idx.fields ?? idx.indexFields);
|
|
263
|
+
const fields = Array.isArray(rawFields)
|
|
264
|
+
? rawFields
|
|
265
|
+
.map((f) => (typeof f === 'string' ? f : f?.fieldName))
|
|
266
|
+
.filter((f) => typeof f === 'string' && f.length > 0)
|
|
267
|
+
: undefined;
|
|
268
|
+
const out = { name };
|
|
269
|
+
if (fields && fields.length > 0)
|
|
270
|
+
out.fields = fields;
|
|
271
|
+
if (idx.alternateKey != null)
|
|
272
|
+
out.alternateKey = idx.alternateKey;
|
|
273
|
+
if (idx.allowDuplicates != null)
|
|
274
|
+
out.allowDuplicates = idx.allowDuplicates;
|
|
275
|
+
return out;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* XML Templates for different D365FO object types
|
|
280
|
+
*/
|
|
281
|
+
export class XmlTemplateGenerator {
|
|
282
|
+
/**
|
|
283
|
+
* Split X++ class source into the Declaration block (class header + field
|
|
284
|
+
* declarations) and individual method bodies, as required by D365FO XML.
|
|
285
|
+
*
|
|
286
|
+
* D365FO XML structure:
|
|
287
|
+
* <Declaration> = class keyword + field declarations (the outer {} block)
|
|
288
|
+
* <Methods> = one <Method><Name/><Source/></Method> per method body
|
|
289
|
+
*
|
|
290
|
+
* AI generators often emit the entire source (header + methods) as a single
|
|
291
|
+
* string. This helper separates them so the generated XML is correct.
|
|
292
|
+
*/
|
|
293
|
+
static splitXppClassSource(fullSource) {
|
|
294
|
+
// Find the '{' that opens the class body
|
|
295
|
+
const firstBrace = fullSource.indexOf('{');
|
|
296
|
+
if (firstBrace === -1)
|
|
297
|
+
return { declaration: fullSource, methods: [] };
|
|
298
|
+
// Walk to the matching '}' that closes the class header block
|
|
299
|
+
let depth = 0;
|
|
300
|
+
let classEndIdx = -1;
|
|
301
|
+
for (let i = firstBrace; i < fullSource.length; i++) {
|
|
302
|
+
if (fullSource[i] === '{')
|
|
303
|
+
depth++;
|
|
304
|
+
else if (fullSource[i] === '}') {
|
|
305
|
+
depth--;
|
|
306
|
+
if (depth === 0) {
|
|
307
|
+
classEndIdx = i;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (classEndIdx === -1)
|
|
313
|
+
return { declaration: fullSource, methods: [] };
|
|
314
|
+
let declaration = fullSource.substring(0, classEndIdx + 1);
|
|
315
|
+
const rest = fullSource.substring(classEndIdx + 1);
|
|
316
|
+
if (!rest.trim()) {
|
|
317
|
+
// Nothing after the class closing brace — methods may be nested inside the class braces instead.
|
|
318
|
+
const innerResult = XmlTemplateGenerator.extractInnerClassMethods(declaration);
|
|
319
|
+
if (innerResult) {
|
|
320
|
+
console.error('[splitXppClassSource] Inner-class methods detected — extracting into ' +
|
|
321
|
+
'separate <Method> elements (D365FO format).');
|
|
322
|
+
return innerResult;
|
|
323
|
+
}
|
|
324
|
+
// Ensure exactly one blank line before the closing '}' when the body has content.
|
|
325
|
+
const bodyStart = declaration.indexOf('{');
|
|
326
|
+
const bodyContent = declaration.substring(bodyStart + 1, declaration.lastIndexOf('}'));
|
|
327
|
+
if (bodyContent.trim().length > 0) {
|
|
328
|
+
declaration = declaration.replace(/\n+(\s*)}(\s*)$/, '\n\n}');
|
|
329
|
+
}
|
|
330
|
+
return { declaration, methods: [] };
|
|
331
|
+
}
|
|
332
|
+
// D365FO requires member-variable declarations inside <Declaration>, so rescue any that
|
|
333
|
+
// appear outside the class {} but before the first method.
|
|
334
|
+
const nextBraceInRest = rest.indexOf('{');
|
|
335
|
+
if (nextBraceInRest !== -1) {
|
|
336
|
+
const preMethodText = rest.substring(0, nextBraceInRest);
|
|
337
|
+
const varLines = preMethodText
|
|
338
|
+
.split('\n')
|
|
339
|
+
.filter(l => {
|
|
340
|
+
const t = l.trim();
|
|
341
|
+
// A variable declaration ends with ';' and does NOT contain '(' (not a method call/signature)
|
|
342
|
+
return t.endsWith(';') && !t.includes('(');
|
|
343
|
+
});
|
|
344
|
+
if (varLines.length > 0) {
|
|
345
|
+
// Inject the rescued declarations into the class body, just before the closing '}'
|
|
346
|
+
const injected = varLines.map(l => ' ' + l.trim()).join('\n');
|
|
347
|
+
declaration = declaration.replace(/}(\s*)$/, `\n${injected}\n\n}`);
|
|
348
|
+
console.error(`[splitXppClassSource] Rescued ${varLines.length} member variable declaration(s) ` +
|
|
349
|
+
'found outside the class {} block — injected into <Declaration>.');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Parse each method block from the remaining source
|
|
353
|
+
const methods = [];
|
|
354
|
+
let pos = 0;
|
|
355
|
+
while (pos < rest.length) {
|
|
356
|
+
const nextBrace = rest.indexOf('{', pos);
|
|
357
|
+
if (nextBrace === -1)
|
|
358
|
+
break;
|
|
359
|
+
const sigText = rest.substring(pos, nextBrace);
|
|
360
|
+
// Find the matching '}' for this method body (depth-counting)
|
|
361
|
+
let d = 0;
|
|
362
|
+
let bodyEnd = -1;
|
|
363
|
+
for (let i = nextBrace; i < rest.length; i++) {
|
|
364
|
+
if (rest[i] === '{')
|
|
365
|
+
d++;
|
|
366
|
+
else if (rest[i] === '}') {
|
|
367
|
+
d--;
|
|
368
|
+
if (d === 0) {
|
|
369
|
+
bodyEnd = i;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (bodyEnd === -1)
|
|
375
|
+
break;
|
|
376
|
+
const methodSource = rest.substring(pos, bodyEnd + 1).trim();
|
|
377
|
+
// Extract method name: last identifier before '(' in the signature
|
|
378
|
+
const parenIdx = sigText.lastIndexOf('(');
|
|
379
|
+
const nameMatch = parenIdx !== -1 ? sigText.substring(0, parenIdx).match(/(\w+)\s*$/) : null;
|
|
380
|
+
const methodName = nameMatch ? nameMatch[1] : `method${methods.length + 1}`;
|
|
381
|
+
methods.push({ name: methodName, source: methodSource });
|
|
382
|
+
pos = bodyEnd + 1;
|
|
383
|
+
}
|
|
384
|
+
// Fallback: if the source had no methods after the class body, they may be
|
|
385
|
+
// nested at depth-1 inside the class body instead — extract them there.
|
|
386
|
+
if (methods.length === 0) {
|
|
387
|
+
const innerResult = XmlTemplateGenerator.extractInnerClassMethods(declaration);
|
|
388
|
+
if (innerResult) {
|
|
389
|
+
console.error('[splitXppClassSource] Extracted inner class methods — moving them from ' +
|
|
390
|
+
'<Declaration> into separate <Method> elements (D365FO format).');
|
|
391
|
+
return innerResult;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { declaration, methods };
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Extract methods that are defined INSIDE the class body (depth-1 inside {}).
|
|
398
|
+
*
|
|
399
|
+
* D365FO XML requires each method as a separate <Method><Source/></Method> element.
|
|
400
|
+
* When AI generates a class with methods inside the class braces, all code ends up
|
|
401
|
+
* in <Declaration> with no blank-line separation between methods.
|
|
402
|
+
*
|
|
403
|
+
* This helper detects that pattern and returns the correct split:
|
|
404
|
+
* declaration = class header + member variable declarations only
|
|
405
|
+
* methods = each method body as a separate entry
|
|
406
|
+
*
|
|
407
|
+
* Returns null when no inner methods are found (i.e. the class body is just fields).
|
|
408
|
+
*/
|
|
409
|
+
static extractInnerClassMethods(classDeclaration) {
|
|
410
|
+
const classOpenIdx = classDeclaration.indexOf('{');
|
|
411
|
+
const classCloseIdx = classDeclaration.lastIndexOf('}');
|
|
412
|
+
if (classOpenIdx === -1 || classCloseIdx <= classOpenIdx)
|
|
413
|
+
return null;
|
|
414
|
+
const classBody = classDeclaration.substring(classOpenIdx + 1, classCloseIdx);
|
|
415
|
+
const methods = [];
|
|
416
|
+
const memberVarLines = [];
|
|
417
|
+
let pos = 0;
|
|
418
|
+
while (pos < classBody.length) {
|
|
419
|
+
const nextBrace = classBody.indexOf('{', pos);
|
|
420
|
+
if (nextBrace === -1) {
|
|
421
|
+
// No more braces — collect any trailing member-variable declarations
|
|
422
|
+
for (const line of classBody.substring(pos).split('\n')) {
|
|
423
|
+
const t = line.trim();
|
|
424
|
+
if (t.length > 0 && t.endsWith(';') && !t.includes('(') &&
|
|
425
|
+
!t.startsWith('//') && !t.startsWith('*')) {
|
|
426
|
+
memberVarLines.push(t);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
const sigText = classBody.substring(pos, nextBrace);
|
|
432
|
+
// Find the matching '}' for this block
|
|
433
|
+
let depth = 0;
|
|
434
|
+
let bodyEnd = -1;
|
|
435
|
+
for (let i = nextBrace; i < classBody.length; i++) {
|
|
436
|
+
if (classBody[i] === '{')
|
|
437
|
+
depth++;
|
|
438
|
+
else if (classBody[i] === '}') {
|
|
439
|
+
depth--;
|
|
440
|
+
if (depth === 0) {
|
|
441
|
+
bodyEnd = i;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (bodyEnd === -1)
|
|
447
|
+
break;
|
|
448
|
+
const parenIdx = sigText.lastIndexOf('(');
|
|
449
|
+
if (parenIdx !== -1) {
|
|
450
|
+
// The block is a method (sigText contains a '(' — it's a parameter list).
|
|
451
|
+
// Collect member-variable lines that appeared before the method signature.
|
|
452
|
+
for (const line of sigText.split('\n')) {
|
|
453
|
+
const t = line.trim();
|
|
454
|
+
if (t.length > 0 && t.endsWith(';') && !t.includes('(') &&
|
|
455
|
+
!t.startsWith('//') && !t.startsWith('*') && !t.startsWith('[')) {
|
|
456
|
+
memberVarLines.push(t);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// Find where the actual method signature starts within sigText:
|
|
460
|
+
// walk backward from the last '(' to include attribute annotations ('[…]').
|
|
461
|
+
const beforeLastParen = sigText.substring(0, parenIdx);
|
|
462
|
+
const lastNewlineBeforeLastParen = beforeLastParen.lastIndexOf('\n');
|
|
463
|
+
let methodStartInSig = lastNewlineBeforeLastParen !== -1
|
|
464
|
+
? lastNewlineBeforeLastParen + 1
|
|
465
|
+
: 0;
|
|
466
|
+
// Include any leading [Attribute] and doc-comment (///) lines that belong to this method.
|
|
467
|
+
// Walking BACKWARDS through the lines that appear above the method signature:
|
|
468
|
+
// we stop as soon as we hit a line that is neither empty, nor an attribute, nor a comment.
|
|
469
|
+
const sigBeforeMethod = sigText.substring(0, methodStartInSig);
|
|
470
|
+
const sigBeforeLines = sigBeforeMethod.split('\n').reverse();
|
|
471
|
+
let droppedChars = 0;
|
|
472
|
+
for (const line of sigBeforeLines) {
|
|
473
|
+
const t = line.trim();
|
|
474
|
+
if (t.length === 0 || t.startsWith('[') || t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) {
|
|
475
|
+
droppedChars += line.length + 1; // +1 for '\n'
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
methodStartInSig = Math.max(0, methodStartInSig - droppedChars);
|
|
482
|
+
const methodSource = classBody
|
|
483
|
+
.substring(pos + methodStartInSig, bodyEnd + 1)
|
|
484
|
+
.trim();
|
|
485
|
+
const nameMatch = sigText.substring(0, parenIdx).match(/(\w+)\s*$/);
|
|
486
|
+
const methodName = nameMatch ? nameMatch[1] : `method${methods.length + 1}`;
|
|
487
|
+
methods.push({ name: methodName, source: methodSource });
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
// Not a method (no '(' in sigText) — collect member-variable declarations
|
|
491
|
+
for (const line of sigText.split('\n')) {
|
|
492
|
+
const t = line.trim();
|
|
493
|
+
if (t.length > 0 && t.endsWith(';') && !t.includes('(') &&
|
|
494
|
+
!t.startsWith('//') && !t.startsWith('*')) {
|
|
495
|
+
memberVarLines.push(t);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Skip the block (e.g. object initialiser)
|
|
499
|
+
}
|
|
500
|
+
pos = bodyEnd + 1;
|
|
501
|
+
}
|
|
502
|
+
if (methods.length === 0)
|
|
503
|
+
return null;
|
|
504
|
+
// Rebuild the declaration as: class header + member variable declarations only
|
|
505
|
+
const classHeader = classDeclaration.substring(0, classOpenIdx + 1);
|
|
506
|
+
const memberVarsXpp = memberVarLines.length > 0
|
|
507
|
+
? '\n' + memberVarLines.map(v => ' ' + v).join('\n') + '\n\n'
|
|
508
|
+
: '\n';
|
|
509
|
+
return {
|
|
510
|
+
declaration: classHeader + memberVarsXpp + '}',
|
|
511
|
+
methods,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* If `declaration`'s own `class`/`interface` header names something other than
|
|
516
|
+
* `className` (e.g. the object is being created as "ContosoFoo" but the caller's
|
|
517
|
+
* X++ still says `class Foo`), rename every self-reference to that stale name —
|
|
518
|
+
* in the header AND in every method body (constructor calls, return types,
|
|
519
|
+
* etc.) — to `className`.
|
|
520
|
+
*
|
|
521
|
+
* Left unfixed, the AOT object's `<Name>` (which the create path always sets to
|
|
522
|
+
* the resolved `className`, independent of whatever the caller typed in
|
|
523
|
+
* `sourceCode`) does not match its own X++ class keyword — a hard xppc build
|
|
524
|
+
* error ("class must be named the same as the object it is contained in"),
|
|
525
|
+
* confirmed by corpus evidence: the caller passed an already-correct,
|
|
526
|
+
* fully-resolved objectName ("ContosoXyzNoteFormatter") — so the existing
|
|
527
|
+
* objectName-vs-finalObjectName prefix-mismatch guard below never fired — while
|
|
528
|
+
* sourceCode's `class XyzNoteFormatter` used the bare, unprefixed name
|
|
529
|
+
* (eval/corpus/runs/2026-07-06T16__L1-class-basic__73707ff.json).
|
|
530
|
+
*/
|
|
531
|
+
static normalizeSelfReferenceName(className, declaration, methods) {
|
|
532
|
+
const declaredName = declaration.match(/\b(?:class|interface)\s+(\w+)/)?.[1];
|
|
533
|
+
if (!declaredName || declaredName === className)
|
|
534
|
+
return { declaration, methods };
|
|
535
|
+
const re = new RegExp(`\\b${declaredName}\\b`, 'g');
|
|
536
|
+
return {
|
|
537
|
+
declaration: declaration.replace(re, className),
|
|
538
|
+
methods: methods.map(m => ({
|
|
539
|
+
...m,
|
|
540
|
+
source: m.source !== undefined ? m.source.replace(re, className) : m.source,
|
|
541
|
+
})),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Parse X++ sourceCode into declaration + methods for the C# bridge.
|
|
546
|
+
*
|
|
547
|
+
* Used by the bridge-first creation path in create_d365fo_file — the C# side
|
|
548
|
+
* expects declaration (class header + member vars) and an array of method
|
|
549
|
+
* objects {name, source} which it sets on the AxClass via IMetadataProvider.
|
|
550
|
+
*
|
|
551
|
+
* Delegates to splitXppClassSource after decoding any XML entities.
|
|
552
|
+
*
|
|
553
|
+
* `className` is the resolved AOT object name the bridge will create this
|
|
554
|
+
* class under (`finalObjectName`) — passing it lets self-references in the
|
|
555
|
+
* caller's sourceCode that don't match get corrected (normalizeSelfReferenceName).
|
|
556
|
+
*/
|
|
557
|
+
static parseSourceForBridge(sourceCode, className) {
|
|
558
|
+
// Same entity-decoding as generateAxClassXml to handle AI-generated < etc.
|
|
559
|
+
const cleaned = decodeXmlEntitiesFromXppSource(sourceCode);
|
|
560
|
+
const split = XmlTemplateGenerator.splitXppClassSource(cleaned);
|
|
561
|
+
const result = className
|
|
562
|
+
? XmlTemplateGenerator.normalizeSelfReferenceName(className, split.declaration, split.methods)
|
|
563
|
+
: split;
|
|
564
|
+
return {
|
|
565
|
+
declaration: result.declaration,
|
|
566
|
+
// The bridge stores each method's source verbatim (no reformatting on its
|
|
567
|
+
// side) — re-derive consistent indentation here so this CREATE path gets
|
|
568
|
+
// the same fix as bridgeAddMethod's MODIFY path.
|
|
569
|
+
methods: result.methods.map(m => ({
|
|
570
|
+
...m,
|
|
571
|
+
source: m.source !== undefined ? reindentXppSource(m.source) : m.source,
|
|
572
|
+
})),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Generate AxClass XML structure
|
|
577
|
+
*/
|
|
578
|
+
static generateAxClassXml(className, sourceCode, properties) {
|
|
579
|
+
// Decode XML entities that AI models may introduce when copying from SSRS report
|
|
580
|
+
// entity-encoded <Text> blocks (e.g. <summary> → <summary>).
|
|
581
|
+
const rawSource = decodeXmlEntitiesFromXppSource(sourceCode || `public class ${className}\n{\n}`);
|
|
582
|
+
// Split full X++ source into Declaration (class header + fields) and Methods.
|
|
583
|
+
// D365FO XML requires member variable declarations in <Declaration> and
|
|
584
|
+
// each method body as a separate <Method> element under <Methods>.
|
|
585
|
+
const rawSplit = XmlTemplateGenerator.splitXppClassSource(rawSource);
|
|
586
|
+
// Correct any stale self-reference (caller's class/interface name doesn't match
|
|
587
|
+
// the resolved `className`) so the emitted <Name> and X++ class keyword agree.
|
|
588
|
+
const { declaration, methods } = XmlTemplateGenerator.normalizeSelfReferenceName(className, rawSplit.declaration, rawSplit.methods);
|
|
589
|
+
const extendsAttr = properties?.extends
|
|
590
|
+
? `\t<Extends>${properties.extends}</Extends>\n`
|
|
591
|
+
: '';
|
|
592
|
+
const implementsAttr = properties?.implements
|
|
593
|
+
? `\t<Implements>${properties.implements}</Implements>\n`
|
|
594
|
+
: '';
|
|
595
|
+
const methodsXml = methods.length === 0
|
|
596
|
+
? '\t\t<Methods />\n'
|
|
597
|
+
: `\t\t<Methods>\n${methods
|
|
598
|
+
.map(m => `\t\t\t<Method>\n\t\t\t\t<Name>${m.name}</Name>\n\t\t\t\t<Source><![CDATA[\n${reindentXppSource(ensureXppDocComment(m.source))}\n\n]]></Source>\n\t\t\t</Method>`)
|
|
599
|
+
.join('\n\n')}\n\t\t</Methods>\n`;
|
|
600
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
601
|
+
<AxClass xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
602
|
+
\t<Name>${className}</Name>
|
|
603
|
+
${extendsAttr}${implementsAttr}\t<SourceCode>
|
|
604
|
+
\t\t<Declaration><![CDATA[
|
|
605
|
+
${ensureBlankLineBeforeClosingBrace(ensureXppDocComment(declaration))}
|
|
606
|
+
]]></Declaration>
|
|
607
|
+
${methodsXml}\t</SourceCode>
|
|
608
|
+
</AxClass>
|
|
609
|
+
`;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Generate AxClass XML structure for a Chain of Command (class-extension).
|
|
613
|
+
* The XML format is identical to a regular AxClass — the distinction is purely
|
|
614
|
+
* in the X++ source code ([ExtensionOf(classStr(...))] + final modifier).
|
|
615
|
+
*
|
|
616
|
+
* properties.baseClass — name of the class being extended (required)
|
|
617
|
+
* properties.modelInfix — naming infix, e.g. "ContosoExt" → BaseClass_ContosoExt_Extension
|
|
618
|
+
*/
|
|
619
|
+
static generateAxClassExtensionXml(extensionName, sourceCode, properties) {
|
|
620
|
+
const baseClass = properties?.baseClass || extensionName.replace(/_[^_]+_Extension$/, '');
|
|
621
|
+
const defaultSource = sourceCode ||
|
|
622
|
+
`[ExtensionOf(classStr(${baseClass}))]\nfinal class ${extensionName}\n{\n // ⚠️ ALWAYS call next <methodName>() — verify exact signature with:\n // get_method(include="signature", "${baseClass}", "methodName")\n //\n // Template for wrapping a method:\n // public ReturnType methodName(ParamType _param)\n // {\n // ReturnType result = next methodName(_param);\n // return result;\n // }\n}`;
|
|
623
|
+
return XmlTemplateGenerator.generateAxClassXml(extensionName, defaultSource, { ...properties });
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Generate AxTable XML structure (based on real D365FO table structure)
|
|
627
|
+
*/
|
|
628
|
+
static generateAxTableXml(tableName, properties) {
|
|
629
|
+
const label = properties?.label || tableName;
|
|
630
|
+
const tableGroup = properties?.tableGroup || 'Main';
|
|
631
|
+
const tableType = properties?.tableType || '';
|
|
632
|
+
const titleField1 = properties?.titleField1 || '';
|
|
633
|
+
const titleField2 = properties?.titleField2 || '';
|
|
634
|
+
const configKey = properties?.configurationKey || '';
|
|
635
|
+
const primaryIndex = properties?.primaryIndex || '';
|
|
636
|
+
const cacheLookup = properties?.cacheLookup || '';
|
|
637
|
+
// Build optional configuration key
|
|
638
|
+
const configKeyXml = configKey
|
|
639
|
+
? `\t<ConfigurationKey>${configKey}</ConfigurationKey>\n`
|
|
640
|
+
: '';
|
|
641
|
+
// Build optional cache lookup (only if explicitly set)
|
|
642
|
+
const cacheLookupXml = cacheLookup
|
|
643
|
+
? `\t<CacheLookup>${cacheLookup}</CacheLookup>\n`
|
|
644
|
+
: '';
|
|
645
|
+
// Build optional primary index (NOTE: ClusteredIndex is NOT in real D365FO files)
|
|
646
|
+
const primaryIndexXml = primaryIndex
|
|
647
|
+
? `\t<PrimaryIndex>${primaryIndex}</PrimaryIndex>\n\t<ReplacementKey>${primaryIndex}</ReplacementKey>\n`
|
|
648
|
+
: '';
|
|
649
|
+
// Build optional TableType (TempDB, InMemory; omit for Regular — it's the default)
|
|
650
|
+
const tableTypeXml = tableType
|
|
651
|
+
? `\t<TableType>${tableType}</TableType>\n`
|
|
652
|
+
: '';
|
|
653
|
+
// Build <Fields> block from properties.fields array (TableFieldSpec[]).
|
|
654
|
+
// Copilot may pass field definitions via properties.fields or via sourceCode JSON —
|
|
655
|
+
// both paths merge into properties before calling here (see generate()).
|
|
656
|
+
// Field-spec keys are unified with the table-extension path (generateAxTableExtensionXml):
|
|
657
|
+
// accept an explicit AxTableField* i:type (fieldType), a primitive base type (type),
|
|
658
|
+
// or infer AxTableFieldEnum from enumType — and always emit <EnumType> for enum fields.
|
|
659
|
+
const fieldSpecs = Array.isArray(properties?.fields) ? properties.fields : [];
|
|
660
|
+
let fieldsXml;
|
|
661
|
+
if (fieldSpecs.length === 0) {
|
|
662
|
+
fieldsXml = '\t<Fields />\n';
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
fieldsXml = '\t<Fields>\n';
|
|
666
|
+
for (const f of fieldSpecs) {
|
|
667
|
+
// Determine i:type: explicit AxTableField* wins; otherwise derive from the
|
|
668
|
+
// primitive type / enumType / EDT name heuristics. NEVER default to
|
|
669
|
+
// AxTableFieldString blindly when an EDT or enumType is present.
|
|
670
|
+
const iType = f.fieldType
|
|
671
|
+
?? fieldTypeToAxType(f.type || (f.enumType ? 'Enum' : 'String'), f.edt);
|
|
672
|
+
fieldsXml += `\t\t<AxTableField xmlns=""\n\t\t\ti:type="${iType}">\n`;
|
|
673
|
+
fieldsXml += `\t\t\t<Name>${f.name}</Name>\n`;
|
|
674
|
+
if (f.edt)
|
|
675
|
+
fieldsXml += `\t\t\t<ExtendedDataType>${f.edt}</ExtendedDataType>\n`;
|
|
676
|
+
if (f.label)
|
|
677
|
+
fieldsXml += `\t\t\t<Label>${f.label}</Label>\n`;
|
|
678
|
+
if (f.mandatory)
|
|
679
|
+
fieldsXml += `\t\t\t<Mandatory>Yes</Mandatory>\n`;
|
|
680
|
+
if (f.enumType)
|
|
681
|
+
fieldsXml += `\t\t\t<EnumType>${f.enumType}</EnumType>\n`;
|
|
682
|
+
fieldsXml += `\t\t</AxTableField>\n`;
|
|
683
|
+
}
|
|
684
|
+
fieldsXml += '\t</Fields>\n';
|
|
685
|
+
}
|
|
686
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
687
|
+
<AxTable xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
688
|
+
\t<Name>${tableName}</Name>
|
|
689
|
+
\t<SourceCode>
|
|
690
|
+
\t\t<Declaration><![CDATA[
|
|
691
|
+
public class ${tableName} extends common
|
|
692
|
+
{
|
|
693
|
+
}
|
|
694
|
+
]]></Declaration>
|
|
695
|
+
\t\t<Methods />
|
|
696
|
+
\t</SourceCode>
|
|
697
|
+
${configKeyXml}\t<Label>${label}</Label>
|
|
698
|
+
\t<TableGroup>${tableGroup}</TableGroup>
|
|
699
|
+
${tableTypeXml}\t<TitleField1>${titleField1}</TitleField1>
|
|
700
|
+
\t<TitleField2>${titleField2}</TitleField2>
|
|
701
|
+
${cacheLookupXml}${primaryIndexXml}\t<DeleteActions />
|
|
702
|
+
\t<FieldGroups>
|
|
703
|
+
\t\t<AxTableFieldGroup>
|
|
704
|
+
\t\t\t<Name>AutoReport</Name>
|
|
705
|
+
\t\t\t<Fields />
|
|
706
|
+
\t\t</AxTableFieldGroup>
|
|
707
|
+
\t\t<AxTableFieldGroup>
|
|
708
|
+
\t\t\t<Name>AutoLookup</Name>
|
|
709
|
+
\t\t\t<Fields />
|
|
710
|
+
\t\t</AxTableFieldGroup>
|
|
711
|
+
\t\t<AxTableFieldGroup>
|
|
712
|
+
\t\t\t<Name>AutoIdentification</Name>
|
|
713
|
+
\t\t\t<AutoPopulate>Yes</AutoPopulate>
|
|
714
|
+
\t\t\t<Fields />
|
|
715
|
+
\t\t</AxTableFieldGroup>
|
|
716
|
+
\t\t<AxTableFieldGroup>
|
|
717
|
+
\t\t\t<Name>AutoSummary</Name>
|
|
718
|
+
\t\t\t<Fields />
|
|
719
|
+
\t\t</AxTableFieldGroup>
|
|
720
|
+
\t\t<AxTableFieldGroup>
|
|
721
|
+
\t\t\t<Name>AutoBrowse</Name>
|
|
722
|
+
\t\t\t<Fields />
|
|
723
|
+
\t\t</AxTableFieldGroup>
|
|
724
|
+
\t</FieldGroups>
|
|
725
|
+
${fieldsXml}\t<FullTextIndexes />
|
|
726
|
+
\t<Indexes />
|
|
727
|
+
\t<Mappings />
|
|
728
|
+
\t<Relations />
|
|
729
|
+
\t<StateMachines />
|
|
730
|
+
</AxTable>
|
|
731
|
+
`;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Generate AxEnum XML structure
|
|
735
|
+
*/
|
|
736
|
+
static generateAxEnumXml(enumName, properties) {
|
|
737
|
+
const label = properties?.label || enumName;
|
|
738
|
+
// Extensible enums MUST have UseEnumValue=No (xppc hard requirement).
|
|
739
|
+
// Explicit <Value> elements also force UseEnumValue=Yes at compile time,
|
|
740
|
+
// so we suppress them when UseEnumValue=No.
|
|
741
|
+
const useEnumValue = (properties?.isExtensible || properties?.useEnumValue === false)
|
|
742
|
+
? 'No'
|
|
743
|
+
: (properties?.useEnumValue ? 'Yes' : 'No');
|
|
744
|
+
const suppressExplicitValues = useEnumValue === 'No';
|
|
745
|
+
const configKeyXml = properties?.configurationKey
|
|
746
|
+
? `\t<ConfigurationKey>${properties.configurationKey}</ConfigurationKey>\n`
|
|
747
|
+
: '';
|
|
748
|
+
// Build <EnumValues> block from properties.enumValues array
|
|
749
|
+
// Each entry: { name: string; value?: number; label?: string; helpText?: string }
|
|
750
|
+
const enumValueSpecs = Array.isArray(properties?.enumValues) ? properties.enumValues : [];
|
|
751
|
+
// D365FO hard limit: max 251 elements (0–250). Warn early — compiler rejects beyond this.
|
|
752
|
+
if (enumValueSpecs.length > 251) {
|
|
753
|
+
throw new Error(`Enum '${enumName}' has ${enumValueSpecs.length} values but D365FO supports a maximum of 251 (0–250). ` +
|
|
754
|
+
`Consider redesigning as a class hierarchy or splitting into multiple enums.`);
|
|
755
|
+
}
|
|
756
|
+
let enumValuesXml;
|
|
757
|
+
if (enumValueSpecs.length === 0) {
|
|
758
|
+
enumValuesXml = '\t<EnumValues />\n';
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
enumValuesXml = '\t<EnumValues>\n';
|
|
762
|
+
let autoValue = 0;
|
|
763
|
+
for (const v of enumValueSpecs) {
|
|
764
|
+
const intValue = v.value ?? autoValue;
|
|
765
|
+
autoValue = intValue + 1;
|
|
766
|
+
enumValuesXml += `\t\t<AxEnumValue>\n`;
|
|
767
|
+
enumValuesXml += `\t\t\t<Name>${v.name}</Name>\n`;
|
|
768
|
+
if (v.label)
|
|
769
|
+
enumValuesXml += `\t\t\t<Label>${v.label}</Label>\n`;
|
|
770
|
+
if (v.helpText)
|
|
771
|
+
enumValuesXml += `\t\t\t<HelpText>${v.helpText}</HelpText>\n`;
|
|
772
|
+
// Omit <Value> when UseEnumValue=No (position-based ordering) or for implicit 0
|
|
773
|
+
if (intValue !== 0 && !suppressExplicitValues)
|
|
774
|
+
enumValuesXml += `\t\t\t<Value>${intValue}</Value>\n`;
|
|
775
|
+
enumValuesXml += `\t\t</AxEnumValue>\n`;
|
|
776
|
+
}
|
|
777
|
+
enumValuesXml += '\t</EnumValues>\n';
|
|
778
|
+
}
|
|
779
|
+
// IsExtensible goes after EnumValues; value is lowercase true/false
|
|
780
|
+
const isExtensibleXml = properties?.isExtensible ? '\t<IsExtensible>true</IsExtensible>\n' : '';
|
|
781
|
+
// Element order matches real D365FO: Name → ConfigurationKey → Label → UseEnumValue → EnumValues → IsExtensible
|
|
782
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
783
|
+
<AxEnum xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
784
|
+
\t<Name>${enumName}</Name>
|
|
785
|
+
${configKeyXml}\t<Label>${label}</Label>
|
|
786
|
+
\t<UseEnumValue>${useEnumValue}</UseEnumValue>
|
|
787
|
+
${enumValuesXml}${isExtensibleXml}</AxEnum>
|
|
788
|
+
`;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Generate AxForm XML for a new form from a pattern template.
|
|
792
|
+
*
|
|
793
|
+
* Delegates to the pattern-compliant {@link FormPatternTemplates} builders so
|
|
794
|
+
* the generated skeleton actually satisfies the form-pattern gate. The old
|
|
795
|
+
* inline skeleton declared a `<Pattern>` over empty `<Controls />`, which the
|
|
796
|
+
* gate rejected as FP003 (required Grid/ActionPane missing) for every pattern
|
|
797
|
+
* — and worse, defaulted to a `DetailsTransaction` pattern even when the
|
|
798
|
+
* caller asked for a `SimpleList` template, guaranteeing a mismatch block.
|
|
799
|
+
*
|
|
800
|
+
* Pattern resolution: callers may express the intent as either `pattern`
|
|
801
|
+
* (the design pattern) or `formTemplate` (the VS template name); both are
|
|
802
|
+
* fuzzy strings normalized to a canonical pattern. When neither is given we
|
|
803
|
+
* default to SimpleList — the most common shape for a new setup table.
|
|
804
|
+
*/
|
|
805
|
+
static generateAxFormXml(formName, properties) {
|
|
806
|
+
const rawPattern = properties?.pattern || properties?.formTemplate;
|
|
807
|
+
const pattern = rawPattern
|
|
808
|
+
? FormPatternTemplates.normalizePattern(String(rawPattern))
|
|
809
|
+
: 'SimpleList';
|
|
810
|
+
return FormPatternTemplates.build(pattern, {
|
|
811
|
+
formName,
|
|
812
|
+
dsName: properties?.dataSource || undefined,
|
|
813
|
+
dsTable: properties?.dataSourceTable || properties?.dataSource || undefined,
|
|
814
|
+
caption: properties?.caption,
|
|
815
|
+
gridFields: Array.isArray(properties?.gridFields) ? properties.gridFields : undefined,
|
|
816
|
+
linesDsName: properties?.linesDataSource,
|
|
817
|
+
linesDsTable: properties?.linesDataSourceTable || properties?.linesDataSource,
|
|
818
|
+
sections: Array.isArray(properties?.sections) ? properties.sections : undefined,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Generate AxQuery XML structure. Delegates to the shared builder so this
|
|
823
|
+
* cannot drift from generateD365Xml.ts's copy — see queryViewXml.ts for the
|
|
824
|
+
* property contract and why `dataSource` matters.
|
|
825
|
+
*/
|
|
826
|
+
static generateAxQueryXml(queryName, properties) {
|
|
827
|
+
return buildAxQueryXml(queryName, properties);
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Generate AxView XML structure. Delegates to the shared builder so this
|
|
831
|
+
* cannot drift from generateD365Xml.ts's copy — see queryViewXml.ts for the
|
|
832
|
+
* property contract and why `query`/`fields` matter.
|
|
833
|
+
*/
|
|
834
|
+
static generateAxViewXml(viewName, properties) {
|
|
835
|
+
return buildAxViewXml(viewName, properties);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Generate AxMap XML structure. Delegates to the shared builder so this
|
|
839
|
+
* cannot drift from generateD365Xml.ts's copy — see mapXml.ts for the
|
|
840
|
+
* property contract.
|
|
841
|
+
*/
|
|
842
|
+
static generateAxMapXml(mapName, properties) {
|
|
843
|
+
return buildAxMapXml(mapName, properties);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Generate AxDataEntityView XML structure
|
|
847
|
+
*/
|
|
848
|
+
/**
|
|
849
|
+
* Generate AxDataEntityView XML. Delegates to the shared builder so this
|
|
850
|
+
* cannot drift from generateD365Xml.ts's copy (they already had — see
|
|
851
|
+
* dataEntityXml.ts for the property contract and why primaryTable/fields
|
|
852
|
+
* matter).
|
|
853
|
+
*/
|
|
854
|
+
static generateAxDataEntityXml(entityName, properties) {
|
|
855
|
+
return buildAxDataEntityXml(entityName, properties);
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Generate AxReport XML skeleton.
|
|
859
|
+
*
|
|
860
|
+
* properties:
|
|
861
|
+
* dpClassName - Data Provider class name (default: <ReportName>DP)
|
|
862
|
+
* tmpTableName - TempDB table name (default: <ReportName>Tmp)
|
|
863
|
+
* datasetName - AxReportDataSet name (default: tmpTableName)
|
|
864
|
+
* designName - AxReportDesign name (default: 'Report')
|
|
865
|
+
* caption - Design caption label ref (e.g. '@MyModel:MyLabel')
|
|
866
|
+
* style - Design style template (e.g. 'TableStyleTemplate')
|
|
867
|
+
* aotQuery - AOT query name for DynamicParameter (e.g. 'SalesTable')
|
|
868
|
+
* fields - Array of { name, alias?, dataType?, caption?, disableAutoCreate? } → AxReportDataSetField
|
|
869
|
+
* datasets - Array of { name, dpClassName, tmpTableName, fields?, aotQuery?, contractParams? } for multi-dataset reports
|
|
870
|
+
* contractParams - Array of { name, dataType?, label?, defaultValue? } → contract class parameters (DataMember)
|
|
871
|
+
* rdlContent - Full RDL XML string to embed (auto-generated from fields when omitted)
|
|
872
|
+
*
|
|
873
|
+
* AOT structure generated (mirrors real D365FO reports like ContosoReports_CashOrder_CZ):
|
|
874
|
+
* <AxReport xmlns="Microsoft.Dynamics.AX.Metadata.V2">
|
|
875
|
+
* <DataMethods />
|
|
876
|
+
* <DataSets>
|
|
877
|
+
* <AxReportDataSet xmlns=""> ← one per dataset
|
|
878
|
+
* <Fields>…</Fields>
|
|
879
|
+
* <Parameters> ← 6 AX system params + {DPCLASS}_DynamicParameter
|
|
880
|
+
* </AxReportDataSet>
|
|
881
|
+
* </DataSets>
|
|
882
|
+
* <DefaultParameterGroup> ← 6 AX params + DynamicParameter (with AOTQuery+DataType)
|
|
883
|
+
* <Designs>
|
|
884
|
+
* <AxReportDesign xmlns="" i:type="AxReportPrecisionDesign">
|
|
885
|
+
* <Text><![CDATA[…RDL…]]></Text> ← 2016 schema with DataSources/DataSets/ReportParameters
|
|
886
|
+
* <DisableIndividualTransformation><Name>…</Name></DisableIndividualTransformation>
|
|
887
|
+
* </Designs>
|
|
888
|
+
* </AxReport>
|
|
889
|
+
*/
|
|
890
|
+
static generateAxReportXml(reportName, properties) {
|
|
891
|
+
// ── Resolve datasets (multi-dataset array OR single-dataset shorthand) ──
|
|
892
|
+
let datasets;
|
|
893
|
+
if (properties?.datasets && Array.isArray(properties.datasets)) {
|
|
894
|
+
datasets = properties.datasets;
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
const tmpTableName = properties?.tmpTableName || `${reportName}Tmp`;
|
|
898
|
+
const dpClassName = properties?.dpClassName || `${reportName}DP`;
|
|
899
|
+
const datasetName = properties?.datasetName || tmpTableName;
|
|
900
|
+
datasets = [{
|
|
901
|
+
name: datasetName,
|
|
902
|
+
dpClassName,
|
|
903
|
+
tmpTableName,
|
|
904
|
+
fields: properties?.fields,
|
|
905
|
+
aotQuery: properties?.aotQuery,
|
|
906
|
+
contractParams: properties?.contractParams,
|
|
907
|
+
}];
|
|
908
|
+
}
|
|
909
|
+
const designName = properties?.designName || 'Report';
|
|
910
|
+
// ── RDL .NET type mapping ──
|
|
911
|
+
const rdlType = (dt) => {
|
|
912
|
+
switch (dt) {
|
|
913
|
+
case 'System.Double': return 'System.Double';
|
|
914
|
+
case 'System.Int32': return 'System.Int32';
|
|
915
|
+
case 'System.Int64': return 'System.Int64';
|
|
916
|
+
case 'System.DateTime': return 'System.DateTime';
|
|
917
|
+
case 'System.Byte[]': return 'System.Byte[]';
|
|
918
|
+
default: return 'System.String';
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
// ── UUID helper — use Node.js crypto for guaranteed RFC-4122 v4 format ──
|
|
922
|
+
const uuid = () => crypto.randomUUID();
|
|
923
|
+
// ── Build one AxReportDataSet XML entry ──
|
|
924
|
+
const buildDatasetXml = (ds) => {
|
|
925
|
+
const dpParamName = `${ds.dpClassName.toUpperCase()}_DynamicParameter`;
|
|
926
|
+
const contractDatasetParamsXml = (ds.contractParams || []).map(cp => {
|
|
927
|
+
const pn = `${ds.name}_ds_${cp.name}`;
|
|
928
|
+
const dt = cp.dataType || 'System.String';
|
|
929
|
+
return `\t\t\t\t<AxReportDataSetParameter>\n\t\t\t\t\t<Name>${pn}</Name>\n\t\t\t\t\t<Alias>${pn}</Alias>\n\t\t\t\t\t<DataType>${dt}</DataType>\n\t\t\t\t\t<Parameter>${pn}</Parameter>\n\t\t\t\t</AxReportDataSetParameter>`;
|
|
930
|
+
}).join('\n');
|
|
931
|
+
let fieldsXml;
|
|
932
|
+
if (ds.fields && ds.fields.length > 0) {
|
|
933
|
+
const entries = ds.fields.map(f => {
|
|
934
|
+
const alias = f.alias || `${ds.tmpTableName}.1.${f.name}`;
|
|
935
|
+
const capLine = f.caption ? `\n\t\t\t\t<Caption>${f.caption}</Caption>` : '';
|
|
936
|
+
const dtLine = f.dataType ? `\n\t\t\t\t<DataType>${f.dataType}</DataType>` : '';
|
|
937
|
+
const disableLine = f.disableAutoCreate ? `\n\t\t\t\t<DisableAutoCreateInDataRegion>true</DisableAutoCreateInDataRegion>` : '';
|
|
938
|
+
return [
|
|
939
|
+
`\t\t\t<AxReportDataSetField>`,
|
|
940
|
+
`\t\t\t\t<Name>${f.name}</Name>`,
|
|
941
|
+
`\t\t\t\t<Alias>${alias}</Alias>${capLine}${dtLine}${disableLine}`,
|
|
942
|
+
`\t\t\t\t<DisplayWidth>Auto</DisplayWidth>`,
|
|
943
|
+
`\t\t\t\t<UserDefined>false</UserDefined>`,
|
|
944
|
+
`\t\t\t</AxReportDataSetField>`,
|
|
945
|
+
].join('\n');
|
|
946
|
+
});
|
|
947
|
+
fieldsXml = `\t\t\t<Fields>\n${entries.join('\n')}\n\t\t\t</Fields>`;
|
|
948
|
+
}
|
|
949
|
+
else {
|
|
950
|
+
// ⚠ No field definitions provided — dataset will have no columns visible in the
|
|
951
|
+
// D365FO Report Designer. Caller MUST pass a `fields` array listing all TmpTable
|
|
952
|
+
// columns that the report should expose (name, alias, dataType). Without this the
|
|
953
|
+
// designer shows an empty dataset and the RDL tablix has no fields to bind to.
|
|
954
|
+
fieldsXml = `\t\t\t<Fields />`;
|
|
955
|
+
}
|
|
956
|
+
return `\t\t<AxReportDataSet xmlns="">
|
|
957
|
+
\t\t\t<Name>${ds.name}</Name>
|
|
958
|
+
\t\t\t<DataSourceType>ReportDataProvider</DataSourceType>
|
|
959
|
+
\t\t\t<Query>SELECT * FROM ${ds.dpClassName}.${ds.tmpTableName}</Query>
|
|
960
|
+
\t\t\t<FieldGroups />
|
|
961
|
+
${fieldsXml}
|
|
962
|
+
\t\t\t<Parameters>
|
|
963
|
+
\t\t\t\t<AxReportDataSetParameter>
|
|
964
|
+
\t\t\t\t\t<Name>AX_PartitionKey</Name>
|
|
965
|
+
\t\t\t\t\t<Alias>AX_PartitionKey</Alias>
|
|
966
|
+
\t\t\t\t\t<DataType>System.String</DataType>
|
|
967
|
+
\t\t\t\t\t<Parameter>AX_PartitionKey</Parameter>
|
|
968
|
+
\t\t\t\t</AxReportDataSetParameter>
|
|
969
|
+
\t\t\t\t<AxReportDataSetParameter>
|
|
970
|
+
\t\t\t\t\t<Name>AX_CompanyName</Name>
|
|
971
|
+
\t\t\t\t\t<Alias>AX_CompanyName</Alias>
|
|
972
|
+
\t\t\t\t\t<DataType>System.String</DataType>
|
|
973
|
+
\t\t\t\t\t<Parameter>AX_CompanyName</Parameter>
|
|
974
|
+
\t\t\t\t</AxReportDataSetParameter>
|
|
975
|
+
\t\t\t\t<AxReportDataSetParameter>
|
|
976
|
+
\t\t\t\t\t<Name>AX_UserContext</Name>
|
|
977
|
+
\t\t\t\t\t<Alias>AX_UserContext</Alias>
|
|
978
|
+
\t\t\t\t\t<DataType>System.String</DataType>
|
|
979
|
+
\t\t\t\t\t<Parameter>AX_UserContext</Parameter>
|
|
980
|
+
\t\t\t\t</AxReportDataSetParameter>
|
|
981
|
+
\t\t\t\t<AxReportDataSetParameter>
|
|
982
|
+
\t\t\t\t\t<Name>AX_RenderingCulture</Name>
|
|
983
|
+
\t\t\t\t\t<Alias>AX_RenderingCulture</Alias>
|
|
984
|
+
\t\t\t\t\t<DataType>System.String</DataType>
|
|
985
|
+
\t\t\t\t\t<Parameter>AX_RenderingCulture</Parameter>
|
|
986
|
+
\t\t\t\t</AxReportDataSetParameter>
|
|
987
|
+
\t\t\t\t<AxReportDataSetParameter>
|
|
988
|
+
\t\t\t\t\t<Name>AX_ReportContext</Name>
|
|
989
|
+
\t\t\t\t\t<Alias>AX_ReportContext</Alias>
|
|
990
|
+
\t\t\t\t\t<DataType>System.String</DataType>
|
|
991
|
+
\t\t\t\t\t<Parameter>AX_ReportContext</Parameter>
|
|
992
|
+
\t\t\t\t</AxReportDataSetParameter>
|
|
993
|
+
\t\t\t\t<AxReportDataSetParameter>
|
|
994
|
+
\t\t\t\t\t<Name>AX_RdpPreProcessedId</Name>
|
|
995
|
+
\t\t\t\t\t<Alias>AX_RdpPreProcessedId</Alias>
|
|
996
|
+
\t\t\t\t\t<DataType>System.String</DataType>
|
|
997
|
+
\t\t\t\t\t<Parameter>AX_RdpPreProcessedId</Parameter>
|
|
998
|
+
\t\t\t\t</AxReportDataSetParameter>
|
|
999
|
+
${contractDatasetParamsXml ? contractDatasetParamsXml + '\n' : ''}\t\t\t\t<AxReportDataSetParameter>
|
|
1000
|
+
\t\t\t\t\t<Name>${dpParamName}</Name>
|
|
1001
|
+
\t\t\t\t\t<Alias>${dpParamName}</Alias>
|
|
1002
|
+
\t\t\t\t\t<DataType>Microsoft.Dynamics.AX.Framework.Services.Client.QueryMetadata</DataType>
|
|
1003
|
+
\t\t\t\t\t<Parameter>${dpParamName}</Parameter>
|
|
1004
|
+
\t\t\t\t</AxReportDataSetParameter>
|
|
1005
|
+
\t\t\t</Parameters>
|
|
1006
|
+
\t\t</AxReportDataSet>`;
|
|
1007
|
+
};
|
|
1008
|
+
const datasetsXml = datasets.map(buildDatasetXml).join('\n');
|
|
1009
|
+
// ── DefaultParameterGroup (uses first dataset's DP for DynamicParameter) ──
|
|
1010
|
+
const firstDs = datasets[0];
|
|
1011
|
+
const dpParamName = `${firstDs.dpClassName.toUpperCase()}_DynamicParameter`;
|
|
1012
|
+
const aotQueryLine = firstDs.aotQuery ? `\n\t\t\t\t<AOTQuery>${firstDs.aotQuery}</AOTQuery>` : '';
|
|
1013
|
+
// Contract parameters (from DataContract class with [DataMember] attributes)
|
|
1014
|
+
const contractParamsXml = (firstDs.contractParams || []).map(cp => {
|
|
1015
|
+
const dataTypeLine = cp.dataType ? `\n\t\t\t\t<DataType>${cp.dataType}</DataType>` : '';
|
|
1016
|
+
const promptLine = cp.label ? `\n\t\t\t\t<PromptString>${cp.label}</PromptString>` : '';
|
|
1017
|
+
return `\t\t\t<AxReportParameterBase xmlns=""
|
|
1018
|
+
\t\t\t\t\ti:type="AxReportParameter">
|
|
1019
|
+
\t\t\t\t<Name>${firstDs.name}_ds_${cp.name}</Name>${dataTypeLine}${promptLine}
|
|
1020
|
+
\t\t\t\t<DefaultValue />
|
|
1021
|
+
\t\t\t\t<Values />
|
|
1022
|
+
\t\t\t</AxReportParameterBase>`;
|
|
1023
|
+
}).join('\n');
|
|
1024
|
+
const defaultParamGroupXml = `\t<DefaultParameterGroup>
|
|
1025
|
+
\t\t<Name xmlns="">Parameters</Name>
|
|
1026
|
+
\t\t<ReportParameterBases xmlns="">
|
|
1027
|
+
\t\t\t<AxReportParameterBase xmlns=""
|
|
1028
|
+
\t\t\t\t\ti:type="AxReportParameter">
|
|
1029
|
+
\t\t\t\t<Name>AX_PartitionKey</Name>
|
|
1030
|
+
\t\t\t\t<AllowBlank>true</AllowBlank>
|
|
1031
|
+
\t\t\t\t<Nullable>true</Nullable>
|
|
1032
|
+
\t\t\t\t<UserVisibility>Hidden</UserVisibility>
|
|
1033
|
+
\t\t\t\t<DefaultValue />
|
|
1034
|
+
\t\t\t\t<Values />
|
|
1035
|
+
\t\t\t</AxReportParameterBase>
|
|
1036
|
+
\t\t\t<AxReportParameterBase xmlns=""
|
|
1037
|
+
\t\t\t\t\ti:type="AxReportParameter">
|
|
1038
|
+
\t\t\t\t<Name>AX_CompanyName</Name>
|
|
1039
|
+
\t\t\t\t<UserVisibility>Hidden</UserVisibility>
|
|
1040
|
+
\t\t\t\t<DefaultValue />
|
|
1041
|
+
\t\t\t\t<Values />
|
|
1042
|
+
\t\t\t</AxReportParameterBase>
|
|
1043
|
+
\t\t\t<AxReportParameterBase xmlns=""
|
|
1044
|
+
\t\t\t\t\ti:type="AxReportParameter">
|
|
1045
|
+
\t\t\t\t<Name>AX_UserContext</Name>
|
|
1046
|
+
\t\t\t\t<AllowBlank>true</AllowBlank>
|
|
1047
|
+
\t\t\t\t<Nullable>true</Nullable>
|
|
1048
|
+
\t\t\t\t<UserVisibility>Hidden</UserVisibility>
|
|
1049
|
+
\t\t\t\t<DefaultValue />
|
|
1050
|
+
\t\t\t\t<Values />
|
|
1051
|
+
\t\t\t</AxReportParameterBase>
|
|
1052
|
+
\t\t\t<AxReportParameterBase xmlns=""
|
|
1053
|
+
\t\t\t\t\ti:type="AxReportParameter">
|
|
1054
|
+
\t\t\t\t<Name>AX_RenderingCulture</Name>
|
|
1055
|
+
\t\t\t\t<AllowBlank>true</AllowBlank>
|
|
1056
|
+
\t\t\t\t<Nullable>true</Nullable>
|
|
1057
|
+
\t\t\t\t<UserVisibility>Hidden</UserVisibility>
|
|
1058
|
+
\t\t\t\t<DefaultValue />
|
|
1059
|
+
\t\t\t\t<Values />
|
|
1060
|
+
\t\t\t</AxReportParameterBase>
|
|
1061
|
+
\t\t\t<AxReportParameterBase xmlns=""
|
|
1062
|
+
\t\t\t\t\ti:type="AxReportParameter">
|
|
1063
|
+
\t\t\t\t<Name>AX_ReportContext</Name>
|
|
1064
|
+
\t\t\t\t<AllowBlank>true</AllowBlank>
|
|
1065
|
+
\t\t\t\t<Nullable>true</Nullable>
|
|
1066
|
+
\t\t\t\t<UserVisibility>Hidden</UserVisibility>
|
|
1067
|
+
\t\t\t\t<DefaultValue />
|
|
1068
|
+
\t\t\t\t<Values />
|
|
1069
|
+
\t\t\t</AxReportParameterBase>
|
|
1070
|
+
\t\t\t<AxReportParameterBase xmlns=""
|
|
1071
|
+
\t\t\t\t\ti:type="AxReportParameter">
|
|
1072
|
+
\t\t\t\t<Name>AX_RdpPreProcessedId</Name>
|
|
1073
|
+
\t\t\t\t<AllowBlank>true</AllowBlank>
|
|
1074
|
+
\t\t\t\t<Nullable>true</Nullable>
|
|
1075
|
+
\t\t\t\t<UserVisibility>Hidden</UserVisibility>
|
|
1076
|
+
\t\t\t\t<DefaultValue />
|
|
1077
|
+
\t\t\t\t<Values />
|
|
1078
|
+
\t\t\t</AxReportParameterBase>
|
|
1079
|
+
${contractParamsXml}${contractParamsXml ? '\n' : ''}\t\t\t<AxReportParameterBase xmlns=""
|
|
1080
|
+
\t\t\t\t\ti:type="AxReportParameter">
|
|
1081
|
+
\t\t\t\t<Name>${dpParamName}</Name>${aotQueryLine}
|
|
1082
|
+
\t\t\t\t<AllowBlank>true</AllowBlank>
|
|
1083
|
+
\t\t\t\t<DataType>Microsoft.Dynamics.AX.Framework.Services.Client.QueryMetadata</DataType>
|
|
1084
|
+
\t\t\t\t<Nullable>true</Nullable>
|
|
1085
|
+
\t\t\t\t<UserVisibility>Hidden</UserVisibility>
|
|
1086
|
+
\t\t\t\t<DefaultValue />
|
|
1087
|
+
\t\t\t\t<Values />
|
|
1088
|
+
\t\t\t</AxReportParameterBase>
|
|
1089
|
+
\t\t</ReportParameterBases>
|
|
1090
|
+
\t</DefaultParameterGroup>`;
|
|
1091
|
+
// ── Auto-generate RDL skeleton (2016 namespace, mirrors real D365FO reports) ──
|
|
1092
|
+
const buildRdlSkeleton = () => {
|
|
1093
|
+
const ns2016 = 'http://schemas.microsoft.com/sqlserver/reporting/2016/01/reportdefinition';
|
|
1094
|
+
const nsRd = 'http://schemas.microsoft.com/SQLServer/reporting/reportdesigner';
|
|
1095
|
+
// DataSources block — single shared AX data source
|
|
1096
|
+
const rdlDataSourcesXml = ` <DataSources>
|
|
1097
|
+
<DataSource Name="AutoGen__ReportDataProvider">
|
|
1098
|
+
<Transaction>true</Transaction>
|
|
1099
|
+
<ConnectionProperties>
|
|
1100
|
+
<DataProvider>AXREPORTDATAPROVIDER</DataProvider>
|
|
1101
|
+
<ConnectString />
|
|
1102
|
+
<IntegratedSecurity>true</IntegratedSecurity>
|
|
1103
|
+
</ConnectionProperties>
|
|
1104
|
+
<rd:DataSourceID>${uuid()}</rd:DataSourceID>
|
|
1105
|
+
</DataSource>
|
|
1106
|
+
</DataSources>`;
|
|
1107
|
+
// Build one RDL DataSet per AxReportDataSet
|
|
1108
|
+
const buildRdlDataset = (ds) => {
|
|
1109
|
+
const dsDpParam = `${ds.dpClassName.toUpperCase()}_DynamicParameter`;
|
|
1110
|
+
const contractParamNamesRdl = (ds.contractParams || []).map(cp => `${ds.name}_ds_${cp.name}`);
|
|
1111
|
+
const paramNames = [
|
|
1112
|
+
'AX_PartitionKey', 'AX_CompanyName', 'AX_UserContext',
|
|
1113
|
+
'AX_RenderingCulture', 'AX_ReportContext', 'AX_RdpPreProcessedId',
|
|
1114
|
+
...contractParamNamesRdl,
|
|
1115
|
+
dsDpParam,
|
|
1116
|
+
];
|
|
1117
|
+
const queryParams = paramNames
|
|
1118
|
+
.map(p => ` <QueryParameter Name="${p}">\n <Value>=Parameters!${p}.Value</Value>\n </QueryParameter>`)
|
|
1119
|
+
.join('\n');
|
|
1120
|
+
let rdlFields = '';
|
|
1121
|
+
if (ds.fields && ds.fields.length > 0) {
|
|
1122
|
+
const flines = ds.fields.map(f => {
|
|
1123
|
+
const alias = f.alias || `${ds.tmpTableName}.1.${f.name}`;
|
|
1124
|
+
return ` <Field Name="${f.name}">\n <DataField>${alias}</DataField>\n <rd:TypeName>${rdlType(f.dataType)}</rd:TypeName>\n </Field>`;
|
|
1125
|
+
});
|
|
1126
|
+
rdlFields = ` <Fields>\n${flines.join('\n')}\n </Fields>\n`;
|
|
1127
|
+
}
|
|
1128
|
+
return ` <DataSet Name="${ds.name}">
|
|
1129
|
+
<rd:DataSetID>${uuid()}</rd:DataSetID>
|
|
1130
|
+
<Query>
|
|
1131
|
+
<DataSourceName>AutoGen__ReportDataProvider</DataSourceName>
|
|
1132
|
+
<QueryParameters>
|
|
1133
|
+
${queryParams}
|
|
1134
|
+
</QueryParameters>
|
|
1135
|
+
<CommandText>SELECT * FROM ${ds.dpClassName}.${ds.tmpTableName}</CommandText>
|
|
1136
|
+
<rd:UseGenericDesigner>true</rd:UseGenericDesigner>
|
|
1137
|
+
</Query>
|
|
1138
|
+
${rdlFields} <rd:DataSetInfo>
|
|
1139
|
+
<rd:DataSetName>${ds.name}</rd:DataSetName>
|
|
1140
|
+
<rd:TableName>Fields</rd:TableName>
|
|
1141
|
+
<rd:TableAdapterFillMethod>Fill</rd:TableAdapterFillMethod>
|
|
1142
|
+
<rd:TableAdapterGetDataMethod>GetData</rd:TableAdapterGetDataMethod>
|
|
1143
|
+
<rd:TableAdapterName>FieldsTableAdapter</rd:TableAdapterName>
|
|
1144
|
+
</rd:DataSetInfo>
|
|
1145
|
+
</DataSet>`;
|
|
1146
|
+
};
|
|
1147
|
+
const rdlDatasetsXml = ` <DataSets>\n${datasets.map(buildRdlDataset).join('\n')}\n </DataSets>`;
|
|
1148
|
+
// ── Build a simple detail tablix for each dataset so the design is not empty ──
|
|
1149
|
+
const buildRdlTablix = (ds) => {
|
|
1150
|
+
if (!ds.fields || ds.fields.length === 0)
|
|
1151
|
+
return '';
|
|
1152
|
+
const n = ds.fields.length;
|
|
1153
|
+
const colW = +Math.min(1.5, 7 / n).toFixed(2);
|
|
1154
|
+
const totalW = +(colW * n).toFixed(2);
|
|
1155
|
+
const grp = `Details_${ds.name}`;
|
|
1156
|
+
const cols = ds.fields.map(() => ` <TablixColumn><Width>${colW}in</Width></TablixColumn>`).join('\n');
|
|
1157
|
+
const hCells = ds.fields.map(f => [
|
|
1158
|
+
` <TablixCell><CellContents>`,
|
|
1159
|
+
` <Textbox Name="Textbox_${f.name}_H">`,
|
|
1160
|
+
` <CanGrow>true</CanGrow><Value>${f.name}</Value>`,
|
|
1161
|
+
` <Style><FontWeight>Bold</FontWeight>`,
|
|
1162
|
+
` <BackgroundColor>LightGrey</BackgroundColor>`,
|
|
1163
|
+
` <Border><Style>Solid</Style></Border>`,
|
|
1164
|
+
` <PaddingLeft>2pt</PaddingLeft><PaddingRight>2pt</PaddingRight>`,
|
|
1165
|
+
` <PaddingTop>2pt</PaddingTop><PaddingBottom>2pt</PaddingBottom>`,
|
|
1166
|
+
` </Style></Textbox>`,
|
|
1167
|
+
` </CellContents></TablixCell>`,
|
|
1168
|
+
].join('\n')).join('\n');
|
|
1169
|
+
const dCells = ds.fields.map(f => [
|
|
1170
|
+
` <TablixCell><CellContents>`,
|
|
1171
|
+
` <Textbox Name="Textbox_${f.name}">`,
|
|
1172
|
+
` <CanGrow>true</CanGrow><Value>=Fields!${f.name}.Value</Value>`,
|
|
1173
|
+
` <Style><Border><Style>Solid</Style></Border>`,
|
|
1174
|
+
` <PaddingLeft>2pt</PaddingLeft><PaddingRight>2pt</PaddingRight>`,
|
|
1175
|
+
` <PaddingTop>2pt</PaddingTop><PaddingBottom>2pt</PaddingBottom>`,
|
|
1176
|
+
` </Style></Textbox>`,
|
|
1177
|
+
` </CellContents></TablixCell>`,
|
|
1178
|
+
].join('\n')).join('\n');
|
|
1179
|
+
const cMembers = ds.fields.map(() => ` <TablixMember />`).join('\n');
|
|
1180
|
+
return [
|
|
1181
|
+
` <Tablix Name="Tablix_${ds.name}">`,
|
|
1182
|
+
` <TablixBody>`,
|
|
1183
|
+
` <TablixColumns>`,
|
|
1184
|
+
cols,
|
|
1185
|
+
` </TablixColumns>`,
|
|
1186
|
+
` <TablixRows>`,
|
|
1187
|
+
` <TablixRow><Height>0.25in</Height><TablixCells>`,
|
|
1188
|
+
hCells,
|
|
1189
|
+
` </TablixCells></TablixRow>`,
|
|
1190
|
+
` <TablixRow><Height>0.25in</Height><TablixCells>`,
|
|
1191
|
+
dCells,
|
|
1192
|
+
` </TablixCells></TablixRow>`,
|
|
1193
|
+
` </TablixRows>`,
|
|
1194
|
+
` </TablixBody>`,
|
|
1195
|
+
` <TablixColumnHierarchy><TablixMembers>`,
|
|
1196
|
+
cMembers,
|
|
1197
|
+
` </TablixMembers></TablixColumnHierarchy>`,
|
|
1198
|
+
` <TablixRowHierarchy><TablixMembers>`,
|
|
1199
|
+
` <TablixMember>`,
|
|
1200
|
+
` <KeepWithGroup>After</KeepWithGroup>`,
|
|
1201
|
+
` <RepeatOnNewPage>true</RepeatOnNewPage>`,
|
|
1202
|
+
` </TablixMember>`,
|
|
1203
|
+
` <TablixMember><Group Name="${grp}"><DataGroupName>${grp}</DataGroupName></Group></TablixMember>`,
|
|
1204
|
+
` </TablixMembers></TablixRowHierarchy>`,
|
|
1205
|
+
` <DataSetName>${ds.name}</DataSetName>`,
|
|
1206
|
+
` <Top>0.5in</Top><Left>0.5in</Left>`,
|
|
1207
|
+
` <Height>0.5in</Height><Width>${totalW}in</Width>`,
|
|
1208
|
+
` <Style><Border><Style>Solid</Style></Border></Style>`,
|
|
1209
|
+
` </Tablix>`,
|
|
1210
|
+
].join('\n');
|
|
1211
|
+
};
|
|
1212
|
+
const rdlBodyItemsXml = datasets.map(buildRdlTablix).filter(Boolean).join('\n');
|
|
1213
|
+
const rdlBodyTag = rdlBodyItemsXml
|
|
1214
|
+
? ` <ReportItems>\n${rdlBodyItemsXml}\n </ReportItems>`
|
|
1215
|
+
: ` <ReportItems />`;
|
|
1216
|
+
// ReportParameters — 6 AX system params + DynamicParameter + contract params (all hidden)
|
|
1217
|
+
const contractRdlParams = (firstDs.contractParams || []).map(cp => ({
|
|
1218
|
+
name: `${firstDs.name}_ds_${cp.name}`,
|
|
1219
|
+
nullable: true, blank: true, usedInQuery: true,
|
|
1220
|
+
}));
|
|
1221
|
+
const rdlParamDefs = [
|
|
1222
|
+
{ name: 'AX_PartitionKey', nullable: true, blank: true, usedInQuery: false },
|
|
1223
|
+
{ name: 'AX_CompanyName', nullable: false, blank: false, usedInQuery: false },
|
|
1224
|
+
{ name: 'AX_UserContext', nullable: true, blank: true, usedInQuery: false },
|
|
1225
|
+
{ name: 'AX_RenderingCulture', nullable: true, blank: true, usedInQuery: false },
|
|
1226
|
+
{ name: 'AX_ReportContext', nullable: true, blank: true, usedInQuery: true },
|
|
1227
|
+
{ name: 'AX_RdpPreProcessedId', nullable: true, blank: true, usedInQuery: false },
|
|
1228
|
+
{ name: dpParamName, nullable: true, blank: true, usedInQuery: false },
|
|
1229
|
+
...contractRdlParams,
|
|
1230
|
+
];
|
|
1231
|
+
const rdlParamsXml = ` <ReportParameters>\n` +
|
|
1232
|
+
rdlParamDefs.map(p => {
|
|
1233
|
+
const nullLine = p.nullable ? `\n <Nullable>true</Nullable>` : '';
|
|
1234
|
+
const blankLine = p.blank ? `\n <AllowBlank>true</AllowBlank>` : '';
|
|
1235
|
+
const usedLine = p.usedInQuery ? `\n <UsedInQuery>true</UsedInQuery>` : '';
|
|
1236
|
+
return ` <ReportParameter Name="${p.name}">\n <DataType>String</DataType>${nullLine}${blankLine}\n <Prompt>${p.name}</Prompt>\n <Hidden>true</Hidden>${usedLine}\n </ReportParameter>`;
|
|
1237
|
+
}).join('\n') + `\n </ReportParameters>`;
|
|
1238
|
+
// ReportParametersLayout
|
|
1239
|
+
const cellDefs = rdlParamDefs
|
|
1240
|
+
.map((p, i) => ` <CellDefinition>\n <ColumnIndex>${i}</ColumnIndex>\n <RowIndex>0</RowIndex>\n <ParameterName>${p.name}</ParameterName>\n </CellDefinition>`)
|
|
1241
|
+
.join('\n');
|
|
1242
|
+
const rdlParamLayoutXml = ` <ReportParametersLayout>
|
|
1243
|
+
<GridLayoutDefinition>
|
|
1244
|
+
<NumberOfColumns>${rdlParamDefs.length}</NumberOfColumns>
|
|
1245
|
+
<NumberOfRows>1</NumberOfRows>
|
|
1246
|
+
<CellDefinitions>
|
|
1247
|
+
${cellDefs}
|
|
1248
|
+
</CellDefinitions>
|
|
1249
|
+
</GridLayoutDefinition>
|
|
1250
|
+
</ReportParametersLayout>`;
|
|
1251
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
1252
|
+
<Report xmlns="${ns2016}" xmlns:rd="${nsRd}">
|
|
1253
|
+
<AutoRefresh>0</AutoRefresh>
|
|
1254
|
+
${rdlDataSourcesXml}
|
|
1255
|
+
${rdlDatasetsXml}
|
|
1256
|
+
<ReportSections>
|
|
1257
|
+
<ReportSection>
|
|
1258
|
+
<Body>
|
|
1259
|
+
${rdlBodyTag}
|
|
1260
|
+
<Height>1in</Height>
|
|
1261
|
+
<Style>
|
|
1262
|
+
<Border>
|
|
1263
|
+
<Style>None</Style>
|
|
1264
|
+
</Border>
|
|
1265
|
+
</Style>
|
|
1266
|
+
</Body>
|
|
1267
|
+
<Width>7.5in</Width>
|
|
1268
|
+
<Page>
|
|
1269
|
+
<PageHeight>11.69in</PageHeight>
|
|
1270
|
+
<PageWidth>8.27in</PageWidth>
|
|
1271
|
+
<InteractiveHeight>11in</InteractiveHeight>
|
|
1272
|
+
<InteractiveWidth>8.5in</InteractiveWidth>
|
|
1273
|
+
<LeftMargin>0.2in</LeftMargin>
|
|
1274
|
+
<TopMargin>0.2in</TopMargin>
|
|
1275
|
+
<Style />
|
|
1276
|
+
</Page>
|
|
1277
|
+
</ReportSection>
|
|
1278
|
+
</ReportSections>
|
|
1279
|
+
${rdlParamsXml}
|
|
1280
|
+
${rdlParamLayoutXml}
|
|
1281
|
+
<Language>en-US</Language>
|
|
1282
|
+
<rd:ReportUnitType>Inch</rd:ReportUnitType>
|
|
1283
|
+
<rd:ReportID>${uuid()}</rd:ReportID>
|
|
1284
|
+
</Report>`;
|
|
1285
|
+
};
|
|
1286
|
+
// ── Design block ──
|
|
1287
|
+
const captionLine = properties?.caption ? `\n\t\t\t<Caption>${properties.caption}</Caption>` : '';
|
|
1288
|
+
const styleLine = properties?.style ? `\n\t\t\t<Style>${properties.style}</Style>` : '';
|
|
1289
|
+
const rdlContent = properties?.rdlContent;
|
|
1290
|
+
// Sanitize: fix old-schema <Header> inside <TablixMember> — renamed to <TablixHeader> in 2016 RDL.
|
|
1291
|
+
// This handles AI-generated or older-tool-generated RDL that still uses the pre-2016 element name.
|
|
1292
|
+
const rdl = (rdlContent || buildRdlSkeleton())
|
|
1293
|
+
.replace(/<Header>/g, '<TablixHeader>')
|
|
1294
|
+
.replace(/<\/Header>/g, '</TablixHeader>');
|
|
1295
|
+
const textElement = `\n\t\t\t<Text><![CDATA[${rdl}]]></Text>`;
|
|
1296
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
1297
|
+
<AxReport xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="Microsoft.Dynamics.AX.Metadata.V2">
|
|
1298
|
+
\t<Name>${reportName}</Name>
|
|
1299
|
+
\t<DataMethods />
|
|
1300
|
+
\t<DataSets>
|
|
1301
|
+
${datasetsXml}
|
|
1302
|
+
\t</DataSets>
|
|
1303
|
+
${defaultParamGroupXml}
|
|
1304
|
+
\t<Designs>
|
|
1305
|
+
\t\t<AxReportDesign xmlns=""
|
|
1306
|
+
\t\t\t\ti:type="AxReportPrecisionDesign">
|
|
1307
|
+
\t\t\t<Name>${designName}</Name>${captionLine}${styleLine}${textElement}
|
|
1308
|
+
\t\t\t<DisableIndividualTransformation>
|
|
1309
|
+
\t\t\t\t<Name>DisableIndividualTransformation</Name>
|
|
1310
|
+
\t\t\t</DisableIndividualTransformation>
|
|
1311
|
+
\t\t</AxReportDesign>
|
|
1312
|
+
\t</Designs>
|
|
1313
|
+
\t<EmbeddedImages />
|
|
1314
|
+
</AxReport>`;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Generate XML based on object type
|
|
1318
|
+
*/
|
|
1319
|
+
static generate(objectType, objectName, sourceCode, properties) {
|
|
1320
|
+
switch (objectType) {
|
|
1321
|
+
case 'class':
|
|
1322
|
+
return this.generateAxClassXml(objectName, sourceCode, properties);
|
|
1323
|
+
case 'class-extension':
|
|
1324
|
+
return this.generateAxClassExtensionXml(objectName, sourceCode, properties);
|
|
1325
|
+
case 'table': {
|
|
1326
|
+
// sourceCode is not used for tables directly, but Copilot may pass field
|
|
1327
|
+
// definitions as a JSON string in sourceCode. Try to parse and merge into properties.
|
|
1328
|
+
let mergedProperties = properties;
|
|
1329
|
+
if (sourceCode && sourceCode.trim().startsWith('{')) {
|
|
1330
|
+
try {
|
|
1331
|
+
const parsed = JSON.parse(sourceCode);
|
|
1332
|
+
if (parsed && (Array.isArray(parsed.fields) || parsed.label || parsed.tableGroup)) {
|
|
1333
|
+
mergedProperties = { ...parsed, ...properties }; // explicit properties win
|
|
1334
|
+
console.error('[create_d365fo_file] Parsed table field definitions from sourceCode JSON');
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
catch {
|
|
1338
|
+
// Not valid JSON — ignore
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return this.generateAxTableXml(objectName, mergedProperties);
|
|
1342
|
+
}
|
|
1343
|
+
case 'enum':
|
|
1344
|
+
return this.generateAxEnumXml(objectName, properties);
|
|
1345
|
+
case 'form':
|
|
1346
|
+
return this.generateAxFormXml(objectName, properties);
|
|
1347
|
+
case 'query':
|
|
1348
|
+
return this.generateAxQueryXml(objectName, properties);
|
|
1349
|
+
case 'view':
|
|
1350
|
+
return this.generateAxViewXml(objectName, properties);
|
|
1351
|
+
case 'map':
|
|
1352
|
+
return this.generateAxMapXml(objectName, properties);
|
|
1353
|
+
case 'data-entity':
|
|
1354
|
+
return this.generateAxDataEntityXml(objectName, properties);
|
|
1355
|
+
case 'report':
|
|
1356
|
+
return this.generateAxReportXml(objectName, properties);
|
|
1357
|
+
case 'edt':
|
|
1358
|
+
return this.generateAxEdtXml(objectName, properties);
|
|
1359
|
+
case 'table-extension':
|
|
1360
|
+
return this.generateAxTableExtensionXml(objectName, properties);
|
|
1361
|
+
case 'form-extension':
|
|
1362
|
+
return this.generateAxFormExtensionXml(objectName);
|
|
1363
|
+
case 'edt-extension':
|
|
1364
|
+
return this.generateAxSimpleExtensionXml('AxEdtExtension', objectName);
|
|
1365
|
+
case 'enum-extension':
|
|
1366
|
+
return this.generateAxEnumExtensionXml(objectName, properties);
|
|
1367
|
+
case 'data-entity-extension':
|
|
1368
|
+
return this.generateAxSimpleExtensionXml('AxDataEntityViewExtension', objectName);
|
|
1369
|
+
case 'menu-item-display':
|
|
1370
|
+
case 'menu-item-action':
|
|
1371
|
+
case 'menu-item-output':
|
|
1372
|
+
return this.generateAxMenuItemXml(objectType, objectName, properties);
|
|
1373
|
+
case 'menu-item-display-extension':
|
|
1374
|
+
return this.generateAxSimpleExtensionXml('AxMenuItemDisplayExtension', objectName);
|
|
1375
|
+
case 'menu-item-action-extension':
|
|
1376
|
+
return this.generateAxSimpleExtensionXml('AxMenuItemActionExtension', objectName);
|
|
1377
|
+
case 'menu-item-output-extension':
|
|
1378
|
+
return this.generateAxSimpleExtensionXml('AxMenuItemOutputExtension', objectName);
|
|
1379
|
+
case 'menu':
|
|
1380
|
+
return this.generateAxMenuXml(objectName, properties);
|
|
1381
|
+
case 'menu-extension':
|
|
1382
|
+
return this.generateAxMenuExtensionXml(objectName);
|
|
1383
|
+
case 'security-privilege':
|
|
1384
|
+
return this.generateAxSecurityPrivilegeXml(objectName, properties);
|
|
1385
|
+
case 'security-duty':
|
|
1386
|
+
return this.generateAxSecurityDutyXml(objectName, properties);
|
|
1387
|
+
case 'security-role':
|
|
1388
|
+
return this.generateAxSecurityRoleXml(objectName, properties);
|
|
1389
|
+
case 'security-duty-extension':
|
|
1390
|
+
return this.generateAxSecurityDutyExtensionXml(objectName, properties);
|
|
1391
|
+
case 'security-role-extension':
|
|
1392
|
+
return this.generateAxSecurityRoleExtensionXml(objectName, properties);
|
|
1393
|
+
case 'business-event':
|
|
1394
|
+
return XmlTemplateGenerator.generateBusinessEventXml(objectName, properties);
|
|
1395
|
+
case 'tile':
|
|
1396
|
+
return XmlTemplateGenerator.generateAxTileXml(objectName, properties);
|
|
1397
|
+
case 'kpi':
|
|
1398
|
+
return XmlTemplateGenerator.generateAxKpiXml(objectName, properties);
|
|
1399
|
+
default:
|
|
1400
|
+
throw new Error(`Unsupported object type: ${objectType}`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Sanitize AxQuery XML — ensures xmlns="" and i:type="AxQuerySimple" are present
|
|
1405
|
+
* on the root <AxQuery> element. D365FO deserializer requires both attributes.
|
|
1406
|
+
*/
|
|
1407
|
+
static sanitizeQueryXml(xml) {
|
|
1408
|
+
return xml.replace(/<AxQuery(\s[^>]*)?>/, (_match, attrs) => {
|
|
1409
|
+
let a = attrs || '';
|
|
1410
|
+
if (!a.includes('xmlns=""')) {
|
|
1411
|
+
a += ' xmlns=""';
|
|
1412
|
+
}
|
|
1413
|
+
if (!a.includes('i:type="AxQuerySimple"')) {
|
|
1414
|
+
a += '\n\ti:type="AxQuerySimple"';
|
|
1415
|
+
}
|
|
1416
|
+
return `<AxQuery${a}>`;
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Sanitize AxReport XML to guarantee the structural elements required by the D365FO
|
|
1421
|
+
* Visual Studio Designer metadata loader, regardless of whether the XML was generated
|
|
1422
|
+
* by the template or supplied verbatim by a caller via the xmlContent parameter.
|
|
1423
|
+
*
|
|
1424
|
+
* Required invariants:
|
|
1425
|
+
* 1. xmlns="Microsoft.Dynamics.AX.Metadata.V2" on <AxReport> root
|
|
1426
|
+
* 2. <DataMethods /> directly after <Name>…</Name>
|
|
1427
|
+
* 3. xmlns="" on every <AxReportDataSet> child element (namespace reset)
|
|
1428
|
+
* 4. </AxReport> closing tag present (guard against truncated XML)
|
|
1429
|
+
* 5. <AxReportDesign> has xmlns="" and i:type="AxReportPrecisionDesign" attributes
|
|
1430
|
+
* (VS Designer won't show Designs sub-nodes without these)
|
|
1431
|
+
*/
|
|
1432
|
+
/**
|
|
1433
|
+
* Sanitize AxEnum XML — fixes common AI-generator mistakes that cause VS2022 to
|
|
1434
|
+
* silently ignore enum values or refuse to open the file:
|
|
1435
|
+
*
|
|
1436
|
+
* 1. <Values>…</Values> → <EnumValues>…</EnumValues>
|
|
1437
|
+
* AI models frequently map the JSON `enumValues` array to a plain <Values> wrapper;
|
|
1438
|
+
* D365FO deserializer requires <EnumValues>.
|
|
1439
|
+
*
|
|
1440
|
+
* 2. <AxEnum> without xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
|
|
1441
|
+
* The attribute is required for the i:type resolution inside the file.
|
|
1442
|
+
*
|
|
1443
|
+
* 3. More than 251 <AxEnumValue> elements — D365FO compiler hard limit.
|
|
1444
|
+
*/
|
|
1445
|
+
static sanitizeEnumXml(xml) {
|
|
1446
|
+
// 1. Rename <Values> container to <EnumValues>
|
|
1447
|
+
if (/<Values>/.test(xml) && !/<EnumValues>/.test(xml)) {
|
|
1448
|
+
xml = xml.replace(/<Values>/g, '<EnumValues>').replace(/<\/Values>/g, '</EnumValues>');
|
|
1449
|
+
console.error('[sanitizeEnumXml] Renamed <Values> → <EnumValues>');
|
|
1450
|
+
}
|
|
1451
|
+
// 2. Add xmlns:i to <AxEnum> root if missing
|
|
1452
|
+
if (!xml.includes('xmlns:i=')) {
|
|
1453
|
+
xml = xml.replace(/(<AxEnum)(\s|>)/, '$1 xmlns:i="http://www.w3.org/2001/XMLSchema-instance"$2');
|
|
1454
|
+
console.error('[sanitizeEnumXml] Added xmlns:i to <AxEnum>');
|
|
1455
|
+
}
|
|
1456
|
+
// 3. Validate max 251 enum values (D365FO compiler hard limit, MS Learn confirmed)
|
|
1457
|
+
const valueCount = (xml.match(/<AxEnumValue>/g) ?? []).length;
|
|
1458
|
+
if (valueCount > 251) {
|
|
1459
|
+
console.error(`[sanitizeEnumXml] ⚠️ WARNING: ${valueCount} enum values detected — D365FO supports max 251 (0–250). ` +
|
|
1460
|
+
`The compiler will reject this file. Consider splitting into multiple enums or using a class hierarchy.`);
|
|
1461
|
+
}
|
|
1462
|
+
return xml;
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Sanitize AxTable XML to ensure correct D365FO field element format.
|
|
1466
|
+
*
|
|
1467
|
+
* D365FO requires fields as:
|
|
1468
|
+
* <AxTableField xmlns=""
|
|
1469
|
+
* i:type="AxTableFieldString"> ... </AxTableField>
|
|
1470
|
+
*
|
|
1471
|
+
* AI generators often emit the shorter form:
|
|
1472
|
+
* <AxTableFieldString> ... </AxTableFieldString>
|
|
1473
|
+
*
|
|
1474
|
+
* This method also ensures <FullTextIndexes /> is present between </Fields> and <Indexes>.
|
|
1475
|
+
*/
|
|
1476
|
+
static sanitizeTableXml(xml) {
|
|
1477
|
+
const fieldTypes = [
|
|
1478
|
+
'AxTableFieldString', 'AxTableFieldInt', 'AxTableFieldInt64',
|
|
1479
|
+
'AxTableFieldReal', 'AxTableFieldDate', 'AxTableFieldUtcDateTime',
|
|
1480
|
+
'AxTableFieldEnum', 'AxTableFieldGuid', 'AxTableFieldContainer',
|
|
1481
|
+
];
|
|
1482
|
+
for (const ft of fieldTypes) {
|
|
1483
|
+
// Opening tag: <AxTableFieldString ...> → <AxTableField xmlns="" i:type="AxTableFieldString" ...>
|
|
1484
|
+
// Only replace if NOT already inside a correct <AxTableField xmlns="" i:type="..."> wrapper
|
|
1485
|
+
const openRe = new RegExp(`<${ft}(\\s[^>]*)?>`, 'g');
|
|
1486
|
+
xml = xml.replace(openRe, (_match, attrs) => {
|
|
1487
|
+
const extra = attrs ? attrs : '';
|
|
1488
|
+
return `<AxTableField xmlns=""\n\t\t\ti:type="${ft}"${extra}>`;
|
|
1489
|
+
});
|
|
1490
|
+
// Closing tag
|
|
1491
|
+
xml = xml.replace(new RegExp(`<\\/${ft}>`, 'g'), '</AxTableField>');
|
|
1492
|
+
}
|
|
1493
|
+
// Ensure <FullTextIndexes /> is present between </Fields> and <Indexes>
|
|
1494
|
+
if (!xml.includes('<FullTextIndexes')) {
|
|
1495
|
+
xml = xml.replace('</Fields>\n\t<Indexes', '</Fields>\n\t<FullTextIndexes />\n\t<Indexes');
|
|
1496
|
+
xml = xml.replace('</Fields>\n<Indexes', '</Fields>\n<FullTextIndexes />\n<Indexes');
|
|
1497
|
+
}
|
|
1498
|
+
return xml;
|
|
1499
|
+
}
|
|
1500
|
+
static sanitizeReportXml(xml) {
|
|
1501
|
+
// 1. Ensure xmlns="Microsoft.Dynamics.AX.Metadata.V2" on <AxReport> opening tag
|
|
1502
|
+
if (!xml.includes('xmlns="Microsoft.Dynamics.AX.Metadata.V2"')) {
|
|
1503
|
+
xml = xml.replace(/<AxReport(\s[^>]*)?>/, (match) => {
|
|
1504
|
+
// Insert the namespace attribute before the closing > of the tag
|
|
1505
|
+
return match.slice(0, -1) + ' xmlns="Microsoft.Dynamics.AX.Metadata.V2">';
|
|
1506
|
+
});
|
|
1507
|
+
console.error('[sanitizeReportXml] Added xmlns="Microsoft.Dynamics.AX.Metadata.V2" to <AxReport>');
|
|
1508
|
+
}
|
|
1509
|
+
// 2. Ensure <DataMethods /> exists directly after the top-level <Name>
|
|
1510
|
+
if (!xml.includes('<DataMethods')) {
|
|
1511
|
+
// Match only the first <Name>…</Name> (the report's own name, not nested ones)
|
|
1512
|
+
xml = xml.replace(/(<Name>[^<]*<\/Name>)/, '$1\n\t<DataMethods />');
|
|
1513
|
+
console.error('[sanitizeReportXml] Inserted missing <DataMethods />');
|
|
1514
|
+
}
|
|
1515
|
+
// 3. Ensure xmlns="" on each <AxReportDataSet> (bare tag without the attribute)
|
|
1516
|
+
if (xml.includes('<AxReportDataSet>')) {
|
|
1517
|
+
xml = xml.replace(/<AxReportDataSet>/g, '<AxReportDataSet xmlns="">');
|
|
1518
|
+
console.error('[sanitizeReportXml] Added xmlns="" to <AxReportDataSet> elements');
|
|
1519
|
+
}
|
|
1520
|
+
// 4. Ensure </AxReport> closing tag is present (guard against truncated XML)
|
|
1521
|
+
const trimmed = xml.trimEnd();
|
|
1522
|
+
if (!trimmed.endsWith('</AxReport>')) {
|
|
1523
|
+
xml = trimmed + '\n</AxReport>';
|
|
1524
|
+
console.error('[sanitizeReportXml] Appended missing </AxReport> closing tag');
|
|
1525
|
+
}
|
|
1526
|
+
// 5. Ensure <AxReportDesign> has xmlns="" and i:type="AxReportPrecisionDesign"
|
|
1527
|
+
// VS Designer requires both attributes to render the Designs sub-tree correctly.
|
|
1528
|
+
// Match bare <AxReportDesign> or one that is already partially attributed.
|
|
1529
|
+
xml = xml.replace(/<AxReportDesign(\s[^>]*)?>/, (match, attrs) => {
|
|
1530
|
+
const current = attrs || '';
|
|
1531
|
+
let updated = current;
|
|
1532
|
+
if (!updated.includes('xmlns=""')) {
|
|
1533
|
+
updated = ` xmlns=""${updated}`;
|
|
1534
|
+
}
|
|
1535
|
+
if (!updated.includes('i:type=')) {
|
|
1536
|
+
updated += `\n\t\t\t\ti:type="AxReportPrecisionDesign"`;
|
|
1537
|
+
}
|
|
1538
|
+
if (updated === current)
|
|
1539
|
+
return match; // nothing changed — idempotent
|
|
1540
|
+
console.error('[sanitizeReportXml] Fixed <AxReportDesign> attributes (xmlns="" + i:type)');
|
|
1541
|
+
return `<AxReportDesign${updated}>`;
|
|
1542
|
+
});
|
|
1543
|
+
// 6. Ensure <Parameters> block inside <AxReportDataSet> for real RDP datasets.
|
|
1544
|
+
// Skipped for stub/minimal datasets that have no <DataSourceType>.
|
|
1545
|
+
if (xml.includes('<DataSourceType>') && !xml.includes('<Parameters>')) {
|
|
1546
|
+
const axDatasetParams = '\t\t\t<Parameters>\n' +
|
|
1547
|
+
'\t\t\t\t<AxReportDataSetParameter>\n' +
|
|
1548
|
+
'\t\t\t\t\t<Name>AX_PartitionKey</Name>\n' +
|
|
1549
|
+
'\t\t\t\t\t<Alias>AX_PartitionKey</Alias>\n' +
|
|
1550
|
+
'\t\t\t\t\t<DataType>System.String</DataType>\n' +
|
|
1551
|
+
'\t\t\t\t\t<Parameter>AX_PartitionKey</Parameter>\n' +
|
|
1552
|
+
'\t\t\t\t</AxReportDataSetParameter>\n' +
|
|
1553
|
+
'\t\t\t\t<AxReportDataSetParameter>\n' +
|
|
1554
|
+
'\t\t\t\t\t<Name>AX_CompanyName</Name>\n' +
|
|
1555
|
+
'\t\t\t\t\t<Alias>AX_CompanyName</Alias>\n' +
|
|
1556
|
+
'\t\t\t\t\t<DataType>System.String</DataType>\n' +
|
|
1557
|
+
'\t\t\t\t\t<Parameter>AX_CompanyName</Parameter>\n' +
|
|
1558
|
+
'\t\t\t\t</AxReportDataSetParameter>\n' +
|
|
1559
|
+
'\t\t\t\t<AxReportDataSetParameter>\n' +
|
|
1560
|
+
'\t\t\t\t\t<Name>AX_UserContext</Name>\n' +
|
|
1561
|
+
'\t\t\t\t\t<Alias>AX_UserContext</Alias>\n' +
|
|
1562
|
+
'\t\t\t\t\t<DataType>System.String</DataType>\n' +
|
|
1563
|
+
'\t\t\t\t\t<Parameter>AX_UserContext</Parameter>\n' +
|
|
1564
|
+
'\t\t\t\t</AxReportDataSetParameter>\n' +
|
|
1565
|
+
'\t\t\t\t<AxReportDataSetParameter>\n' +
|
|
1566
|
+
'\t\t\t\t\t<Name>AX_RenderingCulture</Name>\n' +
|
|
1567
|
+
'\t\t\t\t\t<Alias>AX_RenderingCulture</Alias>\n' +
|
|
1568
|
+
'\t\t\t\t\t<DataType>System.String</DataType>\n' +
|
|
1569
|
+
'\t\t\t\t\t<Parameter>AX_RenderingCulture</Parameter>\n' +
|
|
1570
|
+
'\t\t\t\t</AxReportDataSetParameter>\n' +
|
|
1571
|
+
'\t\t\t\t<AxReportDataSetParameter>\n' +
|
|
1572
|
+
'\t\t\t\t\t<Name>AX_ReportContext</Name>\n' +
|
|
1573
|
+
'\t\t\t\t\t<Alias>AX_ReportContext</Alias>\n' +
|
|
1574
|
+
'\t\t\t\t\t<DataType>System.String</DataType>\n' +
|
|
1575
|
+
'\t\t\t\t\t<Parameter>AX_ReportContext</Parameter>\n' +
|
|
1576
|
+
'\t\t\t\t</AxReportDataSetParameter>\n' +
|
|
1577
|
+
'\t\t\t\t<AxReportDataSetParameter>\n' +
|
|
1578
|
+
'\t\t\t\t\t<Name>AX_RdpPreProcessedId</Name>\n' +
|
|
1579
|
+
'\t\t\t\t\t<Alias>AX_RdpPreProcessedId</Alias>\n' +
|
|
1580
|
+
'\t\t\t\t\t<DataType>System.String</DataType>\n' +
|
|
1581
|
+
'\t\t\t\t\t<Parameter>AX_RdpPreProcessedId</Parameter>\n' +
|
|
1582
|
+
'\t\t\t\t</AxReportDataSetParameter>\n' +
|
|
1583
|
+
'\t\t\t</Parameters>';
|
|
1584
|
+
if (xml.includes('</Fields>')) {
|
|
1585
|
+
xml = xml.replace('</Fields>', `</Fields>\n${axDatasetParams}`);
|
|
1586
|
+
}
|
|
1587
|
+
else if (xml.includes('<Fields />')) {
|
|
1588
|
+
xml = xml.replace('<Fields />', `<Fields />\n${axDatasetParams}`);
|
|
1589
|
+
}
|
|
1590
|
+
else {
|
|
1591
|
+
xml = xml.replace('</AxReportDataSet>', `${axDatasetParams}\n\t\t</AxReportDataSet>`);
|
|
1592
|
+
}
|
|
1593
|
+
console.error('[sanitizeReportXml] Added missing <Parameters> to <AxReportDataSet>');
|
|
1594
|
+
}
|
|
1595
|
+
// 7. Ensure <DefaultParameterGroup> before <Designs> for real RDP datasets.
|
|
1596
|
+
if (xml.includes('<DataSourceType>') && !xml.includes('<DefaultParameterGroup>') && xml.includes('<Designs>')) {
|
|
1597
|
+
const defaultParamGroup = '\t<DefaultParameterGroup>\n' +
|
|
1598
|
+
'\t\t<Name xmlns="">Parameters</Name>\n' +
|
|
1599
|
+
'\t\t<ReportParameterBases xmlns="">\n' +
|
|
1600
|
+
'\t\t\t<AxReportParameterBase xmlns=""\n' +
|
|
1601
|
+
'\t\t\t\t\ti:type="AxReportParameter">\n' +
|
|
1602
|
+
'\t\t\t\t<Name>AX_PartitionKey</Name>\n' +
|
|
1603
|
+
'\t\t\t\t<AllowBlank>true</AllowBlank>\n' +
|
|
1604
|
+
'\t\t\t\t<Nullable>true</Nullable>\n' +
|
|
1605
|
+
'\t\t\t\t<UserVisibility>Hidden</UserVisibility>\n' +
|
|
1606
|
+
'\t\t\t\t<DefaultValue />\n' +
|
|
1607
|
+
'\t\t\t\t<Values />\n' +
|
|
1608
|
+
'\t\t\t</AxReportParameterBase>\n' +
|
|
1609
|
+
'\t\t\t<AxReportParameterBase xmlns=""\n' +
|
|
1610
|
+
'\t\t\t\t\ti:type="AxReportParameter">\n' +
|
|
1611
|
+
'\t\t\t\t<Name>AX_CompanyName</Name>\n' +
|
|
1612
|
+
'\t\t\t\t<UserVisibility>Hidden</UserVisibility>\n' +
|
|
1613
|
+
'\t\t\t\t<DefaultValue />\n' +
|
|
1614
|
+
'\t\t\t\t<Values />\n' +
|
|
1615
|
+
'\t\t\t</AxReportParameterBase>\n' +
|
|
1616
|
+
'\t\t\t<AxReportParameterBase xmlns=""\n' +
|
|
1617
|
+
'\t\t\t\t\ti:type="AxReportParameter">\n' +
|
|
1618
|
+
'\t\t\t\t<Name>AX_UserContext</Name>\n' +
|
|
1619
|
+
'\t\t\t\t<AllowBlank>true</AllowBlank>\n' +
|
|
1620
|
+
'\t\t\t\t<Nullable>true</Nullable>\n' +
|
|
1621
|
+
'\t\t\t\t<UserVisibility>Hidden</UserVisibility>\n' +
|
|
1622
|
+
'\t\t\t\t<DefaultValue />\n' +
|
|
1623
|
+
'\t\t\t\t<Values />\n' +
|
|
1624
|
+
'\t\t\t</AxReportParameterBase>\n' +
|
|
1625
|
+
'\t\t\t<AxReportParameterBase xmlns=""\n' +
|
|
1626
|
+
'\t\t\t\t\ti:type="AxReportParameter">\n' +
|
|
1627
|
+
'\t\t\t\t<Name>AX_RenderingCulture</Name>\n' +
|
|
1628
|
+
'\t\t\t\t<AllowBlank>true</AllowBlank>\n' +
|
|
1629
|
+
'\t\t\t\t<Nullable>true</Nullable>\n' +
|
|
1630
|
+
'\t\t\t\t<UserVisibility>Hidden</UserVisibility>\n' +
|
|
1631
|
+
'\t\t\t\t<DefaultValue />\n' +
|
|
1632
|
+
'\t\t\t\t<Values />\n' +
|
|
1633
|
+
'\t\t\t</AxReportParameterBase>\n' +
|
|
1634
|
+
'\t\t\t<AxReportParameterBase xmlns=""\n' +
|
|
1635
|
+
'\t\t\t\t\ti:type="AxReportParameter">\n' +
|
|
1636
|
+
'\t\t\t\t<Name>AX_ReportContext</Name>\n' +
|
|
1637
|
+
'\t\t\t\t<AllowBlank>true</AllowBlank>\n' +
|
|
1638
|
+
'\t\t\t\t<Nullable>true</Nullable>\n' +
|
|
1639
|
+
'\t\t\t\t<UserVisibility>Hidden</UserVisibility>\n' +
|
|
1640
|
+
'\t\t\t\t<DefaultValue />\n' +
|
|
1641
|
+
'\t\t\t\t<Values />\n' +
|
|
1642
|
+
'\t\t\t</AxReportParameterBase>\n' +
|
|
1643
|
+
'\t\t\t<AxReportParameterBase xmlns=""\n' +
|
|
1644
|
+
'\t\t\t\t\ti:type="AxReportParameter">\n' +
|
|
1645
|
+
'\t\t\t\t<Name>AX_RdpPreProcessedId</Name>\n' +
|
|
1646
|
+
'\t\t\t\t<AllowBlank>true</AllowBlank>\n' +
|
|
1647
|
+
'\t\t\t\t<Nullable>true</Nullable>\n' +
|
|
1648
|
+
'\t\t\t\t<UserVisibility>Hidden</UserVisibility>\n' +
|
|
1649
|
+
'\t\t\t\t<DefaultValue />\n' +
|
|
1650
|
+
'\t\t\t\t<Values />\n' +
|
|
1651
|
+
'\t\t\t</AxReportParameterBase>\n' +
|
|
1652
|
+
'\t\t</ReportParameterBases>\n' +
|
|
1653
|
+
'\t</DefaultParameterGroup>';
|
|
1654
|
+
xml = xml.replace('<Designs>', `${defaultParamGroup}\n\t<Designs>`);
|
|
1655
|
+
console.error('[sanitizeReportXml] Added missing <DefaultParameterGroup>');
|
|
1656
|
+
}
|
|
1657
|
+
// 8. Fix embedded RDL structural issues based on the SSRS namespace version:
|
|
1658
|
+
// 2008/01 — <PageHeader>/<PageFooter> must be inside <Page> (direct child of <Report>).
|
|
1659
|
+
// 2010/01+ (2010, 2016, future) — <Body> and <Page> must NOT be direct children of
|
|
1660
|
+
// <Report>; they must be wrapped in:
|
|
1661
|
+
// <ReportSections><ReportSection>...</ReportSection></ReportSections>
|
|
1662
|
+
// Placing <Page> directly under <Report> causes:
|
|
1663
|
+
// "Deserialization failed: invalid child element 'Page'" in VS Designer.
|
|
1664
|
+
xml = xml.replace(/(<Text><!\[CDATA\[)([\s\S]*?)(\]\]><\/Text>)/, (_whole, open, rdl, close) => {
|
|
1665
|
+
const is2008 = rdl.includes('reporting/2008/01/reportdefinition');
|
|
1666
|
+
// Any SSRS namespace newer than 2008 requires ReportSections wrapping
|
|
1667
|
+
const isModernRdl = !is2008 && /reporting\/20\d\d\/\d\d\/reportdefinition/.test(rdl);
|
|
1668
|
+
const is2010 = isModernRdl; // kept for branch clarity below
|
|
1669
|
+
let fixedRdl = rdl;
|
|
1670
|
+
let changed = false;
|
|
1671
|
+
if (is2010 && !rdl.includes('<ReportSections>')) {
|
|
1672
|
+
// 2010 schema: collect any stray Body/Page/PageHeader/PageFooter that are direct
|
|
1673
|
+
// children of <Report>, then wrap them in ReportSections/ReportSection.
|
|
1674
|
+
let pageEl = '';
|
|
1675
|
+
const existingPageMatch = fixedRdl.match(/<Page(?:\s[^>]*)?>([\s\S]*?)<\/Page>/);
|
|
1676
|
+
if (existingPageMatch) {
|
|
1677
|
+
pageEl = existingPageMatch[0];
|
|
1678
|
+
fixedRdl = fixedRdl.replace(existingPageMatch[0], '');
|
|
1679
|
+
// PageHeader/PageFooter may still be direct children of <Report> (outside the <Page>
|
|
1680
|
+
// element we just extracted). Inject them into pageEl before </Page>.
|
|
1681
|
+
let extraPageContent = '';
|
|
1682
|
+
const phMatch = fixedRdl.match(/<PageHeader[\s\S]*?<\/PageHeader>/);
|
|
1683
|
+
if (phMatch && !pageEl.includes('<PageHeader')) {
|
|
1684
|
+
extraPageContent += phMatch[0];
|
|
1685
|
+
fixedRdl = fixedRdl.replace(phMatch[0], '');
|
|
1686
|
+
}
|
|
1687
|
+
const pfMatch = fixedRdl.match(/<PageFooter[\s\S]*?<\/PageFooter>/);
|
|
1688
|
+
if (pfMatch && !pageEl.includes('<PageFooter')) {
|
|
1689
|
+
extraPageContent += (extraPageContent ? '\n' : '') + pfMatch[0];
|
|
1690
|
+
fixedRdl = fixedRdl.replace(pfMatch[0], '');
|
|
1691
|
+
}
|
|
1692
|
+
if (extraPageContent) {
|
|
1693
|
+
pageEl = pageEl.replace('</Page>', extraPageContent.trim() + '\n</Page>');
|
|
1694
|
+
console.error('[sanitizeReportXml] Moved stray <PageHeader>/<PageFooter> into existing <Page> (2010 RDL)');
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
else {
|
|
1698
|
+
let pageInner = '';
|
|
1699
|
+
const phMatch = fixedRdl.match(/<PageHeader[\s\S]*?<\/PageHeader>/);
|
|
1700
|
+
if (phMatch) {
|
|
1701
|
+
pageInner += phMatch[0];
|
|
1702
|
+
fixedRdl = fixedRdl.replace(phMatch[0], '');
|
|
1703
|
+
}
|
|
1704
|
+
const pfMatch = fixedRdl.match(/<PageFooter[\s\S]*?<\/PageFooter>/);
|
|
1705
|
+
if (pfMatch) {
|
|
1706
|
+
pageInner += (pageInner ? '\n' : '') + pfMatch[0];
|
|
1707
|
+
fixedRdl = fixedRdl.replace(pfMatch[0], '');
|
|
1708
|
+
}
|
|
1709
|
+
if (pageInner)
|
|
1710
|
+
pageEl = '<Page>\n' + pageInner.trim() + '\n</Page>';
|
|
1711
|
+
}
|
|
1712
|
+
const bodyMatch = fixedRdl.match(/<Body[\s\S]*?<\/Body>/);
|
|
1713
|
+
let sectionContent = '';
|
|
1714
|
+
if (bodyMatch) {
|
|
1715
|
+
sectionContent += bodyMatch[0];
|
|
1716
|
+
fixedRdl = fixedRdl.replace(bodyMatch[0], '');
|
|
1717
|
+
}
|
|
1718
|
+
if (pageEl)
|
|
1719
|
+
sectionContent += (sectionContent ? '\n' : '') + pageEl;
|
|
1720
|
+
if (sectionContent) {
|
|
1721
|
+
const reportSections = '<ReportSections>\n<ReportSection>\n' + sectionContent.trim() + '\n</ReportSection>\n</ReportSections>';
|
|
1722
|
+
fixedRdl = fixedRdl.includes('</Report>')
|
|
1723
|
+
? fixedRdl.replace('</Report>', reportSections + '\n</Report>')
|
|
1724
|
+
: fixedRdl + '\n' + reportSections;
|
|
1725
|
+
changed = true;
|
|
1726
|
+
const rdlVersion = rdl.match(/reporting\/(20\d\d\/\d\d)\/reportdefinition/)?.[1] ?? 'modern';
|
|
1727
|
+
console.error(`[sanitizeReportXml] Wrapped Body+Page in <ReportSections>/<ReportSection> for ${rdlVersion} RDL`);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
else if (is2008 && (rdl.includes('<PageHeader') || rdl.includes('<PageFooter'))) {
|
|
1731
|
+
// 2008 schema: <PageHeader>/<PageFooter> must be inside <Page>, not direct children of <Report>.
|
|
1732
|
+
// A real D365FO RDL always has a <Page> element (PageWidth/Height/Margins) but PageHeader is
|
|
1733
|
+
// still a sibling — the old guard `!rdl.match(/<Page...>/)` incorrectly skipped this case.
|
|
1734
|
+
// Strategy:
|
|
1735
|
+
// a) If <Page> already exists — inject PageHeader/PageFooter before </Page>.
|
|
1736
|
+
// b) If <Page> doesn't exist — create one after </Body>.
|
|
1737
|
+
const pageMatch = fixedRdl.match(/<Page(?:\s[^>]*)?>[\s\S]*?<\/Page>/);
|
|
1738
|
+
const alreadyInPage = !!pageMatch &&
|
|
1739
|
+
(pageMatch[0].includes('<PageHeader') || pageMatch[0].includes('<PageFooter'));
|
|
1740
|
+
if (!alreadyInPage) {
|
|
1741
|
+
let pageInner = '';
|
|
1742
|
+
const phMatch = fixedRdl.match(/<PageHeader[\s\S]*?<\/PageHeader>/);
|
|
1743
|
+
if (phMatch) {
|
|
1744
|
+
pageInner += phMatch[0];
|
|
1745
|
+
fixedRdl = fixedRdl.replace(phMatch[0], '');
|
|
1746
|
+
}
|
|
1747
|
+
const pfMatch = fixedRdl.match(/<PageFooter[\s\S]*?<\/PageFooter>/);
|
|
1748
|
+
if (pfMatch) {
|
|
1749
|
+
pageInner += (pageInner ? '\n' : '') + pfMatch[0];
|
|
1750
|
+
fixedRdl = fixedRdl.replace(pfMatch[0], '');
|
|
1751
|
+
}
|
|
1752
|
+
if (pageInner) {
|
|
1753
|
+
if (pageMatch) {
|
|
1754
|
+
// Inject into the existing <Page> before </Page>
|
|
1755
|
+
const updatedPage = pageMatch[0].replace('</Page>', pageInner.trim() + '\n</Page>');
|
|
1756
|
+
fixedRdl = fixedRdl.replace(pageMatch[0], updatedPage);
|
|
1757
|
+
}
|
|
1758
|
+
else {
|
|
1759
|
+
// No existing <Page> — create one after </Body>
|
|
1760
|
+
const pageEl = '<Page>\n' + pageInner.trim() + '\n</Page>';
|
|
1761
|
+
fixedRdl = fixedRdl.includes('</Body>')
|
|
1762
|
+
? fixedRdl.replace('</Body>', '</Body>\n' + pageEl)
|
|
1763
|
+
: fixedRdl.replace('</Report>', pageEl + '\n</Report>');
|
|
1764
|
+
}
|
|
1765
|
+
changed = true;
|
|
1766
|
+
console.error('[sanitizeReportXml] Moved <PageHeader>/<PageFooter> inside <Page> in 2008 RDL');
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (!changed)
|
|
1771
|
+
return _whole;
|
|
1772
|
+
return open + fixedRdl + close;
|
|
1773
|
+
});
|
|
1774
|
+
// 9. Fix wrong margin element names inside embedded RDL.
|
|
1775
|
+
// Some AI-generated RDLs use CSS-style names (MarginTop, MarginLeft, …) instead of
|
|
1776
|
+
// the correct SSRS RDL names (TopMargin, LeftMargin, …). All SSRS namespace versions
|
|
1777
|
+
// require the XMargin form — MarginX causes "invalid child element 'MarginTop'" in
|
|
1778
|
+
// VS Designer even though the value and namespace are otherwise correct.
|
|
1779
|
+
if (xml.includes('<MarginTop>') || xml.includes('<MarginBottom>') ||
|
|
1780
|
+
xml.includes('<MarginLeft>') || xml.includes('<MarginRight>')) {
|
|
1781
|
+
xml = xml
|
|
1782
|
+
.replace(/<MarginTop>/g, '<TopMargin>').replace(/<\/MarginTop>/g, '</TopMargin>')
|
|
1783
|
+
.replace(/<MarginBottom>/g, '<BottomMargin>').replace(/<\/MarginBottom>/g, '</BottomMargin>')
|
|
1784
|
+
.replace(/<MarginLeft>/g, '<LeftMargin>').replace(/<\/MarginLeft>/g, '</LeftMargin>')
|
|
1785
|
+
.replace(/<MarginRight>/g, '<RightMargin>').replace(/<\/MarginRight>/g, '</RightMargin>');
|
|
1786
|
+
console.error('[sanitizeReportXml] Fixed wrong margin element names (MarginX → XMargin) in embedded RDL');
|
|
1787
|
+
}
|
|
1788
|
+
// 10. Ensure <Body> inside embedded RDL <ReportSection> has <ReportItems /> as its first
|
|
1789
|
+
// child element. SSRS schema requires the order: ReportItems → Height → Style.
|
|
1790
|
+
// Without <ReportItems>, VS Designer can't surface the DataSet in the Report Data panel
|
|
1791
|
+
// (it appears as if the dataset "disappeared") and may refuse to open the report.
|
|
1792
|
+
xml = xml.replace(/(<Text><!\[CDATA\[)([\s\S]*?)(\]\]><\/Text>)/, (_whole, open, rdl, close) => {
|
|
1793
|
+
// Match a <Body> that contains <Height> or <Style> but lacks <ReportItems>
|
|
1794
|
+
// (i.e., an empty skeleton body without any report items)
|
|
1795
|
+
const fixedRdl = rdl.replace(/<Body>\s*\n(\s*)((?!<ReportItems)[\s\S]*?)<\/Body>/, (_bodyMatch, indent, bodyContent) => {
|
|
1796
|
+
// Only add <ReportItems /> when the body has no report items at all
|
|
1797
|
+
if (bodyContent.includes('<ReportItems'))
|
|
1798
|
+
return _bodyMatch;
|
|
1799
|
+
console.error('[sanitizeReportXml] Added missing <ReportItems /> as first child of <Body> in embedded RDL');
|
|
1800
|
+
return `<Body>\n${indent}<ReportItems />\n${indent}${bodyContent.trimStart()}</Body>`;
|
|
1801
|
+
});
|
|
1802
|
+
if (fixedRdl === rdl)
|
|
1803
|
+
return _whole;
|
|
1804
|
+
return open + fixedRdl + close;
|
|
1805
|
+
});
|
|
1806
|
+
// 11. Fix doubled closing tags inside embedded RDL CDATA.
|
|
1807
|
+
// AI generators sometimes emit </Foo></Foo> (the closing tag twice).
|
|
1808
|
+
// These are invalid XML and cause "Deserialization failed" in VS Designer.
|
|
1809
|
+
// Pattern: </TagName></TagName> → </TagName>
|
|
1810
|
+
xml = xml.replace(/(<Text><!\[CDATA\[)([\s\S]*?)(\]\]><\/Text>)/, (_whole, open, rdl, close) => {
|
|
1811
|
+
const fixedRdl = rdl.replace(/<\/(\w+)><\/\1>/g, (_m, tag) => {
|
|
1812
|
+
console.error(`[sanitizeReportXml] Removed doubled closing tag </${tag}></${tag}> in embedded RDL`);
|
|
1813
|
+
return `</${tag}>`;
|
|
1814
|
+
});
|
|
1815
|
+
if (fixedRdl === rdl)
|
|
1816
|
+
return _whole;
|
|
1817
|
+
return open + fixedRdl + close;
|
|
1818
|
+
});
|
|
1819
|
+
// 12. Fix <Value> as direct child of <Textbox> in embedded RDL.
|
|
1820
|
+
// SSRS 2008+ schema requires: <Textbox> → <Paragraphs><Paragraph><TextRuns><TextRun><Value>
|
|
1821
|
+
// AI generators sometimes emit <Value> directly inside <Textbox>, which causes:
|
|
1822
|
+
// "invalid child element 'Value'" error in VS Designer.
|
|
1823
|
+
// This fix wraps any bare <Value>…</Value> found as a direct child of <Textbox>
|
|
1824
|
+
// into the correct paragraph/textrun structure.
|
|
1825
|
+
xml = xml.replace(/(<Text><!\[CDATA\[)([\s\S]*?)(\]\]><\/Text>)/, (_whole, open, rdl, close) => {
|
|
1826
|
+
// Look for <Textbox ...> that contains a direct <Value> child (not inside <TextRun>)
|
|
1827
|
+
const fixedRdl = rdl.replace(/(<Textbox\b[^>]*>)([\s\S]*?)(<\/Textbox>)/g, (tbMatch, tbOpen, tbContent, tbClose) => {
|
|
1828
|
+
// Only act if there is a <Value> but no <Paragraphs> wrapping yet
|
|
1829
|
+
if (!tbContent.includes('<Value>') && !tbContent.includes('<Value ='))
|
|
1830
|
+
return tbMatch;
|
|
1831
|
+
if (tbContent.includes('<Paragraphs>'))
|
|
1832
|
+
return tbMatch;
|
|
1833
|
+
const fixedContent = tbContent.replace(/<Value>([\s\S]*?)<\/Value>/, (_vMatch, val) => {
|
|
1834
|
+
console.error('[sanitizeReportXml] Wrapped bare <Value> in <Textbox> into <Paragraphs> structure');
|
|
1835
|
+
return `<Paragraphs>\n <Paragraph>\n <TextRuns>\n <TextRun>\n <Value>${val}</Value>\n <Style />\n </TextRun>\n </TextRuns>\n <Style />\n </Paragraph>\n </Paragraphs>`;
|
|
1836
|
+
});
|
|
1837
|
+
if (fixedContent === tbContent)
|
|
1838
|
+
return tbMatch;
|
|
1839
|
+
return tbOpen + fixedContent + tbClose;
|
|
1840
|
+
});
|
|
1841
|
+
if (fixedRdl === rdl)
|
|
1842
|
+
return _whole;
|
|
1843
|
+
return open + fixedRdl + close;
|
|
1844
|
+
});
|
|
1845
|
+
// 13. Fix <ColSpan>/<RowSpan> as direct children of <TablixCell>.
|
|
1846
|
+
// SSRS schema only allows CellContents, DataElementName, DataElementOutput
|
|
1847
|
+
// as direct children of <TablixCell>. ColSpan/RowSpan must be INSIDE
|
|
1848
|
+
// <CellContents> (after the report item, before </CellContents>).
|
|
1849
|
+
// AI generators emit them BEFORE or AFTER the <CellContents> block:
|
|
1850
|
+
// <TablixCell><ColSpan>2</ColSpan><CellContents>...</CellContents></TablixCell>
|
|
1851
|
+
// <TablixCell><CellContents>...</CellContents><ColSpan>2</ColSpan></TablixCell>
|
|
1852
|
+
// Both cause "invalid child element 'ColSpan'" deserialization error.
|
|
1853
|
+
xml = xml.replace(/(<Text><!\[CDATA\[)([\s\S]*?)(\]\]><\/Text>)/, (_whole, open, rdl, close) => {
|
|
1854
|
+
const fixedRdl = rdl.replace(/(<TablixCell>)([\s\S]*?)(<\/TablixCell>)/g, (tcMatch, tcOpen, tcContent, tcClose) => {
|
|
1855
|
+
// Use indexOf to split tcContent into: beforeCC / ccBlock / afterCC.
|
|
1856
|
+
// This reliably handles spans placed either before OR after CellContents.
|
|
1857
|
+
const ccStart = tcContent.indexOf('<CellContents');
|
|
1858
|
+
const ccEnd = tcContent.indexOf('</CellContents>');
|
|
1859
|
+
if (ccStart === -1 || ccEnd === -1)
|
|
1860
|
+
return tcMatch;
|
|
1861
|
+
const beforeCC = tcContent.substring(0, ccStart);
|
|
1862
|
+
const ccBlock = tcContent.substring(ccStart, ccEnd + '</CellContents>'.length);
|
|
1863
|
+
const afterCC = tcContent.substring(ccEnd + '</CellContents>'.length);
|
|
1864
|
+
// Collect ColSpan/RowSpan from anywhere outside CellContents
|
|
1865
|
+
const spanTagRe = () => /[ \t\r\n]*<(ColSpan|RowSpan)>[^<]*<\/\1>/g;
|
|
1866
|
+
let spans = '';
|
|
1867
|
+
const cleanBefore = beforeCC.replace(spanTagRe(), (m) => { spans += '\n' + m.trim(); return ''; });
|
|
1868
|
+
const cleanAfter = afterCC.replace(spanTagRe(), (m) => { spans += '\n' + m.trim(); return ''; });
|
|
1869
|
+
if (!spans)
|
|
1870
|
+
return tcMatch;
|
|
1871
|
+
// Move collected spans inside CellContents, just before </CellContents>
|
|
1872
|
+
const fixedCC = ccBlock.replace('</CellContents>', `${spans}\n</CellContents>`);
|
|
1873
|
+
console.error('[sanitizeReportXml] Moved <ColSpan>/<RowSpan> from <TablixCell> into <CellContents> in embedded RDL');
|
|
1874
|
+
return tcOpen + cleanBefore + fixedCC + cleanAfter + tcClose;
|
|
1875
|
+
});
|
|
1876
|
+
if (fixedRdl === rdl)
|
|
1877
|
+
return _whole;
|
|
1878
|
+
return open + fixedRdl + close;
|
|
1879
|
+
});
|
|
1880
|
+
// 14. Fix flat border properties as direct children of <Style>.
|
|
1881
|
+
// SSRS <Style> only accepts <Border>, <TopBorder>, <BottomBorder>,
|
|
1882
|
+
// <LeftBorder>, <RightBorder> as border wrappers — not flat attributes
|
|
1883
|
+
// like <BorderStyle>, <BorderColor>, <BorderWidth>.
|
|
1884
|
+
// AI generators often emit:
|
|
1885
|
+
// <Style><BorderStyle>Solid</BorderStyle><BorderColor>#000</BorderColor></Style>
|
|
1886
|
+
// but the correct form is:
|
|
1887
|
+
// <Style><Border><Style>Solid</Style><Color>#000</Color></Border></Style>
|
|
1888
|
+
// Same pattern applies to TopBorderStyle/TopBorderColor/TopBorderWidth etc.
|
|
1889
|
+
//
|
|
1890
|
+
// Previous approach (matching <Style>…</Style> blocks non-greedily) had a
|
|
1891
|
+
// nesting failure: if an outer <Style> contained a nested <Style> element
|
|
1892
|
+
// (e.g. <Border><Style>Solid</Style>…</Border>) BEFORE a flat <BorderStyle>,
|
|
1893
|
+
// the non-greedy regex would bind the outer opening <Style> to the inner
|
|
1894
|
+
// closing </Style>, leaving the flat tag unprocessed.
|
|
1895
|
+
//
|
|
1896
|
+
// New approach: scan the CDATA directly for flat border-property clusters
|
|
1897
|
+
// and replace them with the correct wrapper, independent of the containing
|
|
1898
|
+
// <Style> block. Adjacent flat tags (same group, separated only by
|
|
1899
|
+
// whitespace) are collapsed into a single wrapper element.
|
|
1900
|
+
xml = xml.replace(/(<Text><!\[CDATA\[)([\s\S]*?)(\]\]><\/Text>)/g, (_whole, open, rdl, close) => {
|
|
1901
|
+
let fixedRdl = rdl;
|
|
1902
|
+
let changed = false;
|
|
1903
|
+
const groups = [
|
|
1904
|
+
['Border', 'Border'],
|
|
1905
|
+
['TopBorder', 'TopBorder'],
|
|
1906
|
+
['BottomBorder', 'BottomBorder'],
|
|
1907
|
+
['LeftBorder', 'LeftBorder'],
|
|
1908
|
+
['RightBorder', 'RightBorder'],
|
|
1909
|
+
];
|
|
1910
|
+
for (const [prefix, wrapper] of groups) {
|
|
1911
|
+
const st = `${prefix}Style`;
|
|
1912
|
+
const ct = `${prefix}Color`;
|
|
1913
|
+
const wt = `${prefix}Width`;
|
|
1914
|
+
if (!new RegExp(`<(?:${st}|${ct}|${wt})>`).test(fixedRdl))
|
|
1915
|
+
continue;
|
|
1916
|
+
// Build a regex that matches a cluster of 1–3 adjacent flat border tags
|
|
1917
|
+
// for this group (in any order, with optional whitespace between them).
|
|
1918
|
+
const singleTag = `(?:<${st}>([^<]*)<\\/${st}>|<${ct}>([^<]*)<\\/${ct}>|<${wt}>([^<]*)<\\/${wt}>)`;
|
|
1919
|
+
const clusterRe = new RegExp(`${singleTag}(?:\\s*${singleTag})?(?:\\s*${singleTag})?`, 'g');
|
|
1920
|
+
fixedRdl = fixedRdl.replace(clusterRe, (match) => {
|
|
1921
|
+
// Extract each flat-tag value from the matched cluster via side-effect callbacks.
|
|
1922
|
+
let bStyle = '', bColor = '', bWidth = '';
|
|
1923
|
+
match.replace(new RegExp(`<${st}>([^<]*)<\\/${st}>`), (_, v) => { bStyle = v; return ''; });
|
|
1924
|
+
match.replace(new RegExp(`<${ct}>([^<]*)<\\/${ct}>`), (_, v) => { bColor = v; return ''; });
|
|
1925
|
+
match.replace(new RegExp(`<${wt}>([^<]*)<\\/${wt}>`), (_, v) => { bWidth = v; return ''; });
|
|
1926
|
+
let inner = '';
|
|
1927
|
+
if (bStyle)
|
|
1928
|
+
inner += `<Style>${bStyle}</Style>`;
|
|
1929
|
+
if (bColor)
|
|
1930
|
+
inner += `<Color>${bColor}</Color>`;
|
|
1931
|
+
if (bWidth)
|
|
1932
|
+
inner += `<Width>${bWidth}</Width>`;
|
|
1933
|
+
changed = true;
|
|
1934
|
+
return `<${wrapper}>${inner}</${wrapper}>`;
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
if (!changed)
|
|
1938
|
+
return _whole;
|
|
1939
|
+
console.error('[sanitizeReportXml] Wrapped flat border properties into <Border> inside <Style> in embedded RDL');
|
|
1940
|
+
return open + fixedRdl + close;
|
|
1941
|
+
});
|
|
1942
|
+
// 15. Fix absorbed TablixCells following a ColSpan > 1.
|
|
1943
|
+
// Confirmed by real D365FO reports: when <CellContents> has <ColSpan>N</ColSpan>,
|
|
1944
|
+
// the next N-1 sibling TablixCells in the same <TablixCells> block must be empty
|
|
1945
|
+
// (<TablixCell />). AI generators give those absorbed cells full CellContents,
|
|
1946
|
+
// causing VS Report Designer to render an empty/broken design surface.
|
|
1947
|
+
// Approach: for each <ColSpan>N</ColSpan>, trace past its containing
|
|
1948
|
+
// </CellContents></TablixCell> then depth-count-walk the next N-1 TablixCells
|
|
1949
|
+
// and replace any non-empty ones with <TablixCell />.
|
|
1950
|
+
xml = xml.replace(/(<Text><!\[CDATA\[)([\s\S]*?)(\]\]><\/Text>)/, (_whole, open, rdl, close) => {
|
|
1951
|
+
let fixedRdl = rdl;
|
|
1952
|
+
const patches = [];
|
|
1953
|
+
const csRe = /<ColSpan>(\d+)<\/ColSpan>/g;
|
|
1954
|
+
let csMatch;
|
|
1955
|
+
while ((csMatch = csRe.exec(fixedRdl)) !== null) {
|
|
1956
|
+
const span = parseInt(csMatch[1], 10);
|
|
1957
|
+
if (span <= 1)
|
|
1958
|
+
continue;
|
|
1959
|
+
// Find </CellContents> that closes the CellContents containing this ColSpan
|
|
1960
|
+
const ccCloseIdx = fixedRdl.indexOf('</CellContents>', csMatch.index + csMatch[0].length);
|
|
1961
|
+
if (ccCloseIdx === -1)
|
|
1962
|
+
continue;
|
|
1963
|
+
// Find </TablixCell> that closes the TablixCell containing this CellContents
|
|
1964
|
+
const tcCloseIdx = fixedRdl.indexOf('</TablixCell>', ccCloseIdx + '</CellContents>'.length);
|
|
1965
|
+
if (tcCloseIdx === -1)
|
|
1966
|
+
continue;
|
|
1967
|
+
let pos = tcCloseIdx + '</TablixCell>'.length;
|
|
1968
|
+
for (let i = 0; i < span - 1; i++) {
|
|
1969
|
+
// Skip whitespace
|
|
1970
|
+
while (pos < fixedRdl.length && /\s/.test(fixedRdl[pos]))
|
|
1971
|
+
pos++;
|
|
1972
|
+
if (fixedRdl.startsWith('<TablixCell />', pos) || fixedRdl.startsWith('<TablixCell/>', pos)) {
|
|
1973
|
+
// Already empty — advance
|
|
1974
|
+
pos += fixedRdl.startsWith('<TablixCell />', pos) ? '<TablixCell />'.length : '<TablixCell/>'.length;
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
if (!fixedRdl.startsWith('<TablixCell>', pos))
|
|
1978
|
+
break; // Not a TablixCell
|
|
1979
|
+
// Walk to balanced </TablixCell>, counting nested TablixCell depth
|
|
1980
|
+
let depth = 1;
|
|
1981
|
+
let search = pos + '<TablixCell>'.length;
|
|
1982
|
+
while (depth > 0 && search < fixedRdl.length) {
|
|
1983
|
+
const nextOpen = fixedRdl.indexOf('<TablixCell>', search);
|
|
1984
|
+
const nextClose = fixedRdl.indexOf('</TablixCell>', search);
|
|
1985
|
+
if (nextClose === -1) {
|
|
1986
|
+
depth = 0;
|
|
1987
|
+
break;
|
|
1988
|
+
}
|
|
1989
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
1990
|
+
depth++;
|
|
1991
|
+
search = nextOpen + '<TablixCell>'.length;
|
|
1992
|
+
}
|
|
1993
|
+
else {
|
|
1994
|
+
depth--;
|
|
1995
|
+
search = nextClose + '</TablixCell>'.length;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
const cellEnd = search;
|
|
1999
|
+
patches.push({ start: pos, end: cellEnd });
|
|
2000
|
+
pos = cellEnd;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (patches.length === 0)
|
|
2004
|
+
return _whole;
|
|
2005
|
+
// Apply patches in reverse order to preserve string positions
|
|
2006
|
+
patches.sort((a, b) => b.start - a.start);
|
|
2007
|
+
let result = fixedRdl;
|
|
2008
|
+
for (const p of patches) {
|
|
2009
|
+
result = result.substring(0, p.start) + '<TablixCell />' + result.substring(p.end);
|
|
2010
|
+
}
|
|
2011
|
+
console.error(`[sanitizeReportXml] Emptied ${patches.length} absorbed TablixCell(s) following ColSpan in embedded RDL`);
|
|
2012
|
+
return open + result + close;
|
|
2013
|
+
});
|
|
2014
|
+
// 16. Rename reversed border side wrapper element names.
|
|
2015
|
+
// SSRS schema expects <TopBorder>, <BottomBorder>, <LeftBorder>, <RightBorder>.
|
|
2016
|
+
// AI generators often emit them reversed: <BorderTop>, <BorderBottom>, etc.
|
|
2017
|
+
// The inner content (<Style>, <Color>, <Width>) is already correct — only
|
|
2018
|
+
// the wrapper element name needs to change.
|
|
2019
|
+
if (xml.includes('<BorderTop>') || xml.includes('<BorderBottom>') ||
|
|
2020
|
+
xml.includes('<BorderLeft>') || xml.includes('<BorderRight>')) {
|
|
2021
|
+
xml = xml
|
|
2022
|
+
.replace(/<BorderTop>/g, '<TopBorder>').replace(/<\/BorderTop>/g, '</TopBorder>')
|
|
2023
|
+
.replace(/<BorderBottom>/g, '<BottomBorder>').replace(/<\/BorderBottom>/g, '</BottomBorder>')
|
|
2024
|
+
.replace(/<BorderLeft>/g, '<LeftBorder>').replace(/<\/BorderLeft>/g, '</LeftBorder>')
|
|
2025
|
+
.replace(/<BorderRight>/g, '<RightBorder>').replace(/<\/BorderRight>/g, '</RightBorder>');
|
|
2026
|
+
console.error('[sanitizeReportXml] Fixed reversed border side wrapper names (BorderXxx → XxxBorder) in RDL');
|
|
2027
|
+
}
|
|
2028
|
+
// 17. Add missing </Style> before </Paragraph> when Paragraph-level Style is
|
|
2029
|
+
// left unclosed. AI generators sometimes emit:
|
|
2030
|
+
// <Paragraph><TextRuns>...</TextRuns><Style><TextAlign>Right</TextAlign></Paragraph>
|
|
2031
|
+
// The </Style> before </Paragraph> is missing, which makes the XML
|
|
2032
|
+
// malformed (</Paragraph> appears to close inside <Style>) and causes
|
|
2033
|
+
// SSRS deserialization to fail entirely.
|
|
2034
|
+
xml = xml.replace(/(<Text><!\[CDATA\[)([\s\S]*?)(\]\]><\/Text>)/, (_whole, open, rdl, close) => {
|
|
2035
|
+
const fixedRdl = rdl.replace(/<Paragraph>([\s\S]*?)<\/Paragraph>/g, (match, inner) => {
|
|
2036
|
+
const opens = (inner.match(/<Style>/g) || []).length;
|
|
2037
|
+
const closes = (inner.match(/<\/Style>/g) || []).length;
|
|
2038
|
+
if (opens === closes)
|
|
2039
|
+
return match;
|
|
2040
|
+
const missing = opens - closes;
|
|
2041
|
+
console.error('[sanitizeReportXml] Added missing </Style> tag(s) inside <Paragraph> in embedded RDL');
|
|
2042
|
+
return `<Paragraph>${inner}${'</Style>'.repeat(missing)}</Paragraph>`;
|
|
2043
|
+
});
|
|
2044
|
+
if (fixedRdl === rdl)
|
|
2045
|
+
return _whole;
|
|
2046
|
+
return open + fixedRdl + close;
|
|
2047
|
+
});
|
|
2048
|
+
// 18. Reconcile TablixCells count with TablixColumns count.
|
|
2049
|
+
// Each TablixRow must have exactly as many TablixCell elements as there
|
|
2050
|
+
// are TablixColumn entries in the enclosing Tablix's TablixColumns block.
|
|
2051
|
+
// When the counts are mismatched VS Report Designer throws:
|
|
2052
|
+
// "Index was out of range. Must be non-negative and less than the size
|
|
2053
|
+
// of the collection. Parameter name: index"
|
|
2054
|
+
//
|
|
2055
|
+
// Two cases are handled:
|
|
2056
|
+
// A) A TablixRow has FEWER cells than TablixColumns → pad with <TablixCell />
|
|
2057
|
+
// B) TablixColumns has FEWER entries than max cells per row → pad columns
|
|
2058
|
+
//
|
|
2059
|
+
// The fix finds each top-level <Tablix>…</Tablix> block using depth
|
|
2060
|
+
// tracking (to handle multiple/nested Tablix controls correctly) and
|
|
2061
|
+
// reconciles column vs cell counts within each one independently.
|
|
2062
|
+
xml = xml.replace(/(<Text><!\[CDATA\[)([\s\S]*?)(\]\]><\/Text>)/g, (_whole, open, rdl, close) => {
|
|
2063
|
+
let fixedRdl = rdl;
|
|
2064
|
+
let changed = false;
|
|
2065
|
+
const processTablix = (block) => {
|
|
2066
|
+
// Count declared TablixColumn entries (exclude TablixColumns container)
|
|
2067
|
+
const colsMatch = block.match(/<TablixColumns>([\s\S]*?)<\/TablixColumns>/);
|
|
2068
|
+
if (!colsMatch)
|
|
2069
|
+
return block;
|
|
2070
|
+
const colCount = (colsMatch[1].match(/<TablixColumn[\s>\/]/g) || []).length;
|
|
2071
|
+
if (colCount === 0)
|
|
2072
|
+
return block;
|
|
2073
|
+
// Sub-case A: pad each TablixCells block that has too few cells
|
|
2074
|
+
let result = block.replace(/(<TablixCells>)([\s\S]*?)(<\/TablixCells>)/g, (m, o, inner, c) => {
|
|
2075
|
+
const n = (inner.match(/<TablixCell[\s>\/]/g) || []).length;
|
|
2076
|
+
if (n >= colCount)
|
|
2077
|
+
return m;
|
|
2078
|
+
changed = true;
|
|
2079
|
+
const padding = Array(colCount - n).fill('\t\t\t\t<TablixCell />').join('\n');
|
|
2080
|
+
return `${o}${inner}\n${padding}\n\t\t\t${c}`;
|
|
2081
|
+
});
|
|
2082
|
+
// Sub-case B: ensure TablixColumns has enough entries
|
|
2083
|
+
const cellsBlocks = result.match(/<TablixCells>([\s\S]*?)<\/TablixCells>/g) || [];
|
|
2084
|
+
const maxCells = cellsBlocks.reduce((mx, b) => {
|
|
2085
|
+
const n = (b.match(/<TablixCell[\s>\/]/g) || []).length;
|
|
2086
|
+
return n > mx ? n : mx;
|
|
2087
|
+
}, 0);
|
|
2088
|
+
if (maxCells > colCount) {
|
|
2089
|
+
const extra = Array(maxCells - colCount)
|
|
2090
|
+
.fill('\t\t<TablixColumn><Width>1in</Width></TablixColumn>')
|
|
2091
|
+
.join('\n');
|
|
2092
|
+
result = result.replace('</TablixColumns>', `\n${extra}\n\t\t</TablixColumns>`);
|
|
2093
|
+
changed = true;
|
|
2094
|
+
}
|
|
2095
|
+
return result;
|
|
2096
|
+
};
|
|
2097
|
+
// Depth-tracking scan for each top-level <Tablix>…</Tablix> block.
|
|
2098
|
+
// Child elements (<TablixBody>, <TablixColumns>, <TablixCell>, …) all have
|
|
2099
|
+
// letters immediately after '<Tablix', so the main element is identified by
|
|
2100
|
+
// '<Tablix' followed by '>', ' ', tab, or newline.
|
|
2101
|
+
let pos = 0;
|
|
2102
|
+
while (pos < fixedRdl.length) {
|
|
2103
|
+
const tagStart = fixedRdl.indexOf('<Tablix', pos);
|
|
2104
|
+
if (tagStart < 0)
|
|
2105
|
+
break;
|
|
2106
|
+
const ch = fixedRdl[tagStart + 7];
|
|
2107
|
+
if (ch !== '>' && ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r') {
|
|
2108
|
+
pos = tagStart + 8;
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
// Self-closing <Tablix /> — skip
|
|
2112
|
+
const tagEndIdx = fixedRdl.indexOf('>', tagStart);
|
|
2113
|
+
if (tagEndIdx < 0)
|
|
2114
|
+
break;
|
|
2115
|
+
if (fixedRdl[tagEndIdx - 1] === '/') {
|
|
2116
|
+
pos = tagEndIdx + 1;
|
|
2117
|
+
continue;
|
|
2118
|
+
}
|
|
2119
|
+
// Depth-scan for matching </Tablix>
|
|
2120
|
+
let depth = 1;
|
|
2121
|
+
let scan = tagEndIdx + 1;
|
|
2122
|
+
let closeTagPos = -1;
|
|
2123
|
+
while (depth > 0 && scan < fixedRdl.length) {
|
|
2124
|
+
const nxtOpen = fixedRdl.indexOf('<Tablix', scan);
|
|
2125
|
+
const nxtClose = fixedRdl.indexOf('</Tablix>', scan);
|
|
2126
|
+
if (nxtClose < 0)
|
|
2127
|
+
break;
|
|
2128
|
+
if (nxtOpen >= 0 && nxtOpen < nxtClose) {
|
|
2129
|
+
const nc = fixedRdl[nxtOpen + 7];
|
|
2130
|
+
if (nc === '>' || nc === ' ' || nc === '\t' || nc === '\n' || nc === '\r') {
|
|
2131
|
+
depth++;
|
|
2132
|
+
const innerTagEnd = fixedRdl.indexOf('>', nxtOpen);
|
|
2133
|
+
scan = innerTagEnd >= 0 ? innerTagEnd + 1 : nxtOpen + 8;
|
|
2134
|
+
}
|
|
2135
|
+
else {
|
|
2136
|
+
scan = nxtOpen + 8;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
else {
|
|
2140
|
+
depth--;
|
|
2141
|
+
if (depth === 0)
|
|
2142
|
+
closeTagPos = nxtClose;
|
|
2143
|
+
scan = nxtClose + 9; // '</Tablix>'.length === 9
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
if (closeTagPos < 0)
|
|
2147
|
+
break;
|
|
2148
|
+
const blockEnd = closeTagPos + 9;
|
|
2149
|
+
const block = fixedRdl.substring(tagStart, blockEnd);
|
|
2150
|
+
const fixed = processTablix(block);
|
|
2151
|
+
if (fixed !== block) {
|
|
2152
|
+
fixedRdl = fixedRdl.substring(0, tagStart) + fixed + fixedRdl.substring(blockEnd);
|
|
2153
|
+
pos = tagStart + fixed.length;
|
|
2154
|
+
}
|
|
2155
|
+
else {
|
|
2156
|
+
pos = blockEnd;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
if (!changed)
|
|
2160
|
+
return _whole;
|
|
2161
|
+
console.error('[sanitizeReportXml] Reconciled TablixCell count with TablixColumn count in embedded RDL');
|
|
2162
|
+
return open + fixedRdl + close;
|
|
2163
|
+
});
|
|
2164
|
+
return xml;
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Convert <Text><![CDATA[…RDL…]]></Text> to XML entity-encoded form.
|
|
2168
|
+
*
|
|
2169
|
+
* D365FO stores and expects the embedded RDL as entity-encoded text, not CDATA:
|
|
2170
|
+
* <Text><?xml version="1.0"?><Report ...>...</Report></Text>
|
|
2171
|
+
*
|
|
2172
|
+
* CDATA is valid XML and semantically equivalent, but the VS Designer metadata loader
|
|
2173
|
+
* does not render <Designs> correctly when the <Text> value uses CDATA — the design
|
|
2174
|
+
* appears empty even though no parse error is raised. Using entity encoding matches
|
|
2175
|
+
* what VS writes natively and fixes the empty-design issue.
|
|
2176
|
+
*
|
|
2177
|
+
* This is a SEPARATE method from sanitizeReportXml intentionally:
|
|
2178
|
+
* - sanitizeReportXml operates on CDATA form (efficient regex over raw XML text)
|
|
2179
|
+
* - encodeReportTextElement runs AFTER sanitize, just before writing to disk
|
|
2180
|
+
*/
|
|
2181
|
+
static encodeReportTextElement(xml) {
|
|
2182
|
+
return xml.replace(/<Text><!\[CDATA\[([\s\S]*?)\]\]><\/Text>/g, (_match, rdlInner) => {
|
|
2183
|
+
const encoded = rdlInner
|
|
2184
|
+
.replace(/&/g, '&') // must be first to avoid double-encoding
|
|
2185
|
+
.replace(/</g, '<')
|
|
2186
|
+
.replace(/>/g, '>');
|
|
2187
|
+
return `<Text>${encoded}</Text>`;
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Generate AxEdt XML (Extended Data Type).
|
|
2192
|
+
* Default i:type is AxEdtString; override via properties.edtType.
|
|
2193
|
+
* Accepts either the full AxEdt* form or a plain base-type name
|
|
2194
|
+
* (string → AxEdtString, integer/int → AxEdtInt, int64 → AxEdtInt64,
|
|
2195
|
+
* real → AxEdtReal, date → AxEdtDate, datetime/utcdatetime → AxEdtUtcDateTime,
|
|
2196
|
+
* enum → AxEdtEnum, guid → AxEdtGuid, container → AxEdtContainer).
|
|
2197
|
+
*/
|
|
2198
|
+
static generateAxEdtXml(name, properties) {
|
|
2199
|
+
const edtTypeRaw = properties?.edtType || 'AxEdtString';
|
|
2200
|
+
const edtTypeNormMap = {
|
|
2201
|
+
string: 'AxEdtString',
|
|
2202
|
+
integer: 'AxEdtInt',
|
|
2203
|
+
int: 'AxEdtInt',
|
|
2204
|
+
int64: 'AxEdtInt64',
|
|
2205
|
+
real: 'AxEdtReal',
|
|
2206
|
+
date: 'AxEdtDate',
|
|
2207
|
+
datetime: 'AxEdtUtcDateTime',
|
|
2208
|
+
utcdatetime: 'AxEdtUtcDateTime',
|
|
2209
|
+
enum: 'AxEdtEnum',
|
|
2210
|
+
guid: 'AxEdtGuid',
|
|
2211
|
+
container: 'AxEdtContainer',
|
|
2212
|
+
};
|
|
2213
|
+
const edtType = edtTypeNormMap[edtTypeRaw.toLowerCase()] ?? edtTypeRaw;
|
|
2214
|
+
const label = properties?.label || '@TODO:LabelId';
|
|
2215
|
+
const extends_ = properties?.extends ? `\n\t<Extends>${properties.extends}</Extends>` : '';
|
|
2216
|
+
const stringSize = edtType === 'AxEdtString'
|
|
2217
|
+
? `\n\t<StringSize>${properties?.stringSize ?? 30}</StringSize>` : '';
|
|
2218
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2219
|
+
<AxEdt xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns=""
|
|
2220
|
+
\ti:type="${edtType}">
|
|
2221
|
+
\t<Name>${name}</Name>
|
|
2222
|
+
\t<Label>${label}</Label>${extends_}
|
|
2223
|
+
\t<ArrayElements />
|
|
2224
|
+
\t<Relations />
|
|
2225
|
+
\t<TableReferences />${stringSize}
|
|
2226
|
+
</AxEdt>`;
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Generate a minimal extension XML for AxEdtExtension,
|
|
2230
|
+
* AxDataEntityViewExtension, AxMenuItemDisplayExtension, AxMenuItemActionExtension,
|
|
2231
|
+
* AxMenuItemOutputExtension.
|
|
2232
|
+
* Name convention: BaseObjectName.ExtensionName (e.g. CustTable.MyExtension)
|
|
2233
|
+
*/
|
|
2234
|
+
static generateAxSimpleExtensionXml(rootElement, name) {
|
|
2235
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2236
|
+
<${rootElement} xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
2237
|
+
\t<Name>${name}</Name>
|
|
2238
|
+
\t<PropertyModifications />
|
|
2239
|
+
</${rootElement}>`;
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Generate AxEnumExtension XML.
|
|
2243
|
+
* Name convention: BaseEnumName.PrefixExtension
|
|
2244
|
+
*
|
|
2245
|
+
* Supported properties:
|
|
2246
|
+
* enumValues: Array<{ name, label?, value?, countryRegionCodes?, helpText? }>
|
|
2247
|
+
*/
|
|
2248
|
+
static generateAxEnumExtensionXml(name, properties) {
|
|
2249
|
+
// Build <EnumValues> block
|
|
2250
|
+
const enumValueSpecs = Array.isArray(properties?.enumValues) ? properties.enumValues : [];
|
|
2251
|
+
let enumValuesXml;
|
|
2252
|
+
if (enumValueSpecs.length === 0) {
|
|
2253
|
+
enumValuesXml = '\t<EnumValues />';
|
|
2254
|
+
}
|
|
2255
|
+
else {
|
|
2256
|
+
enumValuesXml = '\t<EnumValues>';
|
|
2257
|
+
for (const v of enumValueSpecs) {
|
|
2258
|
+
enumValuesXml += `\n\t\t<AxEnumValue>`;
|
|
2259
|
+
enumValuesXml += `\n\t\t\t<Name>${v.name}</Name>`;
|
|
2260
|
+
if (v.countryRegionCodes)
|
|
2261
|
+
enumValuesXml += `\n\t\t\t<CountryRegionCodes>${v.countryRegionCodes}</CountryRegionCodes>`;
|
|
2262
|
+
if (v.label)
|
|
2263
|
+
enumValuesXml += `\n\t\t\t<Label>${v.label}</Label>`;
|
|
2264
|
+
if (v.helpText)
|
|
2265
|
+
enumValuesXml += `\n\t\t\t<HelpText>${v.helpText}</HelpText>`;
|
|
2266
|
+
if (v.value !== undefined && v.value !== 0)
|
|
2267
|
+
enumValuesXml += `\n\t\t\t<Value>${v.value}</Value>`;
|
|
2268
|
+
enumValuesXml += `\n\t\t</AxEnumValue>`;
|
|
2269
|
+
}
|
|
2270
|
+
enumValuesXml += '\n\t</EnumValues>';
|
|
2271
|
+
}
|
|
2272
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2273
|
+
<AxEnumExtension xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
2274
|
+
\t<Name>${name}</Name>
|
|
2275
|
+
${enumValuesXml}
|
|
2276
|
+
\t<PropertyModifications />
|
|
2277
|
+
\t<ValueModifications />
|
|
2278
|
+
</AxEnumExtension>`;
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* Generate AxTableExtension XML.
|
|
2282
|
+
* Name convention: TableName.PrefixExtension
|
|
2283
|
+
*
|
|
2284
|
+
* Supported properties:
|
|
2285
|
+
* fields: Array<{ name, edt?, enumType?, label?, mandatory?, fieldType? }>
|
|
2286
|
+
* fieldGroups: Array<{ name, label?, fields?: string[] }>
|
|
2287
|
+
* fieldGroupExtensions: Array<{ name, fields: string[] }> — extend base-table field groups
|
|
2288
|
+
* indexes: Array<{ name, fields: Array<{fieldName, direction?}>, allowDuplicates?, alternateKey? }>
|
|
2289
|
+
* relations: Array<{ name, relatedTable, constraints: Array<{fieldName, relatedFieldName}>,
|
|
2290
|
+
* cardinality?, relatedTableCardinality?, relationshipType? }>
|
|
2291
|
+
*/
|
|
2292
|
+
static generateAxTableExtensionXml(name, properties) {
|
|
2293
|
+
// ── Fields ───────────────────────────────────────────────────────────────
|
|
2294
|
+
const fieldSpecs = Array.isArray(properties?.fields) ? properties.fields : [];
|
|
2295
|
+
let fieldsXml;
|
|
2296
|
+
if (fieldSpecs.length === 0) {
|
|
2297
|
+
fieldsXml = '\t<Fields />';
|
|
2298
|
+
}
|
|
2299
|
+
else {
|
|
2300
|
+
fieldsXml = '\t<Fields>\n';
|
|
2301
|
+
for (const f of fieldSpecs) {
|
|
2302
|
+
const iType = f.fieldType ?? (f.enumType ? 'AxTableFieldEnum' : 'AxTableFieldString');
|
|
2303
|
+
fieldsXml += `\t\t<AxTableField xmlns=""\n\t\t\ti:type="${iType}">\n`;
|
|
2304
|
+
fieldsXml += `\t\t\t<Name>${f.name}</Name>\n`;
|
|
2305
|
+
if (f.edt)
|
|
2306
|
+
fieldsXml += `\t\t\t<ExtendedDataType>${f.edt}</ExtendedDataType>\n`;
|
|
2307
|
+
if (f.label)
|
|
2308
|
+
fieldsXml += `\t\t\t<Label>${f.label}</Label>\n`;
|
|
2309
|
+
if (f.mandatory)
|
|
2310
|
+
fieldsXml += `\t\t\t<Mandatory>Yes</Mandatory>\n`;
|
|
2311
|
+
if (f.enumType)
|
|
2312
|
+
fieldsXml += `\t\t\t<EnumType>${f.enumType}</EnumType>\n`;
|
|
2313
|
+
fieldsXml += `\t\t</AxTableField>\n`;
|
|
2314
|
+
}
|
|
2315
|
+
fieldsXml += '\t</Fields>';
|
|
2316
|
+
}
|
|
2317
|
+
// ── FieldGroups (new groups defined in this extension) ───────────────────
|
|
2318
|
+
const fgSpecs = Array.isArray(properties?.fieldGroups) ? properties.fieldGroups : [];
|
|
2319
|
+
let fieldGroupsXml;
|
|
2320
|
+
if (fgSpecs.length === 0) {
|
|
2321
|
+
fieldGroupsXml = '\t<FieldGroups />';
|
|
2322
|
+
}
|
|
2323
|
+
else {
|
|
2324
|
+
fieldGroupsXml = '\t<FieldGroups>\n';
|
|
2325
|
+
for (const fg of fgSpecs) {
|
|
2326
|
+
fieldGroupsXml += `\t\t<AxTableFieldGroup>\n\t\t\t<Name>${fg.name}</Name>\n`;
|
|
2327
|
+
if (fg.label)
|
|
2328
|
+
fieldGroupsXml += `\t\t\t<Label>${fg.label}</Label>\n`;
|
|
2329
|
+
const fgFields = Array.isArray(fg.fields) ? fg.fields : [];
|
|
2330
|
+
if (fgFields.length === 0) {
|
|
2331
|
+
fieldGroupsXml += `\t\t\t<Fields />\n`;
|
|
2332
|
+
}
|
|
2333
|
+
else {
|
|
2334
|
+
fieldGroupsXml += `\t\t\t<Fields>\n`;
|
|
2335
|
+
for (const df of fgFields) {
|
|
2336
|
+
fieldGroupsXml += `\t\t\t\t<AxTableFieldGroupField>\n\t\t\t\t\t<DataField>${df}</DataField>\n\t\t\t\t</AxTableFieldGroupField>\n`;
|
|
2337
|
+
}
|
|
2338
|
+
fieldGroupsXml += `\t\t\t</Fields>\n`;
|
|
2339
|
+
}
|
|
2340
|
+
fieldGroupsXml += `\t\t</AxTableFieldGroup>\n`;
|
|
2341
|
+
}
|
|
2342
|
+
fieldGroupsXml += '\t</FieldGroups>';
|
|
2343
|
+
}
|
|
2344
|
+
// ── FieldGroupExtensions (extend base-table field groups) ────────────────
|
|
2345
|
+
const fgeSpecs = Array.isArray(properties?.fieldGroupExtensions) ? properties.fieldGroupExtensions : [];
|
|
2346
|
+
let fieldGroupExtensionsXml;
|
|
2347
|
+
if (fgeSpecs.length === 0) {
|
|
2348
|
+
fieldGroupExtensionsXml = '\t<FieldGroupExtensions />';
|
|
2349
|
+
}
|
|
2350
|
+
else {
|
|
2351
|
+
fieldGroupExtensionsXml = '\t<FieldGroupExtensions>\n';
|
|
2352
|
+
for (const fge of fgeSpecs) {
|
|
2353
|
+
fieldGroupExtensionsXml += `\t\t<AxTableFieldGroupExtension>\n\t\t\t<Name>${fge.name}</Name>\n`;
|
|
2354
|
+
const fgeFields = Array.isArray(fge.fields) ? fge.fields : [];
|
|
2355
|
+
if (fgeFields.length === 0) {
|
|
2356
|
+
fieldGroupExtensionsXml += `\t\t\t<Fields />\n`;
|
|
2357
|
+
}
|
|
2358
|
+
else {
|
|
2359
|
+
fieldGroupExtensionsXml += `\t\t\t<Fields>\n`;
|
|
2360
|
+
for (const df of fgeFields) {
|
|
2361
|
+
fieldGroupExtensionsXml += `\t\t\t\t<AxTableFieldGroupField>\n\t\t\t\t\t<DataField>${df}</DataField>\n\t\t\t\t</AxTableFieldGroupField>\n`;
|
|
2362
|
+
}
|
|
2363
|
+
fieldGroupExtensionsXml += `\t\t\t</Fields>\n`;
|
|
2364
|
+
}
|
|
2365
|
+
fieldGroupExtensionsXml += `\t\t</AxTableFieldGroupExtension>\n`;
|
|
2366
|
+
}
|
|
2367
|
+
fieldGroupExtensionsXml += '\t</FieldGroupExtensions>';
|
|
2368
|
+
}
|
|
2369
|
+
// ── Indexes ──────────────────────────────────────────────────────────────
|
|
2370
|
+
const idxSpecs = Array.isArray(properties?.indexes) ? properties.indexes : [];
|
|
2371
|
+
let indexesXml;
|
|
2372
|
+
if (idxSpecs.length === 0) {
|
|
2373
|
+
indexesXml = '\t<Indexes />';
|
|
2374
|
+
}
|
|
2375
|
+
else {
|
|
2376
|
+
indexesXml = '\t<Indexes>\n';
|
|
2377
|
+
for (const idx of idxSpecs) {
|
|
2378
|
+
indexesXml += `\t\t<AxTableIndex>\n\t\t\t<Name>${idx.name}</Name>\n`;
|
|
2379
|
+
if (idx.allowDuplicates !== undefined)
|
|
2380
|
+
indexesXml += `\t\t\t<AllowDuplicates>${idx.allowDuplicates ? 'Yes' : 'No'}</AllowDuplicates>\n`;
|
|
2381
|
+
if (idx.alternateKey)
|
|
2382
|
+
indexesXml += `\t\t\t<AlternateKey>Yes</AlternateKey>\n`;
|
|
2383
|
+
const idxFields = Array.isArray(idx.fields) ? idx.fields : [];
|
|
2384
|
+
if (idxFields.length === 0) {
|
|
2385
|
+
indexesXml += `\t\t\t<Fields />\n`;
|
|
2386
|
+
}
|
|
2387
|
+
else {
|
|
2388
|
+
indexesXml += `\t\t\t<Fields>\n`;
|
|
2389
|
+
for (const f of idxFields) {
|
|
2390
|
+
indexesXml += `\t\t\t\t<AxTableIndexField>\n\t\t\t\t\t<DataField>${f.fieldName}</DataField>\n`;
|
|
2391
|
+
if (f.direction)
|
|
2392
|
+
indexesXml += `\t\t\t\t\t<Direction>${f.direction}</Direction>\n`;
|
|
2393
|
+
indexesXml += `\t\t\t\t</AxTableIndexField>\n`;
|
|
2394
|
+
}
|
|
2395
|
+
indexesXml += `\t\t\t</Fields>\n`;
|
|
2396
|
+
}
|
|
2397
|
+
indexesXml += `\t\t</AxTableIndex>\n`;
|
|
2398
|
+
}
|
|
2399
|
+
indexesXml += '\t</Indexes>';
|
|
2400
|
+
}
|
|
2401
|
+
// ── Relations ────────────────────────────────────────────────────────────
|
|
2402
|
+
const relSpecs = Array.isArray(properties?.relations) ? properties.relations : [];
|
|
2403
|
+
let relationsXml;
|
|
2404
|
+
if (relSpecs.length === 0) {
|
|
2405
|
+
relationsXml = '\t<Relations />';
|
|
2406
|
+
}
|
|
2407
|
+
else {
|
|
2408
|
+
relationsXml = '\t<Relations>\n';
|
|
2409
|
+
for (const rel of relSpecs) {
|
|
2410
|
+
relationsXml += `\t\t<AxTableRelation>\n`;
|
|
2411
|
+
relationsXml += `\t\t\t<Name>${rel.name}</Name>\n`;
|
|
2412
|
+
relationsXml += `\t\t\t<Cardinality>${rel.cardinality || 'ZeroMore'}</Cardinality>\n`;
|
|
2413
|
+
relationsXml += `\t\t\t<RelatedTable>${rel.relatedTable}</RelatedTable>\n`;
|
|
2414
|
+
relationsXml += `\t\t\t<RelatedTableCardinality>${rel.relatedTableCardinality || 'ExactlyOne'}</RelatedTableCardinality>\n`;
|
|
2415
|
+
relationsXml += `\t\t\t<RelationshipType>${rel.relationshipType || 'Association'}</RelationshipType>\n`;
|
|
2416
|
+
const constraints = Array.isArray(rel.constraints) ? rel.constraints : [];
|
|
2417
|
+
if (constraints.length === 0) {
|
|
2418
|
+
relationsXml += `\t\t\t<Constraints />\n`;
|
|
2419
|
+
}
|
|
2420
|
+
else {
|
|
2421
|
+
relationsXml += `\t\t\t<Constraints>\n`;
|
|
2422
|
+
for (const c of constraints) {
|
|
2423
|
+
relationsXml += `\t\t\t\t<AxTableRelationConstraint xmlns="" i:type="AxTableRelationConstraintField">\n`;
|
|
2424
|
+
relationsXml += `\t\t\t\t\t<Name>${c.fieldName}</Name>\n`;
|
|
2425
|
+
relationsXml += `\t\t\t\t\t<Field>${c.fieldName}</Field>\n`;
|
|
2426
|
+
relationsXml += `\t\t\t\t\t<RelatedField>${c.relatedFieldName}</RelatedField>\n`;
|
|
2427
|
+
relationsXml += `\t\t\t\t</AxTableRelationConstraint>\n`;
|
|
2428
|
+
}
|
|
2429
|
+
relationsXml += `\t\t\t</Constraints>\n`;
|
|
2430
|
+
}
|
|
2431
|
+
relationsXml += `\t\t</AxTableRelation>\n`;
|
|
2432
|
+
}
|
|
2433
|
+
relationsXml += '\t</Relations>';
|
|
2434
|
+
}
|
|
2435
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2436
|
+
<AxTableExtension xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
2437
|
+
\t<Name>${name}</Name>
|
|
2438
|
+
${fieldGroupExtensionsXml}
|
|
2439
|
+
${fieldGroupsXml}
|
|
2440
|
+
\t<FieldModifications />
|
|
2441
|
+
${fieldsXml}
|
|
2442
|
+
\t<FullTextIndexes />
|
|
2443
|
+
${indexesXml}
|
|
2444
|
+
\t<Mappings />
|
|
2445
|
+
\t<PropertyModifications />
|
|
2446
|
+
\t<RelationExtensions />
|
|
2447
|
+
\t<RelationModifications />
|
|
2448
|
+
${relationsXml}
|
|
2449
|
+
</AxTableExtension>`;
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Generate AxFormExtension XML.
|
|
2453
|
+
* Name convention: FormName.ExtensionName
|
|
2454
|
+
*/
|
|
2455
|
+
static generateAxFormExtensionXml(name) {
|
|
2456
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2457
|
+
<AxFormExtension xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="Microsoft.Dynamics.AX.Metadata.V6">
|
|
2458
|
+
\t<Name>${name}</Name>
|
|
2459
|
+
\t<ControlModifications />
|
|
2460
|
+
\t<Controls />
|
|
2461
|
+
\t<DataSourceModifications />
|
|
2462
|
+
\t<DataSourceReferences />
|
|
2463
|
+
\t<DataSources />
|
|
2464
|
+
\t<Parts />
|
|
2465
|
+
\t<PropertyModifications />
|
|
2466
|
+
</AxFormExtension>`;
|
|
2467
|
+
}
|
|
2468
|
+
/**
|
|
2469
|
+
* Generate AxSecurityPrivilege XML. Delegates to the shared builder so this
|
|
2470
|
+
* mirror and the one in generateD365Xml.ts cannot drift.
|
|
2471
|
+
* @see buildAxSecurityPrivilegeXml for the property contract and element order.
|
|
2472
|
+
*/
|
|
2473
|
+
static generateAxSecurityPrivilegeXml(name, properties) {
|
|
2474
|
+
return buildAxSecurityPrivilegeXml(name, properties);
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Normalize a name list that may arrive as an array or a comma/semicolon/
|
|
2478
|
+
* newline-separated string (models pass either). Returns trimmed, non-empty names.
|
|
2479
|
+
*/
|
|
2480
|
+
static normalizeNameList(value) {
|
|
2481
|
+
if (!value)
|
|
2482
|
+
return [];
|
|
2483
|
+
const arr = Array.isArray(value) ? value : String(value).split(/[,;\n]+/);
|
|
2484
|
+
return arr.map((s) => String(s).trim()).filter((s) => s.length > 0);
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Render a security reference container: a self-closing tag when empty, or the
|
|
2488
|
+
* wrapped child references (e.g. <AxSecurityRolePermissionSet><Name>…</Name></…>).
|
|
2489
|
+
*/
|
|
2490
|
+
static securityRefContainer(container, childTag, names) {
|
|
2491
|
+
if (names.length === 0)
|
|
2492
|
+
return `\t<${container} />`;
|
|
2493
|
+
const children = names
|
|
2494
|
+
.map(n => `\t\t<${childTag}>\n\t\t\t<Name>${n}</Name>\n\t\t</${childTag}>`)
|
|
2495
|
+
.join('\n');
|
|
2496
|
+
return `\t<${container}>\n${children}\n\t</${container}>`;
|
|
2497
|
+
}
|
|
2498
|
+
/**
|
|
2499
|
+
* Generate AxSecurityDuty XML.
|
|
2500
|
+
* properties.privileges – privilege names to reference (array or comma-separated).
|
|
2501
|
+
*/
|
|
2502
|
+
static generateAxSecurityDutyXml(name, properties) {
|
|
2503
|
+
const label = properties?.label || '@TODO:LabelId';
|
|
2504
|
+
const privileges = this.normalizeNameList(properties?.privileges);
|
|
2505
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2506
|
+
<AxSecurityDuty xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
2507
|
+
\t<Name>${name}</Name>
|
|
2508
|
+
\t<Label>${label}</Label>
|
|
2509
|
+
${this.securityRefContainer('Privileges', 'AxSecurityRolePermissionSet', privileges)}
|
|
2510
|
+
</AxSecurityDuty>`;
|
|
2511
|
+
}
|
|
2512
|
+
/**
|
|
2513
|
+
* Generate AxSecurityRole XML.
|
|
2514
|
+
* properties.duties – duty names to reference (array or comma-separated).
|
|
2515
|
+
* properties.privileges – privilege names to reference directly on the role.
|
|
2516
|
+
*/
|
|
2517
|
+
static generateAxSecurityRoleXml(name, properties) {
|
|
2518
|
+
const label = properties?.label || '@TODO:LabelId';
|
|
2519
|
+
const duties = this.normalizeNameList(properties?.duties);
|
|
2520
|
+
const privileges = this.normalizeNameList(properties?.privileges);
|
|
2521
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2522
|
+
<AxSecurityRole xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
2523
|
+
\t<Name>${name}</Name>
|
|
2524
|
+
\t<Label>${label}</Label>
|
|
2525
|
+
\t<DirectAccessPermissions />
|
|
2526
|
+
${this.securityRefContainer('Duties', 'AxSecurityRoleDutyPermission', duties)}
|
|
2527
|
+
${this.securityRefContainer('Privileges', 'AxSecurityRolePermissionSet', privileges)}
|
|
2528
|
+
\t<SubRoles />
|
|
2529
|
+
</AxSecurityRole>`;
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Generate AxSecurityDutyExtension XML — adds privileges to an EXISTING (often
|
|
2533
|
+
* Microsoft-owned) duty without overlaying it. Real Microsoft object type, e.g.
|
|
2534
|
+
* K:\...\ApplicationCommon\AxSecurityDutyExtension\BatchJobMaintain.ApplicationCommon.xml.
|
|
2535
|
+
* Name convention: "<BaseDuty>.<PrefixOrModel>Extension" (same dot-notation as
|
|
2536
|
+
* menu-extension / table-extension — see DOT_NOTATION_EXTENSION_TYPES).
|
|
2537
|
+
* properties.privileges – privilege names to add to the base duty (array or comma-separated).
|
|
2538
|
+
*/
|
|
2539
|
+
static generateAxSecurityDutyExtensionXml(name, properties) {
|
|
2540
|
+
const privileges = this.normalizeNameList(properties?.privileges);
|
|
2541
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2542
|
+
<AxSecurityDutyExtension xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
2543
|
+
\t<Name>${name}</Name>
|
|
2544
|
+
${this.securityRefContainer('Privileges', 'AxSecurityPrivilegeReference', privileges)}
|
|
2545
|
+
\t<PropertyModifications />
|
|
2546
|
+
</AxSecurityDutyExtension>`;
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Generate AxSecurityRoleExtension XML — adds duties and/or privileges to an
|
|
2550
|
+
* EXISTING (often Microsoft-owned) role without overlaying it. Real Microsoft
|
|
2551
|
+
* object type, e.g. K:\...\ApplicationCommon\AxSecurityRoleExtension\SystemUser.ApplicationCommon.xml.
|
|
2552
|
+
* Name convention: "<BaseRole>.<PrefixOrModel>Extension".
|
|
2553
|
+
* properties.duties – duty names to add to the base role (array or comma-separated).
|
|
2554
|
+
* properties.privileges – privilege names to add directly to the base role.
|
|
2555
|
+
*/
|
|
2556
|
+
static generateAxSecurityRoleExtensionXml(name, properties) {
|
|
2557
|
+
const duties = this.normalizeNameList(properties?.duties);
|
|
2558
|
+
const privileges = this.normalizeNameList(properties?.privileges);
|
|
2559
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2560
|
+
<AxSecurityRoleExtension xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
2561
|
+
\t<Name>${name}</Name>
|
|
2562
|
+
\t<DirectAccessPermissions />
|
|
2563
|
+
${this.securityRefContainer('Duties', 'AxSecurityDutyReference', duties)}
|
|
2564
|
+
${this.securityRefContainer('Privileges', 'AxSecurityPrivilegeReference', privileges)}
|
|
2565
|
+
\t<PropertyModifications />
|
|
2566
|
+
</AxSecurityRoleExtension>`;
|
|
2567
|
+
}
|
|
2568
|
+
/**
|
|
2569
|
+
* Generate BusinessEventsContract class XML (AxClass) for a Business Event.
|
|
2570
|
+
* The class extends BusinessEventsBase and includes a companion contract class.
|
|
2571
|
+
*/
|
|
2572
|
+
static generateBusinessEventXml(name, properties) {
|
|
2573
|
+
const label = properties?.label || `@TODO:${name}Label`;
|
|
2574
|
+
const helpText = properties?.helpText || `@TODO:${name}HelpText`;
|
|
2575
|
+
const module = properties?.module || 'ModuleAxapta::Other';
|
|
2576
|
+
const contractName = `${name}Contract`;
|
|
2577
|
+
const source = `[BusinessEvents(classStr(${contractName}),
|
|
2578
|
+
'${name}',
|
|
2579
|
+
'${name}',
|
|
2580
|
+
${module})]
|
|
2581
|
+
public final class ${name} extends BusinessEventsBase
|
|
2582
|
+
{
|
|
2583
|
+
private ${contractName} contract;
|
|
2584
|
+
|
|
2585
|
+
public static ${name} newFromContract(${contractName} _contract)
|
|
2586
|
+
{
|
|
2587
|
+
${name} event = new ${name}();
|
|
2588
|
+
event.contract = _contract;
|
|
2589
|
+
return event;
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
[Hookable(false)]
|
|
2593
|
+
public BusinessEventsContract buildContract()
|
|
2594
|
+
{
|
|
2595
|
+
return contract;
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// ── Contract class ──────────────────────────────────────────────────────────
|
|
2600
|
+
[DataContractAttribute]
|
|
2601
|
+
public final class ${contractName} extends BusinessEventsContract
|
|
2602
|
+
{
|
|
2603
|
+
// TODO: add private fields and parmXxx() methods for the event payload
|
|
2604
|
+
|
|
2605
|
+
public static ${contractName} newDefault()
|
|
2606
|
+
{
|
|
2607
|
+
${contractName} c = new ${contractName}();
|
|
2608
|
+
return c;
|
|
2609
|
+
}
|
|
2610
|
+
}`;
|
|
2611
|
+
return XmlTemplateGenerator.generateAxClassXml(name, source, { label, helpText });
|
|
2612
|
+
}
|
|
2613
|
+
/**
|
|
2614
|
+
* Generate Workspace Tile XML (AxTile).
|
|
2615
|
+
* Tiles appear in workspace panorama sections as KPI / navigation tiles.
|
|
2616
|
+
*/
|
|
2617
|
+
static generateAxTileXml(name, properties) {
|
|
2618
|
+
const label = properties?.label || `@TODO:${name}Label`;
|
|
2619
|
+
const helpText = properties?.helpText || `@TODO:${name}HelpText`;
|
|
2620
|
+
const tileType = properties?.tileType || 'Count'; // Count | Link | Summary
|
|
2621
|
+
const menuItem = properties?.menuItem || '';
|
|
2622
|
+
const query = properties?.query || '';
|
|
2623
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2624
|
+
<AxTile xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="Microsoft.Dynamics.AX.Metadata.V6">
|
|
2625
|
+
\t<Name>${name}</Name>
|
|
2626
|
+
\t<Label>${label}</Label>
|
|
2627
|
+
\t<HelpText>${helpText}</HelpText>
|
|
2628
|
+
\t<TileType>${tileType}</TileType>${menuItem ? `\n\t<MenuItemName>${menuItem}</MenuItemName>\n\t<MenuItemType>Display</MenuItemType>` : ''}${query ? `\n\t<Query>${query}</Query>` : ''}
|
|
2629
|
+
\t<Size>Wide</Size>
|
|
2630
|
+
\t<RefreshFrequency>600</RefreshFrequency>
|
|
2631
|
+
</AxTile>`;
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* Generate KPI XML (AxKPI).
|
|
2635
|
+
* KPIs appear in workspace summary sections.
|
|
2636
|
+
*/
|
|
2637
|
+
static generateAxKpiXml(name, properties) {
|
|
2638
|
+
const label = properties?.label || `@TODO:${name}Label`;
|
|
2639
|
+
const helpText = properties?.helpText || `@TODO:${name}HelpText`;
|
|
2640
|
+
const measure = properties?.measure || '';
|
|
2641
|
+
const dimension = properties?.dimension || '';
|
|
2642
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2643
|
+
<AxKPI xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="Microsoft.Dynamics.AX.Metadata.V6">
|
|
2644
|
+
\t<Name>${name}</Name>
|
|
2645
|
+
\t<Label>${label}</Label>
|
|
2646
|
+
\t<HelpText>${helpText}</HelpText>${measure ? `\n\t<Measure>${measure}</Measure>` : ''}${dimension ? `\n\t<MeasureDimension>${dimension}</MeasureDimension>` : ''}
|
|
2647
|
+
\t<Goal>0</Goal>
|
|
2648
|
+
\t<GoalType>None</GoalType>
|
|
2649
|
+
\t<Trend>None</Trend>
|
|
2650
|
+
</AxKPI>`;
|
|
2651
|
+
}
|
|
2652
|
+
/**
|
|
2653
|
+
* Generate AxMenu XML.
|
|
2654
|
+
*/
|
|
2655
|
+
static generateAxMenuXml(name, properties) {
|
|
2656
|
+
const label = properties?.label || '@TODO:LabelId';
|
|
2657
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2658
|
+
<AxMenu xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="Microsoft.Dynamics.AX.Metadata.V1">
|
|
2659
|
+
\t<Name>${name}</Name>
|
|
2660
|
+
\t<Label>${label}</Label>
|
|
2661
|
+
\t<Elements />
|
|
2662
|
+
</AxMenu>`;
|
|
2663
|
+
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Generate AxMenuExtension XML.
|
|
2666
|
+
* Name convention: MenuName.ExtensionName
|
|
2667
|
+
*/
|
|
2668
|
+
static generateAxMenuExtensionXml(name) {
|
|
2669
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2670
|
+
<AxMenuExtension xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="Microsoft.Dynamics.AX.Metadata.V1">
|
|
2671
|
+
\t<Name>${name}</Name>
|
|
2672
|
+
\t<Customizations />
|
|
2673
|
+
\t<Elements />
|
|
2674
|
+
\t<MenuElementModifications />
|
|
2675
|
+
\t<PropertyModifications />
|
|
2676
|
+
</AxMenuExtension>`;
|
|
2677
|
+
}
|
|
2678
|
+
/**
|
|
2679
|
+
* Generate AxMenuItemDisplay / AxMenuItemAction / AxMenuItemOutput XML.
|
|
2680
|
+
*
|
|
2681
|
+
* AOT folder mapping:
|
|
2682
|
+
* menu-item-display → AxMenuItemDisplay (ObjectType: Form)
|
|
2683
|
+
* menu-item-action → AxMenuItemAction (ObjectType: Class)
|
|
2684
|
+
* menu-item-output → AxMenuItemOutput (ObjectType: Report)
|
|
2685
|
+
*/
|
|
2686
|
+
static generateAxMenuItemXml(itemType, name, properties) {
|
|
2687
|
+
const elemName = itemType === 'menu-item-action' ? 'AxMenuItemAction'
|
|
2688
|
+
: itemType === 'menu-item-output' ? 'AxMenuItemOutput'
|
|
2689
|
+
: 'AxMenuItemDisplay';
|
|
2690
|
+
const targetObject = properties?.targetObject || properties?.object || name;
|
|
2691
|
+
const label = properties?.label || '@TODO:LabelId';
|
|
2692
|
+
// Determine ObjectType based on item type and explicit properties.
|
|
2693
|
+
// D365FO serializer rules (confirmed from real XML files):
|
|
2694
|
+
// - AxMenuItemAction: ObjectType is always "Class"; must be present.
|
|
2695
|
+
// - AxMenuItemDisplay: ObjectType is OMITTED when targeting a Form (default);
|
|
2696
|
+
// use "Class" only when explicitly set.
|
|
2697
|
+
// - AxMenuItemOutput: ObjectType is "Class" (controller) or "SSRSReport";
|
|
2698
|
+
// "Report" is NOT a valid value — real files use "SSRSReport".
|
|
2699
|
+
const explicitObjType = properties?.objectType || properties?.targetType;
|
|
2700
|
+
let objType;
|
|
2701
|
+
if (itemType === 'menu-item-action') {
|
|
2702
|
+
// Action always needs ObjectType; default to Class
|
|
2703
|
+
objType = explicitObjType || 'Class';
|
|
2704
|
+
}
|
|
2705
|
+
else if (itemType === 'menu-item-output') {
|
|
2706
|
+
// Output: Class (controller pattern) or SSRSReport; "Report" is invalid
|
|
2707
|
+
if (explicitObjType === 'Report') {
|
|
2708
|
+
objType = 'SSRSReport';
|
|
2709
|
+
}
|
|
2710
|
+
else {
|
|
2711
|
+
objType = explicitObjType || 'Class';
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
else {
|
|
2715
|
+
// Display: omit ObjectType entirely when targeting a Form (the implicit default).
|
|
2716
|
+
// Include it only when caller explicitly requests "Class".
|
|
2717
|
+
if (explicitObjType && explicitObjType !== 'Form') {
|
|
2718
|
+
objType = explicitObjType;
|
|
2719
|
+
}
|
|
2720
|
+
// else leave objType undefined → element omitted
|
|
2721
|
+
}
|
|
2722
|
+
const objectTypeXml = objType ? `\n\t<ObjectType>${objType}</ObjectType>` : '';
|
|
2723
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
2724
|
+
<${elemName} xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="Microsoft.Dynamics.AX.Metadata.V1">
|
|
2725
|
+
\t<Name>${name}</Name>
|
|
2726
|
+
\t<Label>${label}</Label>
|
|
2727
|
+
\t<Object>${targetObject}</Object>${objectTypeXml}
|
|
2728
|
+
</${elemName}>`;
|
|
2729
|
+
}
|
|
2730
|
+
/**
|
|
2731
|
+
* Ensure AxMenuItemAction/Display/Output XML always has the required namespace
|
|
2732
|
+
* attributes on the root element. D365FO metadata deserializer rejects the file
|
|
2733
|
+
* without both:
|
|
2734
|
+
* xmlns="Microsoft.Dynamics.AX.Metadata.V1"
|
|
2735
|
+
* xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
|
|
2736
|
+
*
|
|
2737
|
+
* Also fix invalid ObjectType values:
|
|
2738
|
+
* "Form" → remove element entirely (display items targeting a form should
|
|
2739
|
+
* omit ObjectType; D365FO has no ObjectType enum value "Form")
|
|
2740
|
+
* "Report" → "SSRSReport" (only valid values are Class / SSRSReport)
|
|
2741
|
+
*/
|
|
2742
|
+
static sanitizeMenuItemXml(xml) {
|
|
2743
|
+
// 1. Ensure xmlns namespace attributes on root element
|
|
2744
|
+
xml = xml.replace(/<(AxMenuItem(?:Action|Display|Output))(\s[^>]*)?>/, (_match, tag, attrs) => {
|
|
2745
|
+
let a = attrs || '';
|
|
2746
|
+
if (!a.includes('xmlns="Microsoft.Dynamics.AX.Metadata.V1"')) {
|
|
2747
|
+
a += ' xmlns="Microsoft.Dynamics.AX.Metadata.V1"';
|
|
2748
|
+
}
|
|
2749
|
+
if (!a.includes('xmlns:i="')) {
|
|
2750
|
+
a = ` xmlns:i="http://www.w3.org/2001/XMLSchema-instance"` + a;
|
|
2751
|
+
}
|
|
2752
|
+
return `<${tag}${a}>`;
|
|
2753
|
+
});
|
|
2754
|
+
// 2. Fix invalid ObjectType value "Form" → remove element
|
|
2755
|
+
// Real AxMenuItemDisplay files targeting forms simply omit ObjectType.
|
|
2756
|
+
xml = xml.replace(/\s*<ObjectType>Form<\/ObjectType>/g, '');
|
|
2757
|
+
// 3. Fix invalid ObjectType value "Report" → "SSRSReport"
|
|
2758
|
+
xml = xml.replace(/<ObjectType>Report<\/ObjectType>/g, '<ObjectType>SSRSReport</ObjectType>');
|
|
2759
|
+
return xml;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* Visual Studio Project (.rnrproj) Manipulator
|
|
2764
|
+
*/
|
|
2765
|
+
export class ProjectFileManager {
|
|
2766
|
+
parser;
|
|
2767
|
+
builder;
|
|
2768
|
+
constructor() {
|
|
2769
|
+
this.parser = new Parser({
|
|
2770
|
+
explicitArray: false,
|
|
2771
|
+
mergeAttrs: false,
|
|
2772
|
+
trim: true,
|
|
2773
|
+
});
|
|
2774
|
+
this.builder = new Builder({
|
|
2775
|
+
xmldec: { version: '1.0', encoding: 'utf-8' },
|
|
2776
|
+
renderOpts: { pretty: true, indent: ' ' },
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Get friendly display folder name for project (used in Folder Include and Link)
|
|
2781
|
+
* e.g. class → Classes, enum → Base Enums
|
|
2782
|
+
*/
|
|
2783
|
+
getFolderName(objectType) {
|
|
2784
|
+
const folderMap = {
|
|
2785
|
+
class: 'Classes',
|
|
2786
|
+
'class-extension': 'Classes',
|
|
2787
|
+
table: 'Tables',
|
|
2788
|
+
enum: 'Base Enums',
|
|
2789
|
+
form: 'Forms',
|
|
2790
|
+
query: 'Queries',
|
|
2791
|
+
view: 'Views',
|
|
2792
|
+
'data-entity': 'Data Entities',
|
|
2793
|
+
'table-extension': 'Table Extensions',
|
|
2794
|
+
'form-extension': 'Form Extensions',
|
|
2795
|
+
'data-entity-extension': 'Data Entity Extensions',
|
|
2796
|
+
report: 'Reports',
|
|
2797
|
+
'menu-item-display': 'Menu Items Display',
|
|
2798
|
+
'menu-item-action': 'Menu Items Action',
|
|
2799
|
+
'menu-item-output': 'Menu Items Output',
|
|
2800
|
+
'menu-item-display-extension': 'Menu Item Display Extensions',
|
|
2801
|
+
'menu-item-action-extension': 'Menu Item Action Extensions',
|
|
2802
|
+
'menu-item-output-extension': 'Menu Item Output Extensions',
|
|
2803
|
+
edt: 'Extended Data Types',
|
|
2804
|
+
'edt-extension': 'EDT Extensions',
|
|
2805
|
+
'enum-extension': 'Enum Extensions',
|
|
2806
|
+
menu: 'Menus',
|
|
2807
|
+
'menu-extension': 'Menu Extensions',
|
|
2808
|
+
'security-privilege': 'Security Privileges',
|
|
2809
|
+
'security-duty': 'Security Duties',
|
|
2810
|
+
'security-role': 'Security Roles',
|
|
2811
|
+
'security-duty-extension': 'Security Duties',
|
|
2812
|
+
'security-role-extension': 'Security Roles',
|
|
2813
|
+
'business-event': 'Classes',
|
|
2814
|
+
tile: 'Tiles',
|
|
2815
|
+
kpi: 'KPIs',
|
|
2816
|
+
map: 'Maps',
|
|
2817
|
+
};
|
|
2818
|
+
return folderMap[objectType] || 'Classes';
|
|
2819
|
+
}
|
|
2820
|
+
/**
|
|
2821
|
+
* Get AOT folder prefix for Content Include path (no .xml extension)
|
|
2822
|
+
* e.g. class → AxClass, enum → AxEnum, data-entity → AxDataEntityView
|
|
2823
|
+
*/
|
|
2824
|
+
getAxFolderPrefix(objectType) {
|
|
2825
|
+
const prefixMap = {
|
|
2826
|
+
class: 'AxClass',
|
|
2827
|
+
'class-extension': 'AxClass',
|
|
2828
|
+
table: 'AxTable',
|
|
2829
|
+
enum: 'AxEnum',
|
|
2830
|
+
form: 'AxForm',
|
|
2831
|
+
query: 'AxQuery',
|
|
2832
|
+
view: 'AxView',
|
|
2833
|
+
'data-entity': 'AxDataEntityView',
|
|
2834
|
+
'table-extension': 'AxTableExtension',
|
|
2835
|
+
'form-extension': 'AxFormExtension',
|
|
2836
|
+
'data-entity-extension': 'AxDataEntityViewExtension',
|
|
2837
|
+
report: 'AxReport',
|
|
2838
|
+
'menu-item-display': 'AxMenuItemDisplay',
|
|
2839
|
+
'menu-item-action': 'AxMenuItemAction',
|
|
2840
|
+
'menu-item-output': 'AxMenuItemOutput',
|
|
2841
|
+
'menu-item-display-extension': 'AxMenuItemDisplayExtension',
|
|
2842
|
+
'menu-item-action-extension': 'AxMenuItemActionExtension',
|
|
2843
|
+
'menu-item-output-extension': 'AxMenuItemOutputExtension',
|
|
2844
|
+
edt: 'AxEdt',
|
|
2845
|
+
'edt-extension': 'AxEdtExtension',
|
|
2846
|
+
'enum-extension': 'AxEnumExtension',
|
|
2847
|
+
menu: 'AxMenu',
|
|
2848
|
+
'menu-extension': 'AxMenuExtension',
|
|
2849
|
+
'security-privilege': 'AxSecurityPrivilege',
|
|
2850
|
+
'security-duty': 'AxSecurityDuty',
|
|
2851
|
+
'security-role': 'AxSecurityRole',
|
|
2852
|
+
'security-duty-extension': 'AxSecurityDutyExtension',
|
|
2853
|
+
'security-role-extension': 'AxSecurityRoleExtension',
|
|
2854
|
+
'business-event': 'AxClass',
|
|
2855
|
+
tile: 'AxTile',
|
|
2856
|
+
kpi: 'AxKPI',
|
|
2857
|
+
map: 'AxMap',
|
|
2858
|
+
};
|
|
2859
|
+
return prefixMap[objectType] || 'AxClass';
|
|
2860
|
+
}
|
|
2861
|
+
/**
|
|
2862
|
+
* Add file reference to Visual Studio project
|
|
2863
|
+
* D365FO projects use ABSOLUTE paths to XML files in PackagesLocalDirectory
|
|
2864
|
+
* Returns true if file was added, false if file already exists in project
|
|
2865
|
+
*/
|
|
2866
|
+
async addToProject(projectPath, objectType, objectName, _absoluteXmlPath // kept for API compatibility
|
|
2867
|
+
) {
|
|
2868
|
+
return withProjectFileLock(projectPath, () => this._addToProjectLocked(projectPath, objectType, objectName));
|
|
2869
|
+
}
|
|
2870
|
+
async _addToProjectLocked(projectPath, objectType, objectName) {
|
|
2871
|
+
// Read project file (with retry for transient VS file locks)
|
|
2872
|
+
let projectXml = '';
|
|
2873
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
2874
|
+
try {
|
|
2875
|
+
projectXml = await fs.readFile(projectPath, 'utf-8');
|
|
2876
|
+
break;
|
|
2877
|
+
}
|
|
2878
|
+
catch (err) {
|
|
2879
|
+
if ((err.code === 'EBUSY' || err.code === 'EPERM' || err.code === 'EACCES') && attempt < 4) {
|
|
2880
|
+
await new Promise(r => setTimeout(r, 300 * (attempt + 1)));
|
|
2881
|
+
continue;
|
|
2882
|
+
}
|
|
2883
|
+
throw err;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
// Strip UTF-8 BOM if present (VS writes BOM; Node fs.readFile keeps it)
|
|
2887
|
+
let hadBom = false;
|
|
2888
|
+
if (projectXml.charCodeAt(0) === 0xFEFF) {
|
|
2889
|
+
projectXml = projectXml.slice(1);
|
|
2890
|
+
hadBom = true;
|
|
2891
|
+
}
|
|
2892
|
+
const project = await this.parser.parseStringPromise(projectXml);
|
|
2893
|
+
// Ensure project structure exists
|
|
2894
|
+
if (!project.Project) {
|
|
2895
|
+
throw new Error('Invalid .rnrproj file structure');
|
|
2896
|
+
}
|
|
2897
|
+
// Initialize ItemGroup if not exists — insert BEFORE Import elements
|
|
2898
|
+
// so MSBuild/VS sees items before targets (xml2js preserves JS key order)
|
|
2899
|
+
if (!project.Project.ItemGroup) {
|
|
2900
|
+
const { Import, ...rest } = project.Project;
|
|
2901
|
+
project.Project = { ...rest, ItemGroup: [{ Folder: [] }, { Content: [] }] };
|
|
2902
|
+
if (Import) {
|
|
2903
|
+
project.Project.Import = Import;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
// Convert to array if single ItemGroup
|
|
2907
|
+
if (!Array.isArray(project.Project.ItemGroup)) {
|
|
2908
|
+
project.Project.ItemGroup = [project.Project.ItemGroup];
|
|
2909
|
+
}
|
|
2910
|
+
// Find or create Folder ItemGroup
|
|
2911
|
+
let folderGroup = project.Project.ItemGroup.find((group) => group.Folder !== undefined);
|
|
2912
|
+
if (!folderGroup) {
|
|
2913
|
+
folderGroup = { Folder: [] };
|
|
2914
|
+
project.Project.ItemGroup.push(folderGroup);
|
|
2915
|
+
}
|
|
2916
|
+
// Find or create Content ItemGroup
|
|
2917
|
+
let contentGroup = project.Project.ItemGroup.find((group) => group.Content !== undefined);
|
|
2918
|
+
if (!contentGroup) {
|
|
2919
|
+
contentGroup = { Content: [] };
|
|
2920
|
+
project.Project.ItemGroup.push(contentGroup);
|
|
2921
|
+
}
|
|
2922
|
+
// Ensure arrays
|
|
2923
|
+
if (!Array.isArray(folderGroup.Folder)) {
|
|
2924
|
+
folderGroup.Folder = folderGroup.Folder ? [folderGroup.Folder] : [];
|
|
2925
|
+
}
|
|
2926
|
+
if (!Array.isArray(contentGroup.Content)) {
|
|
2927
|
+
contentGroup.Content = contentGroup.Content ? [contentGroup.Content] : [];
|
|
2928
|
+
}
|
|
2929
|
+
// Get folder names for project organization
|
|
2930
|
+
const displayFolderName = this.getFolderName(objectType);
|
|
2931
|
+
const axFolderPrefix = this.getAxFolderPrefix(objectType);
|
|
2932
|
+
// Add folder if not exists (uses friendly display name, e.g. "Classes\")
|
|
2933
|
+
const folderExists = folderGroup.Folder.some((folder) => folder.$ && folder.$.Include === `${displayFolderName}\\`);
|
|
2934
|
+
if (!folderExists) {
|
|
2935
|
+
folderGroup.Folder.push({
|
|
2936
|
+
$: { Include: `${displayFolderName}\\` },
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
// D365FO .rnrproj standard:
|
|
2940
|
+
// Content Include = AxClass\ObjectName (Ax prefix, NO .xml extension)
|
|
2941
|
+
// Link = Classes\ObjectName (display name, NO .xml extension)
|
|
2942
|
+
const contentInclude = `${axFolderPrefix}\\${objectName}`;
|
|
2943
|
+
const linkPath = `${displayFolderName}\\${objectName}`;
|
|
2944
|
+
// Check if file already in project
|
|
2945
|
+
const fileExists = contentGroup.Content.some((content) => content.$ && content.$.Include === contentInclude);
|
|
2946
|
+
if (fileExists) {
|
|
2947
|
+
console.error(`[ProjectFileManager] File ${objectName} is already in the project - skipping`);
|
|
2948
|
+
return false; // File already exists in project
|
|
2949
|
+
}
|
|
2950
|
+
// Add file reference
|
|
2951
|
+
contentGroup.Content.push({
|
|
2952
|
+
$: { Include: contentInclude },
|
|
2953
|
+
SubType: 'Content',
|
|
2954
|
+
Name: objectName,
|
|
2955
|
+
Link: linkPath,
|
|
2956
|
+
});
|
|
2957
|
+
console.error(`[ProjectFileManager] Added file reference to project, Content items: ${contentGroup.Content.length}`);
|
|
2958
|
+
// Write back to project file (with retry for transient VS file locks)
|
|
2959
|
+
const updatedXml = this.builder.buildObject(project);
|
|
2960
|
+
// Restore UTF-8 BOM if the original file had one (VS 2022 writes .rnrproj with BOM)
|
|
2961
|
+
const output = hadBom ? '\uFEFF' + updatedXml : updatedXml;
|
|
2962
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
2963
|
+
try {
|
|
2964
|
+
await fs.writeFile(projectPath, output, 'utf-8');
|
|
2965
|
+
break;
|
|
2966
|
+
}
|
|
2967
|
+
catch (err) {
|
|
2968
|
+
if ((err.code === 'EBUSY' || err.code === 'EPERM' || err.code === 'EACCES') && attempt < 4) {
|
|
2969
|
+
await new Promise(r => setTimeout(r, 300 * (attempt + 1)));
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
throw err;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
console.error(`[ProjectFileManager] Project file saved successfully`);
|
|
2976
|
+
return true; // File successfully added
|
|
2977
|
+
}
|
|
2978
|
+
/**
|
|
2979
|
+
* Add label file entries to Visual Studio project.
|
|
2980
|
+
* Each language needs TWO entries:
|
|
2981
|
+
* 1. AxLabelFile descriptor: Include="AxLabelFile\{id}_{lang}" Link="Label Files\{id}_{lang}"
|
|
2982
|
+
* 2. LabelResources .label.txt: Include="{id}.{lang}.label.txt" DependentUpon="AxLabelFile\{id}_{lang}"
|
|
2983
|
+
* Both are added inside a single file-lock + parse/write cycle for efficiency.
|
|
2984
|
+
* Returns the list of descriptor names that were newly added.
|
|
2985
|
+
*/
|
|
2986
|
+
async addLabelToProject(projectPath, labelFileId, languages) {
|
|
2987
|
+
return withProjectFileLock(projectPath, () => this._addLabelToProjectLocked(projectPath, labelFileId, languages));
|
|
2988
|
+
}
|
|
2989
|
+
async _addLabelToProjectLocked(projectPath, labelFileId, languages) {
|
|
2990
|
+
// Read project file (with retry for transient VS file locks)
|
|
2991
|
+
let projectXml = '';
|
|
2992
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
2993
|
+
try {
|
|
2994
|
+
projectXml = await fs.readFile(projectPath, 'utf-8');
|
|
2995
|
+
break;
|
|
2996
|
+
}
|
|
2997
|
+
catch (err) {
|
|
2998
|
+
if ((err.code === 'EBUSY' || err.code === 'EPERM' || err.code === 'EACCES') && attempt < 4) {
|
|
2999
|
+
await new Promise(r => setTimeout(r, 300 * (attempt + 1)));
|
|
3000
|
+
continue;
|
|
3001
|
+
}
|
|
3002
|
+
throw err;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
let hadBom = false;
|
|
3006
|
+
if (projectXml.charCodeAt(0) === 0xFEFF) {
|
|
3007
|
+
projectXml = projectXml.slice(1);
|
|
3008
|
+
hadBom = true;
|
|
3009
|
+
}
|
|
3010
|
+
const project = await this.parser.parseStringPromise(projectXml);
|
|
3011
|
+
if (!project.Project)
|
|
3012
|
+
throw new Error('Invalid .rnrproj file structure');
|
|
3013
|
+
// Ensure ItemGroup structure
|
|
3014
|
+
if (!project.Project.ItemGroup) {
|
|
3015
|
+
const { Import, ...rest } = project.Project;
|
|
3016
|
+
project.Project = { ...rest, ItemGroup: [{ Folder: [] }, { Content: [] }] };
|
|
3017
|
+
if (Import)
|
|
3018
|
+
project.Project.Import = Import;
|
|
3019
|
+
}
|
|
3020
|
+
if (!Array.isArray(project.Project.ItemGroup)) {
|
|
3021
|
+
project.Project.ItemGroup = [project.Project.ItemGroup];
|
|
3022
|
+
}
|
|
3023
|
+
let folderGroup = project.Project.ItemGroup.find((g) => g.Folder !== undefined);
|
|
3024
|
+
if (!folderGroup) {
|
|
3025
|
+
folderGroup = { Folder: [] };
|
|
3026
|
+
project.Project.ItemGroup.push(folderGroup);
|
|
3027
|
+
}
|
|
3028
|
+
let contentGroup = project.Project.ItemGroup.find((g) => g.Content !== undefined);
|
|
3029
|
+
if (!contentGroup) {
|
|
3030
|
+
contentGroup = { Content: [] };
|
|
3031
|
+
project.Project.ItemGroup.push(contentGroup);
|
|
3032
|
+
}
|
|
3033
|
+
if (!Array.isArray(folderGroup.Folder))
|
|
3034
|
+
folderGroup.Folder = folderGroup.Folder ? [folderGroup.Folder] : [];
|
|
3035
|
+
if (!Array.isArray(contentGroup.Content))
|
|
3036
|
+
contentGroup.Content = contentGroup.Content ? [contentGroup.Content] : [];
|
|
3037
|
+
// Ensure "Label Files\" folder entry
|
|
3038
|
+
const folderExists = folderGroup.Folder.some((f) => f.$ && f.$.Include === 'Label Files\\');
|
|
3039
|
+
if (!folderExists) {
|
|
3040
|
+
folderGroup.Folder.push({ $: { Include: 'Label Files\\' } });
|
|
3041
|
+
}
|
|
3042
|
+
const added = [];
|
|
3043
|
+
let newEntries = 0;
|
|
3044
|
+
const existingIncludes = new Set(contentGroup.Content.map((c) => c.$?.Include).filter(Boolean));
|
|
3045
|
+
for (const lang of languages) {
|
|
3046
|
+
const descriptorName = `${labelFileId}_${lang}`;
|
|
3047
|
+
const descriptorInclude = `AxLabelFile\\${descriptorName}`;
|
|
3048
|
+
const resourceFileName = `${labelFileId}.${lang}.label.txt`;
|
|
3049
|
+
// 1. AxLabelFile descriptor entry
|
|
3050
|
+
if (!existingIncludes.has(descriptorInclude)) {
|
|
3051
|
+
contentGroup.Content.push({
|
|
3052
|
+
$: { Include: descriptorInclude },
|
|
3053
|
+
SubType: 'Content',
|
|
3054
|
+
Name: descriptorName,
|
|
3055
|
+
Link: `Label Files\\${descriptorName}`,
|
|
3056
|
+
});
|
|
3057
|
+
existingIncludes.add(descriptorInclude);
|
|
3058
|
+
added.push(descriptorName);
|
|
3059
|
+
newEntries++;
|
|
3060
|
+
}
|
|
3061
|
+
// 2. LabelResources .label.txt entry with DependentUpon
|
|
3062
|
+
if (!existingIncludes.has(resourceFileName)) {
|
|
3063
|
+
contentGroup.Content.push({
|
|
3064
|
+
$: { Include: resourceFileName },
|
|
3065
|
+
SubType: 'Content',
|
|
3066
|
+
Name: resourceFileName,
|
|
3067
|
+
DependentUpon: descriptorInclude,
|
|
3068
|
+
});
|
|
3069
|
+
existingIncludes.add(resourceFileName);
|
|
3070
|
+
newEntries++;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
if (newEntries === 0) {
|
|
3074
|
+
console.error(`[ProjectFileManager] All label entries already in project — skipping write`);
|
|
3075
|
+
return added;
|
|
3076
|
+
}
|
|
3077
|
+
// Write back
|
|
3078
|
+
const updatedXml = this.builder.buildObject(project);
|
|
3079
|
+
const output = hadBom ? '\uFEFF' + updatedXml : updatedXml;
|
|
3080
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
3081
|
+
try {
|
|
3082
|
+
await fs.writeFile(projectPath, output, 'utf-8');
|
|
3083
|
+
break;
|
|
3084
|
+
}
|
|
3085
|
+
catch (err) {
|
|
3086
|
+
if ((err.code === 'EBUSY' || err.code === 'EPERM' || err.code === 'EACCES') && attempt < 4) {
|
|
3087
|
+
await new Promise(r => setTimeout(r, 300 * (attempt + 1)));
|
|
3088
|
+
continue;
|
|
3089
|
+
}
|
|
3090
|
+
throw err;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
console.error(`[ProjectFileManager] Added ${added.length} label descriptor(s) + resource entries to project`);
|
|
3094
|
+
return added;
|
|
3095
|
+
}
|
|
3096
|
+
/**
|
|
3097
|
+
* Extract ModelName from Visual Studio project file
|
|
3098
|
+
* Returns the actual model name from PropertyGroup/Model or PropertyGroup/ModelName
|
|
3099
|
+
*/
|
|
3100
|
+
async extractModelName(projectPath) {
|
|
3101
|
+
try {
|
|
3102
|
+
console.error(`[ProjectFileManager] Extracting model name from: ${projectPath}`);
|
|
3103
|
+
// Read project file
|
|
3104
|
+
let projectXml = await fs.readFile(projectPath, 'utf-8');
|
|
3105
|
+
// Strip UTF-8 BOM if present
|
|
3106
|
+
if (projectXml.charCodeAt(0) === 0xFEFF) {
|
|
3107
|
+
projectXml = projectXml.slice(1);
|
|
3108
|
+
}
|
|
3109
|
+
const project = await this.parser.parseStringPromise(projectXml);
|
|
3110
|
+
// Look for PropertyGroup with Model or ModelName
|
|
3111
|
+
if (project.Project && project.Project.PropertyGroup) {
|
|
3112
|
+
const propertyGroups = Array.isArray(project.Project.PropertyGroup)
|
|
3113
|
+
? project.Project.PropertyGroup
|
|
3114
|
+
: [project.Project.PropertyGroup];
|
|
3115
|
+
for (const group of propertyGroups) {
|
|
3116
|
+
// Try <Model> tag first (standard D365FO format)
|
|
3117
|
+
if (group.Model) {
|
|
3118
|
+
const modelName = group.Model;
|
|
3119
|
+
console.error(`[ProjectFileManager] Found Model in project: ${modelName}`);
|
|
3120
|
+
return modelName;
|
|
3121
|
+
}
|
|
3122
|
+
// Fallback to <ModelName> tag (alternative format)
|
|
3123
|
+
if (group.ModelName) {
|
|
3124
|
+
const modelName = group.ModelName;
|
|
3125
|
+
console.error(`[ProjectFileManager] Found ModelName in project: ${modelName}`);
|
|
3126
|
+
return modelName;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
console.error(`[ProjectFileManager] No Model or ModelName found in project file`);
|
|
3131
|
+
return null;
|
|
3132
|
+
}
|
|
3133
|
+
catch (error) {
|
|
3134
|
+
console.error(`[ProjectFileManager] Error extracting model name:`, error);
|
|
3135
|
+
return null;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
/**
|
|
3140
|
+
* Returns a BP warning string when a `label` property is raw text (not a @File:Id reference).
|
|
3141
|
+
* xppbp raises BPErrorLabelIsText for any object-level label that is not a label ID.
|
|
3142
|
+
* Use the `labels` tool to find or create a label ID before writing the object.
|
|
3143
|
+
*/
|
|
3144
|
+
function rawLabelBpWarning(properties, objectName) {
|
|
3145
|
+
const label = properties?.label;
|
|
3146
|
+
if (typeof label === 'string' && label.length > 0 && !label.startsWith('@')) {
|
|
3147
|
+
return `\n\n⚠️ **BPErrorLabelIsText risk:** Label "${label}" is raw text, not a label ID.\n` +
|
|
3148
|
+
`xppbp will report BPErrorLabelIsText on ${objectName}.\n` +
|
|
3149
|
+
`Fix: call \`labels(action="search", text="${label}")\` to find an existing @LabelFile:Id,\n` +
|
|
3150
|
+
`or \`labels(action="create", ...)\` to create one, then re-create with the @reference.`;
|
|
3151
|
+
}
|
|
3152
|
+
return '';
|
|
3153
|
+
}
|
|
3154
|
+
/**
|
|
3155
|
+
* Create D365FO file handler function
|
|
3156
|
+
*/
|
|
3157
|
+
export async function handleCreateD365File(request, context) {
|
|
3158
|
+
const args = CreateD365FileArgsSchema.parse(request.params.arguments);
|
|
3159
|
+
// Grounding enforcement: extension objects modify the behaviour of existing
|
|
3160
|
+
// code, so when GROUNDING_ENFORCE=true the model must prove (via prepare_change)
|
|
3161
|
+
// that it inspected the real object before writing the extension.
|
|
3162
|
+
if (args.objectType.endsWith('-extension')) {
|
|
3163
|
+
const groundingError = enforceGrounding(args.groundingToken, `d365fo_file(action="create", objectType="${args.objectType}", objectName="${args.objectName}")`, args.objectName);
|
|
3164
|
+
if (groundingError)
|
|
3165
|
+
return groundingError;
|
|
3166
|
+
}
|
|
3167
|
+
// Semantic reference gate: when GROUNDING_ENFORCE=true, every identifier in the
|
|
3168
|
+
// X++ source must be proven against the symbol index before it reaches disk.
|
|
3169
|
+
const referenceError = gateOnReferenceErrors(args.sourceCode, context?.symbolIndex, `d365fo_file(action="create", objectType="${args.objectType}", objectName="${args.objectName}")`);
|
|
3170
|
+
if (referenceError)
|
|
3171
|
+
return referenceError;
|
|
3172
|
+
try {
|
|
3173
|
+
// Step 1: Try to find and parse .rnrproj to get actual ModelName
|
|
3174
|
+
let actualModelName = args.modelName;
|
|
3175
|
+
let wasAutoExtracted = false;
|
|
3176
|
+
let projectPathToUse = args.projectPath;
|
|
3177
|
+
let solutionPathToUse = args.solutionPath;
|
|
3178
|
+
console.error(`[create_d365fo_file] Initial modelName: ${actualModelName}`);
|
|
3179
|
+
// If neither projectPath nor solutionPath provided, try to get from config or auto-detect
|
|
3180
|
+
if (!projectPathToUse && !solutionPathToUse) {
|
|
3181
|
+
const configManager = getConfigManager();
|
|
3182
|
+
// Try to auto-detect from workspace (async)
|
|
3183
|
+
projectPathToUse = await configManager.getProjectPath() || undefined;
|
|
3184
|
+
solutionPathToUse = await configManager.getSolutionPath() || undefined;
|
|
3185
|
+
// If model name was not passed as argument, try to resolve from mcp.json config
|
|
3186
|
+
if (!actualModelName) {
|
|
3187
|
+
actualModelName = configManager.getModelName() ?? undefined;
|
|
3188
|
+
if (actualModelName) {
|
|
3189
|
+
const ctx = configManager.getContext();
|
|
3190
|
+
const source = ctx?.modelName ? 'modelName (mcp.json)' : 'workspacePath (mcp.json)';
|
|
3191
|
+
console.error(`[create_d365fo_file] Using modelName from ${source}: ${actualModelName}`);
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
if (projectPathToUse) {
|
|
3195
|
+
console.error(`[create_d365fo_file] Using projectPath (auto-detected or from .mcp.json): ${projectPathToUse}`);
|
|
3196
|
+
}
|
|
3197
|
+
else if (solutionPathToUse) {
|
|
3198
|
+
console.error(`[create_d365fo_file] Using solutionPath (auto-detected or from .mcp.json): ${solutionPathToUse}`);
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
// If projectPath is available, extract model name from it
|
|
3202
|
+
if (projectPathToUse) {
|
|
3203
|
+
const projectManager = new ProjectFileManager();
|
|
3204
|
+
const extractedModelName = await projectManager.extractModelName(projectPathToUse);
|
|
3205
|
+
if (extractedModelName) {
|
|
3206
|
+
actualModelName = extractedModelName;
|
|
3207
|
+
wasAutoExtracted = true;
|
|
3208
|
+
console.error(`[create_d365fo_file] Extracted ModelName from projectPath: ${actualModelName}`);
|
|
3209
|
+
// ✨ Register extracted model as custom (since it came from user's project)
|
|
3210
|
+
registerCustomModel(actualModelName);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
// If solutionPath is available, try to find .rnrproj and extract model name
|
|
3214
|
+
else if (solutionPathToUse) {
|
|
3215
|
+
const foundProjectPath = await ProjectFileFinder.findProjectInSolution(solutionPathToUse, actualModelName ?? '');
|
|
3216
|
+
if (foundProjectPath) {
|
|
3217
|
+
const projectManager = new ProjectFileManager();
|
|
3218
|
+
const extractedModelName = await projectManager.extractModelName(foundProjectPath);
|
|
3219
|
+
if (extractedModelName) {
|
|
3220
|
+
actualModelName = extractedModelName;
|
|
3221
|
+
wasAutoExtracted = true;
|
|
3222
|
+
console.error(`[create_d365fo_file] Extracted ModelName from solutionPath .rnrproj: ${actualModelName}`);
|
|
3223
|
+
// ✨ Register extracted model as custom (since it came from user's project)
|
|
3224
|
+
registerCustomModel(actualModelName);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
// ⚠️ CRITICAL: modelName is required — must come from args, mcp.json, or .rnrproj extraction
|
|
3229
|
+
if (!actualModelName) {
|
|
3230
|
+
const errorMsg = '❌ ERROR: modelName could not be resolved.\n\n' +
|
|
3231
|
+
'Provide it in one of these ways:\n' +
|
|
3232
|
+
' 1. Pass modelName explicitly in the tool call arguments\n' +
|
|
3233
|
+
' 2. Add modelName to .mcp.json context: { "context": { "modelName": "YourModel" } }\n' +
|
|
3234
|
+
' 3. Add workspacePath ending with the package/model name: { "context": { "workspacePath": "C:\\\\AosService\\\\PackagesLocalDirectory\\\\YourModel" } }\n' +
|
|
3235
|
+
' 4. Add projectPath or solutionPath to .mcp.json so the model is auto-extracted from .rnrproj';
|
|
3236
|
+
console.error(`[create_d365fo_file] ${errorMsg}`);
|
|
3237
|
+
return { content: [{ type: 'text', text: errorMsg }], isError: true };
|
|
3238
|
+
}
|
|
3239
|
+
// ⚠️ CRITICAL WARNING: If no project/solution path available anywhere
|
|
3240
|
+
if (!projectPathToUse && !solutionPathToUse) {
|
|
3241
|
+
console.error(`[create_d365fo_file] ⚠️ WARNING: No projectPath or solutionPath available (not in args, not in .mcp.json)!`);
|
|
3242
|
+
console.error(`[create_d365fo_file] ⚠️ Using modelName AS-IS: "${actualModelName}"`);
|
|
3243
|
+
console.error(`[create_d365fo_file] ⚠️ If "${actualModelName}" is a Microsoft model (e.g., ApplicationSuite), this will create the file in the WRONG location!`);
|
|
3244
|
+
console.error(`[create_d365fo_file] ⚠️ Add projectPath or solutionPath to .mcp.json config to auto-extract correct ModelName from .rnrproj!`);
|
|
3245
|
+
// Extra validation: Check for suspicious/placeholder model names
|
|
3246
|
+
const suspiciousNames = ['auto', 'test', 'example', 'temp', 'undefined', 'null'];
|
|
3247
|
+
// Known Microsoft standard D365FO models — NEVER use for custom code
|
|
3248
|
+
const knownMicrosoftModels = [
|
|
3249
|
+
'applicationsuite', 'applicationcommon', 'applicationfoundation', 'applicationplatform',
|
|
3250
|
+
'applicationwebcomponents', 'applicationworkspaces', 'foundation',
|
|
3251
|
+
'directory', 'dimensions', 'currency', 'calendar', 'casemanagement',
|
|
3252
|
+
'contactperson', 'datasharing', 'dataupgrade', 'datamaintenance',
|
|
3253
|
+
'electronicreporting', 'electronicreportingcore',
|
|
3254
|
+
'banktype', 'banktypes', 'benefitsmanagement', 'creditmanagement',
|
|
3255
|
+
];
|
|
3256
|
+
const modelLower = actualModelName.toLowerCase();
|
|
3257
|
+
const isPlaceholder = suspiciousNames.includes(modelLower);
|
|
3258
|
+
const isMicrosoftModel = knownMicrosoftModels.includes(modelLower);
|
|
3259
|
+
if (isPlaceholder || isMicrosoftModel) {
|
|
3260
|
+
const reason = isPlaceholder
|
|
3261
|
+
? `"${actualModelName}" is a placeholder value, not a real D365FO model`
|
|
3262
|
+
: `"${actualModelName}" is a Microsoft standard model — custom code must NEVER be created there`;
|
|
3263
|
+
const errorMsg = `❌ ERROR: ${reason}\n\n` +
|
|
3264
|
+
`Root cause: No projectPath or solutionPath was found (not in tool args, not in .mcp.json config).\n` +
|
|
3265
|
+
`Without projectPath, the tool uses the modelName parameter AS-IS, which is wrong.\n\n` +
|
|
3266
|
+
`To fix — add projectPath to .mcp.json (in the MCP server directory)::\n` +
|
|
3267
|
+
` {\n` +
|
|
3268
|
+
` "servers": {\n` +
|
|
3269
|
+
` "context": {\n` +
|
|
3270
|
+
` "projectPath": "C:\\\\VSProjects\\\\YourSolution\\\\YourProject\\\\YourProject.rnrproj",\n` +
|
|
3271
|
+
` "solutionPath": "C:\\\\VSProjects\\\\YourSolution",\n` +
|
|
3272
|
+
` "packagePath": "C:\\\\AosService\\\\PackagesLocalDirectory"\n` +
|
|
3273
|
+
` }\n` +
|
|
3274
|
+
` }\n` +
|
|
3275
|
+
` }\n\n` +
|
|
3276
|
+
`Or pass projectPath explicitly in the tool call arguments.`;
|
|
3277
|
+
console.error(`[create_d365fo_file] ${errorMsg}`);
|
|
3278
|
+
return {
|
|
3279
|
+
content: [
|
|
3280
|
+
{
|
|
3281
|
+
type: 'text',
|
|
3282
|
+
text: errorMsg
|
|
3283
|
+
}
|
|
3284
|
+
],
|
|
3285
|
+
isError: true,
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
console.error(`[create_d365fo_file] Final ModelName to use: ${actualModelName}${wasAutoExtracted ? ' (auto-extracted ✓)' : ' (as-is, NOT auto-extracted ⚠️)'}`);
|
|
3290
|
+
// Guard: refuse to create objects in generic placeholder model names.
|
|
3291
|
+
// These are never real D365FO models — if the AI reaches this point with a placeholder,
|
|
3292
|
+
// the workspace was not detected correctly and the file would land in the wrong location.
|
|
3293
|
+
const PLACEHOLDER_MODELS = new Set([
|
|
3294
|
+
'mymodel', 'mypackage', 'model', 'package', 'modelname', 'packagename',
|
|
3295
|
+
'yourmodel', 'yourpackage', 'custommodel', 'custompackage',
|
|
3296
|
+
'testmodel', 'testpackage', 'samplemodel', 'samplepackage',
|
|
3297
|
+
]);
|
|
3298
|
+
if (actualModelName && PLACEHOLDER_MODELS.has(actualModelName.toLowerCase())) {
|
|
3299
|
+
return {
|
|
3300
|
+
content: [
|
|
3301
|
+
{
|
|
3302
|
+
type: 'text',
|
|
3303
|
+
text: `❌ Model name "${actualModelName}" looks like a placeholder — file creation aborted.\n\n` +
|
|
3304
|
+
`The workspace / project path was not detected correctly, so the model name\n` +
|
|
3305
|
+
`could not be resolved from the .rnrproj file.\n\n` +
|
|
3306
|
+
`To fix this, provide one of:\n` +
|
|
3307
|
+
` • projectPath — full path to the .rnrproj file (e.g. K:\\...\\MyProject.rnrproj)\n` +
|
|
3308
|
+
` • solutionPath — directory containing the .rnrproj\n` +
|
|
3309
|
+
` • A correct modelName that matches an actual D365FO model on disk\n\n` +
|
|
3310
|
+
`Never use "MyModel", "MyPackage" or similar placeholders as modelName.`,
|
|
3311
|
+
},
|
|
3312
|
+
],
|
|
3313
|
+
isError: true,
|
|
3314
|
+
};
|
|
3315
|
+
}
|
|
3316
|
+
// Apply extension prefix to object name
|
|
3317
|
+
const objectPrefix = resolveObjectPrefix(actualModelName);
|
|
3318
|
+
const namingStyle = getExtensionNamingStyle();
|
|
3319
|
+
// If EXTENSION_PREFIX differs from modelName, the AI may have embedded the modelName
|
|
3320
|
+
// in the extension name. Strip it so applyObjectPrefix injects the correct prefix only.
|
|
3321
|
+
// NOTE: this stripping only makes sense for the prefix-infix style. Under the
|
|
3322
|
+
// model-name style the model name IS the desired token, so the stripping below is
|
|
3323
|
+
// skipped and applyObjectPrefix (given actualModelName) normalises the name instead.
|
|
3324
|
+
let effectiveObjectName = args.objectName;
|
|
3325
|
+
// Case A: dot-notation extension elements (table/form/EDT/enum extensions)
|
|
3326
|
+
// e.g. "CustTable.MyModelExtension" with modelName="MyModel" → "CustTable.Extension"
|
|
3327
|
+
// applyObjectPrefix then produces "CustTable.MyExtension"
|
|
3328
|
+
if (namingStyle !== 'model-name' &&
|
|
3329
|
+
args.objectName.includes('.') &&
|
|
3330
|
+
args.objectName.toLowerCase().endsWith('extension') &&
|
|
3331
|
+
actualModelName &&
|
|
3332
|
+
objectPrefix.toLowerCase() !== actualModelName.toLowerCase()) {
|
|
3333
|
+
const dotIdx = args.objectName.lastIndexOf('.');
|
|
3334
|
+
const basePart = args.objectName.slice(0, dotIdx);
|
|
3335
|
+
const suffixPart = args.objectName.slice(dotIdx + 1);
|
|
3336
|
+
if (suffixPart.toLowerCase().startsWith(actualModelName.toLowerCase())) {
|
|
3337
|
+
effectiveObjectName = `${basePart}.${suffixPart.slice(actualModelName.length)}`;
|
|
3338
|
+
console.error(`[create_d365fo_file] Stripped model name from dot-notation extension: ` +
|
|
3339
|
+
`${args.objectName} → ${effectiveObjectName}`);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
// Case B: extension classes (objectName ends with "_Extension")
|
|
3343
|
+
// e.g. "SalesFormLetterContoso_Extension" with modelName="ContosoExt" → "SalesFormLetter_Extension"
|
|
3344
|
+
// applyObjectPrefix then produces "SalesFormLetterContoso_Extension"
|
|
3345
|
+
if (namingStyle !== 'model-name' &&
|
|
3346
|
+
args.objectName.endsWith('_Extension') &&
|
|
3347
|
+
actualModelName &&
|
|
3348
|
+
objectPrefix.toLowerCase() !== actualModelName.toLowerCase()) {
|
|
3349
|
+
const baseName = args.objectName.slice(0, -'_Extension'.length);
|
|
3350
|
+
if (baseName.toLowerCase().endsWith(actualModelName.toLowerCase())) {
|
|
3351
|
+
effectiveObjectName = baseName.slice(0, -actualModelName.length) + '_Extension';
|
|
3352
|
+
console.error(`[create_d365fo_file] Stripped model name infix "${actualModelName}" from extension class: ` +
|
|
3353
|
+
`${args.objectName} → ${effectiveObjectName}`);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
// Case C: dot-notation extension types provided without a dot (bare base name)
|
|
3357
|
+
// e.g. objectType="table-extension", objectName="PurchTable"
|
|
3358
|
+
// → effectiveObjectName="PurchTable.Extension" so applyObjectPrefix SPECIAL CASE A
|
|
3359
|
+
// produces the correct "PurchTable.ContosoExtension" instead of falling into NORMAL CASE
|
|
3360
|
+
// and producing the wrong "ContosoPurchTable".
|
|
3361
|
+
const DOT_NOTATION_EXTENSION_TYPES = new Set([
|
|
3362
|
+
'table-extension', 'form-extension', 'enum-extension', 'edt-extension',
|
|
3363
|
+
'data-entity-extension', 'menu-item-display-extension', 'menu-item-action-extension',
|
|
3364
|
+
'menu-item-output-extension', 'menu-extension',
|
|
3365
|
+
'security-duty-extension', 'security-role-extension',
|
|
3366
|
+
]);
|
|
3367
|
+
if (DOT_NOTATION_EXTENSION_TYPES.has(args.objectType) && !effectiveObjectName.includes('.')) {
|
|
3368
|
+
effectiveObjectName = `${effectiveObjectName}.Extension`;
|
|
3369
|
+
console.error(`[create_d365fo_file] Bare extension name auto-converted to dot-notation: ` +
|
|
3370
|
+
`${args.objectName} → ${effectiveObjectName}`);
|
|
3371
|
+
}
|
|
3372
|
+
// Case D: class extensions (CoC) provided as a bare base class name, i.e. WITHOUT
|
|
3373
|
+
// the "_Extension" suffix.
|
|
3374
|
+
// e.g. objectType="class-extension", objectName="SalesFormLetter"
|
|
3375
|
+
// → effectiveObjectName="SalesFormLetter_Extension" so applyObjectPrefix's
|
|
3376
|
+
// extension-class branch produces the correct name for the active style:
|
|
3377
|
+
// prefix style → SalesFormLetterCr_Extension
|
|
3378
|
+
// model-name style → SalesFormLetter_ContosoRobotics_Extension
|
|
3379
|
+
//
|
|
3380
|
+
// Without this, a bare base name has no dot and does not end in "_Extension", so it
|
|
3381
|
+
// falls into applyObjectPrefix's NORMAL CASE and is treated as a brand-new object —
|
|
3382
|
+
// wrongly producing "CrSalesFormLetter". This mirrors the dot-notation Case C above;
|
|
3383
|
+
// class-extension was the only extension type missing bare-name normalisation
|
|
3384
|
+
// (the EXTENSION_NAMING_STYLE work added the model-name branches but assumed the
|
|
3385
|
+
// caller always supplies the "_Extension" form for CoC classes).
|
|
3386
|
+
if (args.objectType === 'class-extension' && !effectiveObjectName.endsWith('_Extension')) {
|
|
3387
|
+
effectiveObjectName = `${effectiveObjectName}_Extension`;
|
|
3388
|
+
console.error(`[create_d365fo_file] Bare class-extension name auto-converted to _Extension form: ` +
|
|
3389
|
+
`${args.objectName} → ${effectiveObjectName}`);
|
|
3390
|
+
}
|
|
3391
|
+
// Pass actualModelName so the model-name naming style can use it as the extension
|
|
3392
|
+
// token. For the default prefix style (or non-extension objects) it is ignored.
|
|
3393
|
+
let finalObjectName = applyObjectPrefix(effectiveObjectName, objectPrefix, actualModelName);
|
|
3394
|
+
// Trailing suffix (EXTENSION_SUFFIX) applies to NEW objects only — never to
|
|
3395
|
+
// extension elements/classes. (For the prefix style applyObjectSuffix already
|
|
3396
|
+
// skips _Extension and dot-notation "…Extension" names; this guard additionally
|
|
3397
|
+
// covers the model-name style's "Base.ModelName" form, which has no "Extension"
|
|
3398
|
+
// word and would otherwise wrongly receive the suffix.)
|
|
3399
|
+
const isExtensionObjectType = args.objectType === 'class-extension' || DOT_NOTATION_EXTENSION_TYPES.has(args.objectType);
|
|
3400
|
+
const objectSuffix = getObjectSuffix();
|
|
3401
|
+
if (!isExtensionObjectType) {
|
|
3402
|
+
finalObjectName = applyObjectSuffix(finalObjectName, objectSuffix);
|
|
3403
|
+
}
|
|
3404
|
+
if (finalObjectName !== args.objectName) {
|
|
3405
|
+
console.error(`[create_d365fo_file] Applied naming: ${args.objectName} → ${finalObjectName}`);
|
|
3406
|
+
}
|
|
3407
|
+
// Determine object folder based on type
|
|
3408
|
+
const objectFolderMap = {
|
|
3409
|
+
class: 'AxClass',
|
|
3410
|
+
'class-extension': 'AxClass',
|
|
3411
|
+
table: 'AxTable',
|
|
3412
|
+
enum: 'AxEnum',
|
|
3413
|
+
form: 'AxForm',
|
|
3414
|
+
query: 'AxQuery',
|
|
3415
|
+
view: 'AxView',
|
|
3416
|
+
'data-entity': 'AxDataEntityView',
|
|
3417
|
+
report: 'AxReport',
|
|
3418
|
+
edt: 'AxEdt',
|
|
3419
|
+
'edt-extension': 'AxEdtExtension',
|
|
3420
|
+
'table-extension': 'AxTableExtension',
|
|
3421
|
+
'form-extension': 'AxFormExtension',
|
|
3422
|
+
'data-entity-extension': 'AxDataEntityViewExtension',
|
|
3423
|
+
'enum-extension': 'AxEnumExtension',
|
|
3424
|
+
'menu-item-display': 'AxMenuItemDisplay',
|
|
3425
|
+
'menu-item-action': 'AxMenuItemAction',
|
|
3426
|
+
'menu-item-output': 'AxMenuItemOutput',
|
|
3427
|
+
'menu-item-display-extension': 'AxMenuItemDisplayExtension',
|
|
3428
|
+
'menu-item-action-extension': 'AxMenuItemActionExtension',
|
|
3429
|
+
'menu-item-output-extension': 'AxMenuItemOutputExtension',
|
|
3430
|
+
menu: 'AxMenu',
|
|
3431
|
+
'menu-extension': 'AxMenuExtension',
|
|
3432
|
+
'security-privilege': 'AxSecurityPrivilege',
|
|
3433
|
+
'security-duty': 'AxSecurityDuty',
|
|
3434
|
+
'security-role': 'AxSecurityRole',
|
|
3435
|
+
'security-duty-extension': 'AxSecurityDutyExtension',
|
|
3436
|
+
'security-role-extension': 'AxSecurityRoleExtension',
|
|
3437
|
+
'business-event': 'AxClass',
|
|
3438
|
+
tile: 'AxTile',
|
|
3439
|
+
kpi: 'AxKPI',
|
|
3440
|
+
map: 'AxMap',
|
|
3441
|
+
};
|
|
3442
|
+
const objectFolder = objectFolderMap[args.objectType];
|
|
3443
|
+
if (!objectFolder) {
|
|
3444
|
+
throw new Error(`Unsupported object type: ${args.objectType}`);
|
|
3445
|
+
}
|
|
3446
|
+
// Construct full path - resolve package name
|
|
3447
|
+
// Package name can differ from model name in any environment (not just UDE).
|
|
3448
|
+
const configManager = getConfigManager();
|
|
3449
|
+
const configPackagePath = configManager.getPackagePath();
|
|
3450
|
+
const envType = await configManager.getDevEnvironmentType();
|
|
3451
|
+
let basePath;
|
|
3452
|
+
let resolvedPackageName;
|
|
3453
|
+
// Resolve the custom write root (D365FO_CUSTOM_PACKAGES_PATH).
|
|
3454
|
+
// Applies in both UDE and traditional mode — it always points to the repo
|
|
3455
|
+
// working tree where custom model XML lives, regardless of dev env type.
|
|
3456
|
+
const customWritePath = await configManager.getCustomPackagesPath();
|
|
3457
|
+
if (args.packageName) {
|
|
3458
|
+
// Explicit packageName always wins, regardless of environment type
|
|
3459
|
+
resolvedPackageName = args.packageName;
|
|
3460
|
+
// Custom write root beats the MS PLD for explicit packageName calls too.
|
|
3461
|
+
basePath = args.packagePath || customWritePath || configPackagePath || fallbackPackagePath();
|
|
3462
|
+
}
|
|
3463
|
+
else if (envType === 'ude') {
|
|
3464
|
+
// UDE mode: auto-resolve package name via descriptor scan across both roots
|
|
3465
|
+
const msPath = await configManager.getMicrosoftPackagesPath();
|
|
3466
|
+
const roots = [customWritePath, msPath].filter(Boolean);
|
|
3467
|
+
const resolver = new PackageResolver(roots);
|
|
3468
|
+
const resolved = await resolver.resolve(actualModelName);
|
|
3469
|
+
if (resolved) {
|
|
3470
|
+
resolvedPackageName = resolved.packageName;
|
|
3471
|
+
basePath = resolved.rootPath;
|
|
3472
|
+
}
|
|
3473
|
+
else {
|
|
3474
|
+
// Fallback: assume package == model (common case)
|
|
3475
|
+
resolvedPackageName = actualModelName;
|
|
3476
|
+
basePath = customWritePath || args.packagePath || configPackagePath || fallbackPackagePath();
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
else {
|
|
3480
|
+
// Traditional mode: try descriptor-based resolution first so a package
|
|
3481
|
+
// whose name differs from the model name (e.g. package "ISVPackage",
|
|
3482
|
+
// model "ISV Package") resolves correctly without an explicit packageName
|
|
3483
|
+
// arg. Scan both the custom write root and D365FO_PACKAGE_PATH for the
|
|
3484
|
+
// matching descriptor; fall back to assuming package == model otherwise.
|
|
3485
|
+
const roots = [customWritePath, configPackagePath].filter(Boolean);
|
|
3486
|
+
const resolver = new PackageResolver(roots);
|
|
3487
|
+
const resolved = roots.length ? await resolver.resolve(actualModelName) : null;
|
|
3488
|
+
if (resolved) {
|
|
3489
|
+
resolvedPackageName = resolved.packageName;
|
|
3490
|
+
basePath = resolved.rootPath;
|
|
3491
|
+
}
|
|
3492
|
+
else {
|
|
3493
|
+
// Fallback: assume package == model.
|
|
3494
|
+
// Prefer the custom write root over D365FO_PACKAGE_PATH so custom model
|
|
3495
|
+
// XML lands in the repo working tree rather than the MS PackagesLocalDirectory.
|
|
3496
|
+
resolvedPackageName = actualModelName;
|
|
3497
|
+
basePath =
|
|
3498
|
+
args.packagePath ||
|
|
3499
|
+
customWritePath ||
|
|
3500
|
+
configPackagePath ||
|
|
3501
|
+
fallbackPackagePath();
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
console.error(`[create_d365fo_file] Environment: ${envType}, Package: ${resolvedPackageName}, Model: ${actualModelName}, BasePath: ${basePath}`);
|
|
3505
|
+
const modelPath = path.join(basePath, resolvedPackageName, actualModelName, objectFolder);
|
|
3506
|
+
const fileName = `${finalObjectName}.xml`;
|
|
3507
|
+
const fullPath = path.join(modelPath, fileName);
|
|
3508
|
+
// Security: prevent path traversal. path.join() resolves ".." segments,
|
|
3509
|
+
// so a crafted modelName/objectName could escape basePath entirely.
|
|
3510
|
+
// Resolve both paths and assert the target stays within basePath.
|
|
3511
|
+
const resolvedBase = path.resolve(basePath);
|
|
3512
|
+
const resolvedTarget = path.resolve(fullPath);
|
|
3513
|
+
if (!resolvedTarget.startsWith(resolvedBase + path.sep) && resolvedTarget !== resolvedBase) {
|
|
3514
|
+
throw new Error(`❌ Security error: resolved path "${resolvedTarget}" is outside base directory "${resolvedBase}".\n` +
|
|
3515
|
+
`Check modelName, packageName, objectName, and packagePath for path traversal sequences.`);
|
|
3516
|
+
}
|
|
3517
|
+
// Normalize path to Windows format (backslashes) for consistency
|
|
3518
|
+
const normalizedFullPath = fullPath.replace(/\//g, '\\');
|
|
3519
|
+
// Ensure directory exists (create if needed)
|
|
3520
|
+
const directory = path.dirname(normalizedFullPath);
|
|
3521
|
+
// Verify drive/root exists before attempting recursive mkdir.
|
|
3522
|
+
// path.parse().root works on Windows but returns '' for Windows-style paths on POSIX,
|
|
3523
|
+
// so we extract the drive letter with a regex as a fallback.
|
|
3524
|
+
// (Node.js gives a cryptic '\\?' error when the drive letter doesn't exist)
|
|
3525
|
+
const windowsDriveMatch = /^([A-Za-z]:[/\\])/.exec(normalizedFullPath);
|
|
3526
|
+
const driveOrRoot = windowsDriveMatch ? windowsDriveMatch[1] : path.parse(directory).root; // e.g. "K:\" or "C:\"
|
|
3527
|
+
if (driveOrRoot) {
|
|
3528
|
+
try {
|
|
3529
|
+
await fs.access(driveOrRoot);
|
|
3530
|
+
}
|
|
3531
|
+
catch {
|
|
3532
|
+
const nonWindowsHint = process.platform !== 'win32'
|
|
3533
|
+
? `\n\n⚠️ This server is running on ${process.platform}. Windows drive letters (${driveOrRoot}) are not accessible.\n` +
|
|
3534
|
+
`Run the MCP server locally on the D365FO Windows VM instead.`
|
|
3535
|
+
: '';
|
|
3536
|
+
throw new Error(`❌ Drive or root path does not exist: ${driveOrRoot}\n\n` +
|
|
3537
|
+
`Attempting to create: ${directory}\n\n` +
|
|
3538
|
+
`The packagePath in your .mcp.json points to a drive that is not accessible.\n` +
|
|
3539
|
+
`Update "packagePath" in .mcp.json to match your actual D365FO installation:\n\n` +
|
|
3540
|
+
`Common paths:\n` +
|
|
3541
|
+
` C:\\AosService\\PackagesLocalDirectory\n` +
|
|
3542
|
+
` K:\\AosService\\PackagesLocalDirectory\n` +
|
|
3543
|
+
` J:\\AosService\\PackagesLocalDirectory\n\n` +
|
|
3544
|
+
`Current packagePath: ${basePath}\n` +
|
|
3545
|
+
`Current drive checked: ${driveOrRoot}${nonWindowsHint}`);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
try {
|
|
3549
|
+
await fs.mkdir(directory, { recursive: true });
|
|
3550
|
+
}
|
|
3551
|
+
catch (mkdirError) {
|
|
3552
|
+
console.error(`[create_d365fo_file] Failed to create directory:`, mkdirError);
|
|
3553
|
+
const hint = (mkdirError instanceof Error && mkdirError.message.includes('\\?'))
|
|
3554
|
+
? `\n\nHint: The path "${directory}" could not be created. ` +
|
|
3555
|
+
`Verify the drive letter exists and the path is correct. ` +
|
|
3556
|
+
`Update "packagePath" in .mcp.json to fix this.`
|
|
3557
|
+
: '';
|
|
3558
|
+
throw new Error(`Failed to create directory ${directory}: ${mkdirError instanceof Error ? mkdirError.message : 'Unknown error'}${hint}`);
|
|
3559
|
+
}
|
|
3560
|
+
// Check if file already exists
|
|
3561
|
+
let fileExisted = false;
|
|
3562
|
+
try {
|
|
3563
|
+
await fs.access(normalizedFullPath);
|
|
3564
|
+
fileExisted = true;
|
|
3565
|
+
}
|
|
3566
|
+
catch {
|
|
3567
|
+
// File does not exist — normal creation path
|
|
3568
|
+
}
|
|
3569
|
+
if (fileExisted) {
|
|
3570
|
+
if (!args.overwrite) {
|
|
3571
|
+
// Surface what's already on disk so the caller doesn't have to read the
|
|
3572
|
+
// file in chunks just to discover its contents. Previously this branch
|
|
3573
|
+
// returned only "already exists", forcing repeated read_file calls.
|
|
3574
|
+
let existingContent = '';
|
|
3575
|
+
try {
|
|
3576
|
+
existingContent = await fs.readFile(normalizedFullPath, 'utf-8');
|
|
3577
|
+
}
|
|
3578
|
+
catch { /* unreadable — fall through with no summary */ }
|
|
3579
|
+
let existingSummary = '';
|
|
3580
|
+
let inlineContent = '';
|
|
3581
|
+
if (existingContent) {
|
|
3582
|
+
const methodNames = [...existingContent.matchAll(/<Method>\s*<Name>([^<]+)<\/Name>/g)].map(m => m[1]);
|
|
3583
|
+
const fieldNames = [...existingContent.matchAll(/<AxTableField[A-Za-z]*>\s*<Name>([^<]+)<\/Name>/g)].map(m => m[1]);
|
|
3584
|
+
const summaryParts = [];
|
|
3585
|
+
if (methodNames.length) {
|
|
3586
|
+
summaryParts.push(`${methodNames.length} method(s): ${methodNames.slice(0, 30).join(', ')}${methodNames.length > 30 ? ', …' : ''}`);
|
|
3587
|
+
}
|
|
3588
|
+
if (fieldNames.length) {
|
|
3589
|
+
summaryParts.push(`${fieldNames.length} field(s): ${fieldNames.slice(0, 30).join(', ')}${fieldNames.length > 30 ? ', …' : ''}`);
|
|
3590
|
+
}
|
|
3591
|
+
const sizeKb = (Buffer.byteLength(existingContent, 'utf-8') / 1024).toFixed(1);
|
|
3592
|
+
existingSummary = `\n\n📄 Existing file (${sizeKb} KB):` +
|
|
3593
|
+
(summaryParts.length ? `\n ${summaryParts.join('\n ')}` : ' (no methods/fields detected)');
|
|
3594
|
+
// Inline the full content when small enough to be useful in one shot;
|
|
3595
|
+
// otherwise point at the targeted readers rather than dumping a huge file.
|
|
3596
|
+
const INLINE_LIMIT = 8000;
|
|
3597
|
+
inlineContent = existingContent.length <= INLINE_LIMIT
|
|
3598
|
+
? `\n\n----- BEGIN ${path.basename(normalizedFullPath)} -----\n${existingContent}\n----- END -----`
|
|
3599
|
+
: `\n\n(File is ${sizeKb} KB — too large to inline. Use get_method / get_object_info to read specific members.)`;
|
|
3600
|
+
}
|
|
3601
|
+
// When the requested objectName was normalized to a different on-disk name,
|
|
3602
|
+
// say so explicitly — the file that "already exists" can otherwise look
|
|
3603
|
+
// unrelated to what the caller asked for (e.g. "Foo_Extension" → "FooAc_Extension").
|
|
3604
|
+
const nameNote = finalObjectName !== args.objectName
|
|
3605
|
+
? `\n\nℹ️ Note: objectName "${args.objectName}" was normalized to "${finalObjectName}" ` +
|
|
3606
|
+
`(active naming style/prefix), so this is the file that matches your request.`
|
|
3607
|
+
: '';
|
|
3608
|
+
return {
|
|
3609
|
+
content: [
|
|
3610
|
+
{
|
|
3611
|
+
type: 'text',
|
|
3612
|
+
text: `⚠️ File already exists: ${normalizedFullPath}${nameNote}${existingSummary}\n\nOptions:\n` +
|
|
3613
|
+
` 1. Pass overwrite=true together with xmlContent to replace the file.\n` +
|
|
3614
|
+
` 2. Use d365fo_file(action="modify") to make targeted changes (rename-field, replace-all-fields, modify-property, …).\n` +
|
|
3615
|
+
` 3. Choose a different objectName.${inlineContent}`,
|
|
3616
|
+
},
|
|
3617
|
+
],
|
|
3618
|
+
isError: true,
|
|
3619
|
+
};
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
// ── Phase 4: Bridge-first creation via IMetadataProvider.Create() ──
|
|
3623
|
+
// For 15 supported types (class, class-extension, table, enum, edt, query, view, form,
|
|
3624
|
+
// menu, 3 menu-items, table/form/enum-extension): try C# bridge first.
|
|
3625
|
+
// Falls back to TypeScript XML generation if bridge unavailable or unsupported type
|
|
3626
|
+
// (report, data-entity, tile, kpi, business-event, security-privilege/duty/role, etc.).
|
|
3627
|
+
//
|
|
3628
|
+
// EXCEPTION — extensible enums: the C# bridge does not set UseEnumValue=No and
|
|
3629
|
+
// emits explicit <Value> elements, both of which xppc rejects with
|
|
3630
|
+
// "UseEnumValue property must be set to 'No' when IsExtensible is True".
|
|
3631
|
+
// Use the TypeScript XML generator (which handles this correctly) instead.
|
|
3632
|
+
//
|
|
3633
|
+
// EXCEPTION — security-privilege/duty/role: excluded from BRIDGE_CREATE_TYPES
|
|
3634
|
+
// entirely (see bridgeAdapter.ts) because the bridge silently drops their
|
|
3635
|
+
// structured collections (EntryPoints, DataEntityPermissions, Privileges, Duties).
|
|
3636
|
+
const skipBridgeForExtensibleEnum = args.objectType === 'enum'
|
|
3637
|
+
&& Boolean(args.properties?.isExtensible);
|
|
3638
|
+
if (!args.xmlContent && !skipBridgeForExtensibleEnum && context?.bridge && actualModelName && canBridgeCreate(args.objectType)) {
|
|
3639
|
+
try {
|
|
3640
|
+
// The bridge's `properties` is a flat string map (C# Dictionary<string,string>).
|
|
3641
|
+
// Keep only SCALAR values and stringify them. Structured collections
|
|
3642
|
+
// (fields/fieldGroups/indexes/relations/values/enumValues/methods) are
|
|
3643
|
+
// arrays/objects passed via their own bridge params below — if they leak into
|
|
3644
|
+
// `properties` the C# GetDictParam calls GetString() on a JSON array/boolean and
|
|
3645
|
+
// the whole create throws ("requires an element of type 'String', but the target
|
|
3646
|
+
// element has type 'Array'/'True'").
|
|
3647
|
+
const scalarProperties = args.properties
|
|
3648
|
+
? Object.fromEntries(Object.entries(args.properties)
|
|
3649
|
+
.filter(([, v]) => v != null && (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'))
|
|
3650
|
+
.map(([k, v]) => [k, String(v)]))
|
|
3651
|
+
: undefined;
|
|
3652
|
+
// Prepare parameters for the bridge
|
|
3653
|
+
const bridgeParams = {
|
|
3654
|
+
objectType: args.objectType,
|
|
3655
|
+
objectName: finalObjectName,
|
|
3656
|
+
modelName: actualModelName,
|
|
3657
|
+
properties: scalarProperties && Object.keys(scalarProperties).length > 0 ? scalarProperties : undefined,
|
|
3658
|
+
};
|
|
3659
|
+
// For classes: parse sourceCode into declaration + methods
|
|
3660
|
+
if ((args.objectType === 'class' || args.objectType === 'class-extension') && args.sourceCode) {
|
|
3661
|
+
const parsed = XmlTemplateGenerator.parseSourceForBridge(args.sourceCode, finalObjectName);
|
|
3662
|
+
bridgeParams.declaration = parsed.declaration;
|
|
3663
|
+
bridgeParams.methods = parsed.methods;
|
|
3664
|
+
}
|
|
3665
|
+
// For tables AND table-extensions: pass fields, fieldGroups, indexes, relations
|
|
3666
|
+
// from properties. (Previously only 'table' was handled, so a table-extension's
|
|
3667
|
+
// properties.fields were silently dropped and the file got an empty <Fields />.)
|
|
3668
|
+
// Field specs are normalized to the bridge's WriteFieldParam key shape so that
|
|
3669
|
+
// `{ edt, type }` keys are not lost — otherwise every field becomes a bare String.
|
|
3670
|
+
if ((args.objectType === 'table' || args.objectType === 'table-extension') && args.properties) {
|
|
3671
|
+
const props = args.properties;
|
|
3672
|
+
if (props.fields)
|
|
3673
|
+
bridgeParams.fields = normalizeFieldSpecsForBridge(props.fields);
|
|
3674
|
+
if (props.fieldGroups)
|
|
3675
|
+
bridgeParams.fieldGroups = props.fieldGroups;
|
|
3676
|
+
if (props.indexes)
|
|
3677
|
+
bridgeParams.indexes = normalizeIndexSpecsForBridge(props.indexes);
|
|
3678
|
+
if (props.relations)
|
|
3679
|
+
bridgeParams.relations = props.relations;
|
|
3680
|
+
if (props.methods) {
|
|
3681
|
+
bridgeParams.methods = props.methods.map(m => ({
|
|
3682
|
+
...m,
|
|
3683
|
+
source: m.source !== undefined ? reindentXppSource(m.source) : m.source,
|
|
3684
|
+
}));
|
|
3685
|
+
}
|
|
3686
|
+
// Resolve each field's base type from its EDT when the caller only gave
|
|
3687
|
+
// `edt` (the documented usage — the tool schema says "EDT auto-resolved
|
|
3688
|
+
// when omitted"). Without this, C# CreateTableField() defaults ANY field
|
|
3689
|
+
// whose `type` is unset to AxTableFieldString regardless of the EDT's real
|
|
3690
|
+
// base type — a Real-based EDT (e.g. a rate/amount) or a Date-based EDT
|
|
3691
|
+
// silently becomes a string field. `generateSmartTable`/`generate_object`
|
|
3692
|
+
// already resolve this; the plain d365fo_file(create, table/table-extension)
|
|
3693
|
+
// path never did. Mirrors that resolution: bridge (authoritative) → indexed
|
|
3694
|
+
// edt_metadata chain → name heuristic.
|
|
3695
|
+
if (bridgeParams.fields && bridgeParams.fields.length > 0) {
|
|
3696
|
+
const db = context.symbolIndex?.getReadDb?.();
|
|
3697
|
+
for (const f of bridgeParams.fields) {
|
|
3698
|
+
const edt = f.edt;
|
|
3699
|
+
if (!edt || f.type)
|
|
3700
|
+
continue;
|
|
3701
|
+
if (db && !f.enumType && isEnumName(edt, db)) {
|
|
3702
|
+
f.enumType = edt;
|
|
3703
|
+
f.type = 'Enum';
|
|
3704
|
+
delete f.edt;
|
|
3705
|
+
continue;
|
|
3706
|
+
}
|
|
3707
|
+
const resolved = (await bridgeEdtBaseType(context.bridge, edt))
|
|
3708
|
+
?? (db ? resolveEdtBaseType(edt, db) : undefined)
|
|
3709
|
+
?? heuristicEdtBaseType(edt);
|
|
3710
|
+
if (resolved) {
|
|
3711
|
+
f.type = resolved;
|
|
3712
|
+
// An EDT whose OWN base type is Enum (e.g. "Posted" extends "NoYes") only
|
|
3713
|
+
// gets the literal string "Enum" back from the resolvers above — without
|
|
3714
|
+
// the actual enum name, the bridge cannot emit a valid AxTableFieldEnum and
|
|
3715
|
+
// silently falls back to AxTableFieldString. Look up the underlying enum
|
|
3716
|
+
// name so the field is created correctly instead of building "successfully"
|
|
3717
|
+
// as a mistyped string field.
|
|
3718
|
+
if (resolved === 'Enum' && !f.enumType && db) {
|
|
3719
|
+
const enumType = resolveEdtEnumType(edt, db);
|
|
3720
|
+
if (enumType)
|
|
3721
|
+
f.enumType = enumType;
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
// For enums AND enum-extensions: pass values from properties.
|
|
3728
|
+
// Accept both `enumValues` (documented in tool description) and `values` (legacy).
|
|
3729
|
+
// Regression: only 'enum' was handled here, so an enum-extension's properties.enumValues
|
|
3730
|
+
// never reached bridgeParams.values — C# CreateEnumExtension() happily accepts a `values`
|
|
3731
|
+
// list, but was always called with null, silently writing an empty <EnumValues />
|
|
3732
|
+
// (write reported success; the dropped value surfaced two calls later as an unrelated
|
|
3733
|
+
// "unresolved enum value" build error). Same class of bug as the table/table-extension
|
|
3734
|
+
// fields gap fixed above.
|
|
3735
|
+
if ((args.objectType === 'enum' || args.objectType === 'enum-extension') && args.properties) {
|
|
3736
|
+
const props = args.properties;
|
|
3737
|
+
const enumVals = (props.enumValues ?? props.values);
|
|
3738
|
+
if (enumVals)
|
|
3739
|
+
bridgeParams.values = enumVals;
|
|
3740
|
+
}
|
|
3741
|
+
// For views: pass fields from properties
|
|
3742
|
+
if (args.objectType === 'view' && args.properties) {
|
|
3743
|
+
const props = args.properties;
|
|
3744
|
+
if (props.fields)
|
|
3745
|
+
bridgeParams.fields = props.fields;
|
|
3746
|
+
}
|
|
3747
|
+
// For EDTs: translate the tool's documented `edtType` property to the bridge's
|
|
3748
|
+
// expected `BaseType` key. C# CreateEdt() does `properties.TryGetValue("BaseType", ...)`
|
|
3749
|
+
// — a literal, case-SENSITIVE dictionary lookup — so sending `edtType` (as documented
|
|
3750
|
+
// in the tool schema and as suggest_edt/prepare recommend) never matched, silently
|
|
3751
|
+
// defaulting every bridge-created EDT to AxEdtString regardless of the requested type
|
|
3752
|
+
// (Real, Int, Date, Enum, ...). Confirmed via a live create of an EDT with
|
|
3753
|
+
// edtType:"Real", extends:"AmountCur" — the written XML came back i:type="AxEdtString".
|
|
3754
|
+
if (args.objectType === 'edt' && bridgeParams.properties && 'edtType' in bridgeParams.properties) {
|
|
3755
|
+
const { edtType, ...rest } = bridgeParams.properties;
|
|
3756
|
+
bridgeParams.properties = { ...rest, BaseType: edtType };
|
|
3757
|
+
}
|
|
3758
|
+
// For plain 'table' creates, prefer the bridge's BP-smart path
|
|
3759
|
+
// (CreateSmartTable) over the generic createObject/CreateTable RPC.
|
|
3760
|
+
// CreateTable writes exactly what the caller passed and nothing more —
|
|
3761
|
+
// no CacheLookup, PrimaryIndex/ClusteredIndex/ReplacementKey, TitleField1/2,
|
|
3762
|
+
// or the 5 standard FieldGroups (AutoReport/AutoLookup/AutoIdentification/
|
|
3763
|
+
// AutoSummary/AutoBrowse) that every real D365FO table gets. generate_object
|
|
3764
|
+
// (mode="scaffold"/"generate", objectType="table") already routes through
|
|
3765
|
+
// CreateSmartTable and gets these correctly; the plain create verb — the
|
|
3766
|
+
// one a generic "create a table" instruction naturally maps to — silently
|
|
3767
|
+
// produced a BP-defaults-free skeleton instead (eval corpus: L1-table-basic,
|
|
3768
|
+
// L3-form-detailstransaction, L4-ssrs-report-basic — golden_diff missing
|
|
3769
|
+
// CacheLookup/ClusteredIndex/PrimaryIndex/ReplacementKey/TitleField1/TitleField2
|
|
3770
|
+
// and all 5 standard FieldGroups). Try the smart path first; any failure or
|
|
3771
|
+
// unavailability falls through to the existing generic bridgeCreateObject/XML
|
|
3772
|
+
// paths below, unchanged.
|
|
3773
|
+
if (args.objectType === 'table') {
|
|
3774
|
+
const rawTableProps = args.properties ?? {};
|
|
3775
|
+
const smartTableGroup = typeof rawTableProps.tableGroup === 'string' ? rawTableProps.tableGroup : undefined;
|
|
3776
|
+
const smartTableType = typeof rawTableProps.tableType === 'string' ? rawTableProps.tableType : undefined;
|
|
3777
|
+
const smartLabel = typeof rawTableProps.label === 'string' ? rawTableProps.label : undefined;
|
|
3778
|
+
const smartExtraProperties = scalarProperties
|
|
3779
|
+
? Object.fromEntries(Object.entries(scalarProperties).filter(([k]) => !['tableGroup', 'tableType', 'label'].includes(k)))
|
|
3780
|
+
: undefined;
|
|
3781
|
+
try {
|
|
3782
|
+
const smartResult = await bridgeCreateSmartTable(context.bridge, {
|
|
3783
|
+
objectName: finalObjectName,
|
|
3784
|
+
modelName: actualModelName,
|
|
3785
|
+
tableGroup: smartTableGroup,
|
|
3786
|
+
tableType: smartTableType,
|
|
3787
|
+
label: smartLabel ?? finalObjectName,
|
|
3788
|
+
fields: bridgeParams.fields,
|
|
3789
|
+
extraFieldGroups: bridgeParams.fieldGroups,
|
|
3790
|
+
indexes: bridgeParams.indexes,
|
|
3791
|
+
relations: bridgeParams.relations,
|
|
3792
|
+
methods: bridgeParams.methods,
|
|
3793
|
+
extraProperties: smartExtraProperties && Object.keys(smartExtraProperties).length > 0
|
|
3794
|
+
? smartExtraProperties
|
|
3795
|
+
: undefined,
|
|
3796
|
+
});
|
|
3797
|
+
if (smartResult?.success && smartResult.filePath) {
|
|
3798
|
+
console.error(`[create_d365fo_file] ✅ Created via C# bridge (BP-smart): ${smartResult.filePath}`);
|
|
3799
|
+
let projectMsg = '';
|
|
3800
|
+
if (args.addToProject !== false) {
|
|
3801
|
+
if (projectPathToUse) {
|
|
3802
|
+
try {
|
|
3803
|
+
const projectManager = new ProjectFileManager();
|
|
3804
|
+
await projectManager.addToProject(projectPathToUse, args.objectType, finalObjectName, smartResult.filePath);
|
|
3805
|
+
projectMsg = `\n✅ Added to project: ${path.basename(projectPathToUse)}`;
|
|
3806
|
+
}
|
|
3807
|
+
catch (projErr) {
|
|
3808
|
+
projectMsg = `\n⚠️ Could not add to project: ${projErr}`;
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
else if (solutionPathToUse) {
|
|
3812
|
+
try {
|
|
3813
|
+
const detectedPath = await ProjectFileFinder.findProjectInSolution(solutionPathToUse, actualModelName);
|
|
3814
|
+
if (detectedPath) {
|
|
3815
|
+
const projectManager = new ProjectFileManager();
|
|
3816
|
+
await projectManager.addToProject(detectedPath, args.objectType, finalObjectName, smartResult.filePath);
|
|
3817
|
+
projectMsg = `\n✅ Added to project: ${path.basename(detectedPath)}`;
|
|
3818
|
+
}
|
|
3819
|
+
else {
|
|
3820
|
+
projectMsg = `\n⚠️ Could not find .rnrproj for model '${actualModelName}' in ${solutionPathToUse}`;
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
catch (projErr) {
|
|
3824
|
+
projectMsg = `\n⚠️ Could not add to project: ${projErr}`;
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
else {
|
|
3828
|
+
projectMsg = `\n⚠️ addToProject=true but no projectPath could be resolved.\n` +
|
|
3829
|
+
`Add projectPath to .mcp.json or pass it as a parameter.`;
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
// Eagerly refresh the bridge provider so the new object is immediately
|
|
3833
|
+
// resolvable by subsequent modify calls in the same session.
|
|
3834
|
+
try {
|
|
3835
|
+
await bridgeRefreshProvider(context.bridge);
|
|
3836
|
+
}
|
|
3837
|
+
catch { /* best-effort */ }
|
|
3838
|
+
const rawLabelWarning = rawLabelBpWarning(args.properties, finalObjectName);
|
|
3839
|
+
const bp = smartResult.bpDefaults;
|
|
3840
|
+
const bpSummary = bp
|
|
3841
|
+
? `\n📋 BP defaults: CacheLookup=${bp.cacheLookup ?? '(n/a)'}, TitleField1=${bp.titleField1 ?? '(none)'}, ` +
|
|
3842
|
+
`TitleField2=${bp.titleField2 ?? '(none)'}, PrimaryIndex=${bp.primaryIndex ?? '(none)'}, ` +
|
|
3843
|
+
`ClusteredIndex=${bp.clusteredIndex ?? '(none)'}`
|
|
3844
|
+
: '';
|
|
3845
|
+
return {
|
|
3846
|
+
content: [
|
|
3847
|
+
{
|
|
3848
|
+
type: 'text',
|
|
3849
|
+
text: `✅ Created ${args.objectType} '${finalObjectName}' via IMetadataProvider.Create() (Smart)\n` +
|
|
3850
|
+
`📁 ${smartResult.filePath}${projectMsg}\n` +
|
|
3851
|
+
`🔧 API: ${smartResult.api ?? 'IMetaTableProvider.Create (Smart)'}${bpSummary}${rawLabelWarning}`,
|
|
3852
|
+
},
|
|
3853
|
+
],
|
|
3854
|
+
};
|
|
3855
|
+
}
|
|
3856
|
+
console.error(`[create_d365fo_file] createSmartTable returned ${JSON.stringify(smartResult)} — falling back to generic bridge create`);
|
|
3857
|
+
}
|
|
3858
|
+
catch (smartErr) {
|
|
3859
|
+
console.error(`[create_d365fo_file] createSmartTable failed, falling back to generic bridge create: ${smartErr}`);
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
const bridgeResult = await bridgeCreateObject(context.bridge, bridgeParams);
|
|
3863
|
+
if (bridgeResult?.success && bridgeResult.filePath) {
|
|
3864
|
+
console.error(`[create_d365fo_file] ✅ Created via C# bridge: ${bridgeResult.filePath}`);
|
|
3865
|
+
// Add to .rnrproj if requested
|
|
3866
|
+
let projectMsg = '';
|
|
3867
|
+
if (args.addToProject !== false) {
|
|
3868
|
+
if (projectPathToUse) {
|
|
3869
|
+
try {
|
|
3870
|
+
const projectManager = new ProjectFileManager();
|
|
3871
|
+
await projectManager.addToProject(projectPathToUse, args.objectType, finalObjectName, bridgeResult.filePath);
|
|
3872
|
+
projectMsg = `\n✅ Added to project: ${path.basename(projectPathToUse)}`;
|
|
3873
|
+
}
|
|
3874
|
+
catch (projErr) {
|
|
3875
|
+
projectMsg = `\n⚠️ Could not add to project: ${projErr}`;
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
else if (solutionPathToUse) {
|
|
3879
|
+
// Try to find project in solution directory (same logic as XML fallback path)
|
|
3880
|
+
try {
|
|
3881
|
+
const detectedPath = await ProjectFileFinder.findProjectInSolution(solutionPathToUse, actualModelName);
|
|
3882
|
+
if (detectedPath) {
|
|
3883
|
+
const projectManager = new ProjectFileManager();
|
|
3884
|
+
await projectManager.addToProject(detectedPath, args.objectType, finalObjectName, bridgeResult.filePath);
|
|
3885
|
+
projectMsg = `\n✅ Added to project: ${path.basename(detectedPath)}`;
|
|
3886
|
+
}
|
|
3887
|
+
else {
|
|
3888
|
+
projectMsg = `\n⚠️ Could not find .rnrproj for model '${actualModelName}' in ${solutionPathToUse}`;
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
catch (projErr) {
|
|
3892
|
+
projectMsg = `\n⚠️ Could not add to project: ${projErr}`;
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
else {
|
|
3896
|
+
projectMsg = `\n⚠️ addToProject=true but no projectPath could be resolved.\n` +
|
|
3897
|
+
`Add projectPath to .mcp.json or pass it as a parameter.`;
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
// Eagerly refresh the bridge provider so the new object is immediately
|
|
3901
|
+
// resolvable by subsequent modify calls in the same session.
|
|
3902
|
+
try {
|
|
3903
|
+
await bridgeRefreshProvider(context.bridge);
|
|
3904
|
+
}
|
|
3905
|
+
catch { /* best-effort */ }
|
|
3906
|
+
const rawLabelWarning = rawLabelBpWarning(args.properties, finalObjectName);
|
|
3907
|
+
return {
|
|
3908
|
+
content: [
|
|
3909
|
+
{
|
|
3910
|
+
type: 'text',
|
|
3911
|
+
text: `✅ Created ${args.objectType} '${finalObjectName}' via IMetadataProvider.Create()\n` +
|
|
3912
|
+
`📁 ${bridgeResult.filePath}${projectMsg}\n` +
|
|
3913
|
+
`🔧 API: ${bridgeResult.message}${rawLabelWarning}`,
|
|
3914
|
+
},
|
|
3915
|
+
],
|
|
3916
|
+
};
|
|
3917
|
+
}
|
|
3918
|
+
// If bridge returned null or success=false, fall through to XML generation
|
|
3919
|
+
console.error(`[create_d365fo_file] Bridge returned ${JSON.stringify(bridgeResult)} — falling back to XML generation`);
|
|
3920
|
+
}
|
|
3921
|
+
catch (bridgeErr) {
|
|
3922
|
+
console.error(`[create_d365fo_file] Bridge create failed, falling back to XML: ${bridgeErr}`);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
// Generate (or use provided) XML content
|
|
3926
|
+
let xmlContent = args.xmlContent
|
|
3927
|
+
? args.xmlContent
|
|
3928
|
+
: XmlTemplateGenerator.generate(args.objectType, finalObjectName, args.sourceCode, args.properties);
|
|
3929
|
+
// Guard against HTML-entity-escaped xmlContent (e.g. "<?xml..." instead of "<?xml...").
|
|
3930
|
+
// This writes silently — no XML parse happens on this path — so a caller mistake here
|
|
3931
|
+
// looks like a success and only breaks on the next build. Found authoring the
|
|
3932
|
+
// L2-numberseq-basic eval case: passing already-escaped XML through xmlContent produced
|
|
3933
|
+
// a file containing literal "<"/">" instead of real tags.
|
|
3934
|
+
if (args.xmlContent && /<\?xml|<Ax\w/.test(args.xmlContent) && !args.xmlContent.trimStart().startsWith('<')) {
|
|
3935
|
+
throw new Error('xmlContent appears to be HTML-entity-escaped (contains "<" but does not start with a literal "<"). ' +
|
|
3936
|
+
'Pass raw XML (with literal < and >), not HTML-encoded text — this parameter is written to disk verbatim, unparsed.');
|
|
3937
|
+
}
|
|
3938
|
+
// CRITICAL FIX: Replace unprefixed class/table names with prefixed finalObjectName
|
|
3939
|
+
// When xmlContent or sourceCode contains `class MyClass` but finalObjectName is `MyPrefixMyClass`,
|
|
3940
|
+
// the file would be named MyPrefixMyClass.xml but contain `class MyClass` — inconsistency!
|
|
3941
|
+
if (finalObjectName !== args.objectName && (args.xmlContent || args.sourceCode)) {
|
|
3942
|
+
const orig = args.objectName;
|
|
3943
|
+
const final = finalObjectName;
|
|
3944
|
+
// Escape for use in RegExp (handles dots in extension names like "Foo.Extension")
|
|
3945
|
+
const escapedOrig = orig.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3946
|
+
// 1. `class OriginalName` / `public class OriginalName` etc.
|
|
3947
|
+
const classPattern = new RegExp(`\\b(public\\s+|private\\s+|protected\\s+|internal\\s+|final\\s+)?class\\s+${escapedOrig}\\b`, 'g');
|
|
3948
|
+
let replaced = xmlContent.replace(classPattern, (match) => match.replace(orig, final));
|
|
3949
|
+
// 2. classnum(OriginalName) — X++ intrinsic that refers to the class by name.
|
|
3950
|
+
// Callers often write classnum(OriginalName) in the source code before prefixing.
|
|
3951
|
+
const classnumPattern = new RegExp(`\\bclassnum\\(\\s*${escapedOrig}\\s*\\)`, 'gi');
|
|
3952
|
+
replaced = replaced.replace(classnumPattern, (m) => m.replace(new RegExp(escapedOrig, 'i'), final));
|
|
3953
|
+
// 3. classStr(OriginalName) — used in [ExtensionOf(classStr(...))] and SysOperation attributes.
|
|
3954
|
+
const classStrPattern = new RegExp(`\\bclassStr\\(\\s*${escapedOrig}\\s*\\)`, 'gi');
|
|
3955
|
+
replaced = replaced.replace(classStrPattern, (m) => m.replace(new RegExp(escapedOrig, 'i'), final));
|
|
3956
|
+
if (replaced !== xmlContent) {
|
|
3957
|
+
console.error(`[create_d365fo_file] ✅ Fixed class name inconsistency: ` +
|
|
3958
|
+
`replaced \`${orig}\` with \`${final}\` in XML content (class decl, classnum, classStr)`);
|
|
3959
|
+
xmlContent = replaced;
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
// Sanitize AxReport XML structure — ensures required D365FO VS Designer elements
|
|
3963
|
+
// are always present, regardless of whether xmlContent came from the template or a caller.
|
|
3964
|
+
if (args.objectType === 'report') {
|
|
3965
|
+
xmlContent = XmlTemplateGenerator.sanitizeReportXml(xmlContent);
|
|
3966
|
+
// Convert remaining <Text><![CDATA[…]]></Text> to entity-encoded form.
|
|
3967
|
+
// sanitizeReportXml operates on CDATA internally; this final step converts
|
|
3968
|
+
// the output so that D365FO VS Designer renders the design correctly.
|
|
3969
|
+
xmlContent = XmlTemplateGenerator.encodeReportTextElement(xmlContent);
|
|
3970
|
+
}
|
|
3971
|
+
// Sanitize menu item XML — D365FO metadata deserializer requires
|
|
3972
|
+
// xmlns="Microsoft.Dynamics.AX.Metadata.V1" on the root element.
|
|
3973
|
+
if (args.objectType === 'menu-item-display' ||
|
|
3974
|
+
args.objectType === 'menu-item-action' ||
|
|
3975
|
+
args.objectType === 'menu-item-output') {
|
|
3976
|
+
xmlContent = XmlTemplateGenerator.sanitizeMenuItemXml(xmlContent);
|
|
3977
|
+
}
|
|
3978
|
+
// Sanitize table XML — ensures correct field element format required by D365FO deserializer.
|
|
3979
|
+
if (args.objectType === 'table') {
|
|
3980
|
+
xmlContent = XmlTemplateGenerator.sanitizeTableXml(xmlContent);
|
|
3981
|
+
}
|
|
3982
|
+
// Sanitize query XML — ensures xmlns="" and i:type="AxQuerySimple" on root element.
|
|
3983
|
+
if (args.objectType === 'query') {
|
|
3984
|
+
xmlContent = XmlTemplateGenerator.sanitizeQueryXml(xmlContent);
|
|
3985
|
+
}
|
|
3986
|
+
// Sanitize enum XML — fixes <Values> → <EnumValues> and adds xmlns:i if missing.
|
|
3987
|
+
// Applies to both template-generated and caller-provided xmlContent.
|
|
3988
|
+
if (args.objectType === 'enum') {
|
|
3989
|
+
xmlContent = XmlTemplateGenerator.sanitizeEnumXml(xmlContent);
|
|
3990
|
+
}
|
|
3991
|
+
// Safety net: ensure every pair of adjacent </Method>…<Method> is separated by
|
|
3992
|
+
// exactly one blank line. This guards against xmlContent supplied by callers
|
|
3993
|
+
// (e.g. from generate or generate_d365fo_xml) that might already be
|
|
3994
|
+
// correct, or against edge-cases in the generator that produces no blank line.
|
|
3995
|
+
// The replacement is idempotent: \n\n\n → \n\n (no double-blank lines created).
|
|
3996
|
+
xmlContent = xmlContent.replace(/<\/Method>\n(\t*)<Method>/g, '</Method>\n\n$1<Method>');
|
|
3997
|
+
// Form pattern gate: structural pattern violations (FP001-FP005, FP007)
|
|
3998
|
+
// block the write when FORM_PATTERN_ENFORCE is enabled (default).
|
|
3999
|
+
// Recommendations are appended to the success message instead.
|
|
4000
|
+
let formPatternWarnings = '';
|
|
4001
|
+
if (args.objectType === 'form') {
|
|
4002
|
+
const gate = await gateOnFormPatternErrors(xmlContent, `d365fo_file(action="create", form ${finalObjectName})`);
|
|
4003
|
+
if (gate.blocked) {
|
|
4004
|
+
return gate.blocked;
|
|
4005
|
+
}
|
|
4006
|
+
if (gate.warningsText) {
|
|
4007
|
+
formPatternWarnings = `\n${gate.warningsText}\n`;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
// Form-extension control-shape gate: reject the malformed control shapes an AI
|
|
4011
|
+
// tends to hand-write (<AxFormControlExtension>, <ParentControlName>,
|
|
4012
|
+
// <FormControlExtension> wrapping the control, AxFormIntControl) — they pass XML
|
|
4013
|
+
// well-formedness but the D365FO deserializer rejects them. Blocks when
|
|
4014
|
+
// FORM_PATTERN_ENFORCE is on (default), else appends a warning.
|
|
4015
|
+
if (args.objectType === 'form-extension' && args.xmlContent) {
|
|
4016
|
+
const shapeProblems = validateFormExtensionControlShape(xmlContent);
|
|
4017
|
+
if (shapeProblems.length > 0) {
|
|
4018
|
+
const shapeError = buildFormExtensionShapeError(finalObjectName, shapeProblems);
|
|
4019
|
+
if (isFormPatternEnforceEnabled()) {
|
|
4020
|
+
return { content: [{ type: 'text', text: shapeError }], isError: true };
|
|
4021
|
+
}
|
|
4022
|
+
formPatternWarnings += `\n⚠️ ${shapeError}\n`;
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
// Debug: Log XML content length
|
|
4026
|
+
const xmlSource = args.xmlContent ? 'provided by caller' : 'generated from template';
|
|
4027
|
+
console.error(`[create_d365fo_file] XML content (${xmlSource}): ${xmlContent.length} bytes`);
|
|
4028
|
+
console.error(`[create_d365fo_file] XML preview: ${xmlContent.substring(0, 200)}...`);
|
|
4029
|
+
// Write file matching D365FO convention: no BOM, CRLF, no trailing newline
|
|
4030
|
+
try {
|
|
4031
|
+
await fs.writeFile(normalizedFullPath, normalizeD365Xml(xmlContent), 'utf-8');
|
|
4032
|
+
}
|
|
4033
|
+
catch (writeError) {
|
|
4034
|
+
console.error(`[create_d365fo_file] Failed to write file:`, writeError);
|
|
4035
|
+
// Check if it's a disk/path issue
|
|
4036
|
+
const errorMessage = writeError instanceof Error ? writeError.message : String(writeError);
|
|
4037
|
+
if (errorMessage.includes('EINVAL') || errorMessage.includes('ENOENT')) {
|
|
4038
|
+
throw new Error(`Failed to write file to ${normalizedFullPath}.\n\n` +
|
|
4039
|
+
`Possible causes:\n` +
|
|
4040
|
+
`1. Drive K:\\ does not exist (running on Linux/Mac? Use packagePath parameter to override)\n` +
|
|
4041
|
+
`2. Directory ${path.dirname(normalizedFullPath)} is not accessible\n` +
|
|
4042
|
+
`3. Insufficient permissions\n\n` +
|
|
4043
|
+
`Original error: ${errorMessage}`);
|
|
4044
|
+
}
|
|
4045
|
+
throw writeError;
|
|
4046
|
+
}
|
|
4047
|
+
// Verify file was written
|
|
4048
|
+
const stats = await fs.stat(normalizedFullPath);
|
|
4049
|
+
const fileSizeKb = (stats.size / 1024).toFixed(1);
|
|
4050
|
+
console.error(`[create_d365fo_file] ✅ Written: ${normalizedFullPath} (${fileSizeKb} KB)`);
|
|
4051
|
+
// Eagerly refresh the bridge provider so the new object is immediately
|
|
4052
|
+
// resolvable by subsequent modify calls in the same session.
|
|
4053
|
+
try {
|
|
4054
|
+
await bridgeRefreshProvider(context?.bridge);
|
|
4055
|
+
}
|
|
4056
|
+
catch { /* best-effort */ }
|
|
4057
|
+
// Post-write validation via C# bridge (best-effort, non-fatal, fire-and-forget).
|
|
4058
|
+
// Not awaited: the validation goes through the sequential bridge stdin/stdout
|
|
4059
|
+
// pipe and can take 60s+, which would block all subsequent MCP calls.
|
|
4060
|
+
// See: https://github.com/dynamics365ninja/d365fo-mcp-server/issues/407
|
|
4061
|
+
let bridgeValidation = '';
|
|
4062
|
+
bridgeValidateAfterWrite(context?.bridge, args.objectType, finalObjectName).then(validationMsg => {
|
|
4063
|
+
if (validationMsg) {
|
|
4064
|
+
console.error(`[create_d365fo_file] Bridge validation: ${validationMsg}`);
|
|
4065
|
+
}
|
|
4066
|
+
}).catch(e => {
|
|
4067
|
+
console.error(`[create_d365fo_file] Bridge validation skipped: ${e}`);
|
|
4068
|
+
});
|
|
4069
|
+
// Add to Visual Studio project if requested
|
|
4070
|
+
let projectMessage = '';
|
|
4071
|
+
if (args.addToProject) {
|
|
4072
|
+
// Try to find project file if not explicitly specified
|
|
4073
|
+
// Use projectPathToUse which includes values from .mcp.json config
|
|
4074
|
+
let projectPath = projectPathToUse;
|
|
4075
|
+
if (!projectPath && solutionPathToUse) {
|
|
4076
|
+
// Try to find project in solution directory
|
|
4077
|
+
// Use solutionPathToUse which includes values from .mcp.json config
|
|
4078
|
+
console.error(`[create_d365fo_file] Searching for .rnrproj in solution: ${solutionPathToUse}, model: ${actualModelName}`);
|
|
4079
|
+
const detectedPath = await ProjectFileFinder.findProjectInSolution(solutionPathToUse, actualModelName);
|
|
4080
|
+
if (!detectedPath) {
|
|
4081
|
+
console.error(`[create_d365fo_file] No .rnrproj found in solution directory`);
|
|
4082
|
+
projectMessage = `\n⚠️ Could not find .rnrproj file for model '${actualModelName}' in solution directory.\n` +
|
|
4083
|
+
`Searched in: ${solutionPathToUse}\n` +
|
|
4084
|
+
`Please specify projectPath parameter explicitly or add it to .mcp.json.\n`;
|
|
4085
|
+
}
|
|
4086
|
+
else {
|
|
4087
|
+
console.error(`[create_d365fo_file] Found project file: ${detectedPath}`);
|
|
4088
|
+
projectPath = detectedPath;
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
else if (!projectPath) {
|
|
4092
|
+
projectMessage = `\n⚠️ Cannot add to project: projectPath could not be resolved.\n` +
|
|
4093
|
+
`Add projectPath to .mcp.json config, or pass it as a tool argument.\n` +
|
|
4094
|
+
`Example .mcp.json: { "servers": { "context": { "projectPath": "K:\\\\VSProjects\\\\MySolution\\\\MyModel\\\\MyModel.rnrproj" } } }\n`;
|
|
4095
|
+
}
|
|
4096
|
+
if (projectPath) {
|
|
4097
|
+
try {
|
|
4098
|
+
// Validate project file exists
|
|
4099
|
+
await fs.access(projectPath);
|
|
4100
|
+
// D365FO projects expect ABSOLUTE paths to XML files, not relative
|
|
4101
|
+
// The full path must point to the exact XML location in PackagesLocalDirectory
|
|
4102
|
+
// Ensure Windows path format with backslashes
|
|
4103
|
+
const absoluteXmlPath = normalizedFullPath;
|
|
4104
|
+
// Add to project
|
|
4105
|
+
const projectManager = new ProjectFileManager();
|
|
4106
|
+
const wasAdded = await projectManager.addToProject(projectPath, args.objectType, finalObjectName, absoluteXmlPath);
|
|
4107
|
+
if (wasAdded) {
|
|
4108
|
+
console.error(`[create_d365fo_file] Successfully added to project`);
|
|
4109
|
+
projectMessage = `\n✅ Successfully added to Visual Studio project:\n📋 Project: ${projectPath}\n` +
|
|
4110
|
+
`ℹ️ If the file does not appear in VS Solution Explorer, right-click the project → Reload Project.`;
|
|
4111
|
+
}
|
|
4112
|
+
else {
|
|
4113
|
+
console.error(`[create_d365fo_file] File already exists in project`);
|
|
4114
|
+
projectMessage = `\n✅ File already exists in Visual Studio project:\n📋 Project: ${projectPath}\n`;
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
catch (projectError) {
|
|
4118
|
+
const errMsg = projectError instanceof Error ? projectError.message : 'Unknown error';
|
|
4119
|
+
const isLocked = errMsg.includes('EBUSY') || errMsg.includes('EPERM') || errMsg.includes('EACCES');
|
|
4120
|
+
console.error(`[create_d365fo_file] Failed to add to project:`, projectError);
|
|
4121
|
+
projectMessage = `\n⚠️ File created but failed to add to project:\n${errMsg}\n` +
|
|
4122
|
+
(isLocked
|
|
4123
|
+
? `This usually means Visual Studio has the .rnrproj file locked.\n` +
|
|
4124
|
+
`Close Visual Studio (or unload the project), re-run the tool, then reopen.\n`
|
|
4125
|
+
: '');
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
else if (!projectMessage) {
|
|
4129
|
+
// No projectPath found from any source — surface this in the response so AI and user see it
|
|
4130
|
+
projectMessage = `\n⚠️ addToProject=true but no projectPath could be resolved.\n` +
|
|
4131
|
+
`The file was created on disk but was NOT added to the Visual Studio project.\n\n` +
|
|
4132
|
+
`To fix this, add projectPath to your .mcp.json:\n` +
|
|
4133
|
+
` {\n` +
|
|
4134
|
+
` "servers": { "context": {\n` +
|
|
4135
|
+
` "projectPath": "K:\\\\VSProjects\\\\YourSolution\\\\YourModel\\\\YourModel.rnrproj"\n` +
|
|
4136
|
+
` } }\n` +
|
|
4137
|
+
` }\n` +
|
|
4138
|
+
`Until then, add the file manually in Visual Studio: right-click project → Add Existing Item → ${normalizedFullPath}\n`;
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
// Build success message
|
|
4142
|
+
const nextSteps = args.addToProject
|
|
4143
|
+
? `Next steps:\n` +
|
|
4144
|
+
`1. Reload project in Visual Studio (or close/reopen solution)\n` +
|
|
4145
|
+
`2. Build the project to synchronize the object\n` +
|
|
4146
|
+
`3. Refresh AOT in Visual Studio to see the new object\n`
|
|
4147
|
+
: `Next steps:\n` +
|
|
4148
|
+
`1. Add the file to your Visual Studio project (.rnrproj)\n` +
|
|
4149
|
+
`2. Build the project to synchronize the object\n` +
|
|
4150
|
+
`3. Refresh AOT in Visual Studio to see the new object\n`;
|
|
4151
|
+
// Return success message with file path
|
|
4152
|
+
return {
|
|
4153
|
+
content: [
|
|
4154
|
+
{
|
|
4155
|
+
type: 'text',
|
|
4156
|
+
text: `✅ Successfully created D365FO ${args.objectType} file:\n\n` +
|
|
4157
|
+
`📁 Path: ${normalizedFullPath}\n` +
|
|
4158
|
+
`📄 Object: ${finalObjectName}${finalObjectName !== args.objectName ? ` (prefixed from "${args.objectName}")` : ''}\n` +
|
|
4159
|
+
`📦 Model: ${actualModelName}\n` +
|
|
4160
|
+
`🔧 Type: ${objectFolder}\n` +
|
|
4161
|
+
bridgeValidation +
|
|
4162
|
+
formPatternWarnings +
|
|
4163
|
+
rawLabelBpWarning(args.properties, finalObjectName) +
|
|
4164
|
+
projectMessage +
|
|
4165
|
+
`\n${nextSteps}\n` +
|
|
4166
|
+
`⛔ TASK COMPLETE — do NOT call \`generate\`, \`generate\`, or \`d365fo_file(action="create")\` again for this object.`,
|
|
4167
|
+
},
|
|
4168
|
+
],
|
|
4169
|
+
};
|
|
4170
|
+
}
|
|
4171
|
+
catch (error) {
|
|
4172
|
+
return {
|
|
4173
|
+
content: [
|
|
4174
|
+
{
|
|
4175
|
+
type: 'text',
|
|
4176
|
+
text: `❌ Error creating D365FO file:\n\n${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
4177
|
+
},
|
|
4178
|
+
],
|
|
4179
|
+
isError: true,
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
// Tool registration (name, description, inputSchema) lives inline in
|
|
4184
|
+
// src/server/mcpServer.ts - the single source of truth for tool instructions.
|
|
4185
|
+
//# sourceMappingURL=createD365File.js.map
|