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,2815 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X++ Knowledge Base Tool
|
|
3
|
+
* Queryable knowledge base of D365FO / X++ patterns, best practices,
|
|
4
|
+
* and AX2012 → D365FO migration guidance.
|
|
5
|
+
*
|
|
6
|
+
* Data is embedded — no DB or disk access needed. Available in all server modes.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
// ─── Schema ─────────────────────────────────────────────────────────────────
|
|
10
|
+
const XppKnowledgeArgsSchema = z.object({
|
|
11
|
+
topic: z.string().describe('Topic to query — e.g. "batch job", "ttsbegin", "RunBase vs SysOperation", ' +
|
|
12
|
+
'"set-based operations", "CoC", "data entities", "number sequences", "security", ' +
|
|
13
|
+
'"temp tables", "today() deprecated", "query patterns", "form patterns", ' +
|
|
14
|
+
'"inventory", "feature management", "dual-write", "DMF", "warehouse", ' +
|
|
15
|
+
'"trade agreements", "configuration keys", "Power Platform", ' +
|
|
16
|
+
'"read Excel/CSV", "parallel batch", "direct SQL"'),
|
|
17
|
+
format: z.enum(['concise', 'detailed']).optional().default('concise').describe('concise = quick reference (default), detailed = full explanation with code examples'),
|
|
18
|
+
});
|
|
19
|
+
// ─── Knowledge Base ─────────────────────────────────────────────────────────
|
|
20
|
+
export const KNOWLEDGE_BASE = [
|
|
21
|
+
// ── Batch / SysOperation ────────────────────────────────────────────────
|
|
22
|
+
{
|
|
23
|
+
id: 'sysoperation',
|
|
24
|
+
title: 'SysOperation Framework (replaces RunBase)',
|
|
25
|
+
keywords: ['batch', 'sysoperation', 'runbase', 'batch job', 'dialog', 'contract', 'controller', 'service', 'srsreportruncontroller'],
|
|
26
|
+
summary: 'D365FO uses the SysOperation framework for batch-capable operations. ' +
|
|
27
|
+
'RunBase still works but is legacy — new code should always use SysOperation. ' +
|
|
28
|
+
'The framework separates concerns: DataContract (parameters), Service (logic), Controller (execution).',
|
|
29
|
+
migration: {
|
|
30
|
+
ax2012: 'class MyBatch extends RunBaseBatch { dialog(), run(), pack/unpack }',
|
|
31
|
+
d365fo: 'DataContract + Service class + Controller (or just [SysEntryPointAttribute] service)',
|
|
32
|
+
},
|
|
33
|
+
rules: [
|
|
34
|
+
'New batch jobs: ALWAYS use SysOperation (DataContract + Service + Controller)',
|
|
35
|
+
'RunBase is legacy — only extend existing RunBase classes, never create new ones',
|
|
36
|
+
'DataContract: decorate with [DataContractAttribute], parm methods with [DataMemberAttribute]',
|
|
37
|
+
'Service: business logic class, no UI references',
|
|
38
|
+
'Controller: extends SysOperationServiceController, sets caption, calls service',
|
|
39
|
+
'For simple batch: controller.parmClassName / parmMethodName can point directly to a static method',
|
|
40
|
+
'Menu items: type = Action, point to Controller class',
|
|
41
|
+
],
|
|
42
|
+
examples: [
|
|
43
|
+
{
|
|
44
|
+
label: 'DataContract',
|
|
45
|
+
code: `[DataContractAttribute]
|
|
46
|
+
class MyProcessContract
|
|
47
|
+
{
|
|
48
|
+
TransDate fromDate;
|
|
49
|
+
TransDate toDate;
|
|
50
|
+
|
|
51
|
+
[DataMemberAttribute,
|
|
52
|
+
SysOperationLabelAttribute(literalStr("@MyModel:FromDate")),
|
|
53
|
+
SysOperationDisplayOrderAttribute('1')]
|
|
54
|
+
public TransDate parmFromDate(TransDate _fromDate = fromDate)
|
|
55
|
+
{
|
|
56
|
+
fromDate = _fromDate;
|
|
57
|
+
return fromDate;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[DataMemberAttribute,
|
|
61
|
+
SysOperationLabelAttribute(literalStr("@MyModel:ToDate")),
|
|
62
|
+
SysOperationDisplayOrderAttribute('2')]
|
|
63
|
+
public TransDate parmToDate(TransDate _toDate = toDate)
|
|
64
|
+
{
|
|
65
|
+
toDate = _toDate;
|
|
66
|
+
return toDate;
|
|
67
|
+
}
|
|
68
|
+
}`,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: 'Service',
|
|
72
|
+
code: `class MyProcessService
|
|
73
|
+
{
|
|
74
|
+
/// <summary>
|
|
75
|
+
/// Processes records within the specified date range.
|
|
76
|
+
/// </summary>
|
|
77
|
+
public void processRecords(MyProcessContract _contract)
|
|
78
|
+
{
|
|
79
|
+
TransDate fromDate = _contract.parmFromDate();
|
|
80
|
+
TransDate toDate = _contract.parmToDate();
|
|
81
|
+
|
|
82
|
+
// Business logic here
|
|
83
|
+
}
|
|
84
|
+
}`,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: 'Controller',
|
|
88
|
+
code: `class MyProcessController extends SysOperationServiceController
|
|
89
|
+
{
|
|
90
|
+
/// <summary>
|
|
91
|
+
/// Constructs the controller for the batch operation.
|
|
92
|
+
/// </summary>
|
|
93
|
+
public static MyProcessController construct()
|
|
94
|
+
{
|
|
95
|
+
MyProcessController controller = new MyProcessController();
|
|
96
|
+
controller.parmClassName(classStr(MyProcessService));
|
|
97
|
+
controller.parmMethodName(methodStr(MyProcessService, processRecords));
|
|
98
|
+
return controller;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public static void main(Args _args)
|
|
102
|
+
{
|
|
103
|
+
MyProcessController controller = MyProcessController::construct();
|
|
104
|
+
controller.parmDialogCaption("@MyModel:ProcessRecords");
|
|
105
|
+
controller.startOperation();
|
|
106
|
+
}
|
|
107
|
+
}`,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
related: ['transactions', 'error-handling'],
|
|
111
|
+
},
|
|
112
|
+
// ── Transactions ────────────────────────────────────────────────────────
|
|
113
|
+
{
|
|
114
|
+
id: 'transactions',
|
|
115
|
+
title: 'Transaction Handling (ttsbegin / ttscommit)',
|
|
116
|
+
keywords: ['tts', 'ttsbegin', 'ttscommit', 'ttsabort', 'transaction', 'concurrency', 'occ', 'optimistic concurrency', 'update conflict'],
|
|
117
|
+
summary: 'X++ uses ttsbegin/ttscommit for transaction scoping. Transactions are nestable (reference-counted). ' +
|
|
118
|
+
'OCC (Optimistic Concurrency Control) is the default — always handle UpdateConflict exceptions.',
|
|
119
|
+
rules: [
|
|
120
|
+
'ALWAYS pair ttsbegin with ttscommit — unbalanced calls cause runtime crash',
|
|
121
|
+
'NEVER put try/catch INSIDE ttsbegin..ttscommit — transaction is already rolled back when exception is caught',
|
|
122
|
+
'Put try/catch OUTSIDE the tts block, catch UpdateConflict, then retry',
|
|
123
|
+
'Use forupdate keyword on select when modifying records',
|
|
124
|
+
'Use pessimisticlock for high-concurrency scenarios (e.g. number sequences)',
|
|
125
|
+
'NEVER call ttsabort() as normal flow — it\'s for unrecoverable situations only',
|
|
126
|
+
'Set-based operations (update_recordset, insert_recordset) run inside implicit tts if not explicitly scoped',
|
|
127
|
+
'Maximum retry count for OCC: typically 5 (use a counter variable)',
|
|
128
|
+
],
|
|
129
|
+
examples: [
|
|
130
|
+
{
|
|
131
|
+
label: 'Correct OCC retry pattern',
|
|
132
|
+
code: `int retryCount = 0;
|
|
133
|
+
const int maxRetries = 5;
|
|
134
|
+
boolean success = false;
|
|
135
|
+
|
|
136
|
+
while (!success && retryCount < maxRetries)
|
|
137
|
+
{
|
|
138
|
+
try
|
|
139
|
+
{
|
|
140
|
+
ttsbegin;
|
|
141
|
+
CustTable custTable;
|
|
142
|
+
select forupdate custTable
|
|
143
|
+
where custTable.AccountNum == '1001';
|
|
144
|
+
custTable.CreditMax = 10000;
|
|
145
|
+
custTable.update();
|
|
146
|
+
ttscommit;
|
|
147
|
+
success = true;
|
|
148
|
+
}
|
|
149
|
+
catch (Exception::UpdateConflict)
|
|
150
|
+
{
|
|
151
|
+
retryCount++;
|
|
152
|
+
if (retryCount >= maxRetries)
|
|
153
|
+
{
|
|
154
|
+
throw Exception::UpdateConflictNotRecovered;
|
|
155
|
+
}
|
|
156
|
+
// retry — loop continues
|
|
157
|
+
}
|
|
158
|
+
}`,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
label: 'WRONG — try/catch inside tts',
|
|
162
|
+
code: `// ❌ NEVER DO THIS — transaction is already rolled back
|
|
163
|
+
ttsbegin;
|
|
164
|
+
try
|
|
165
|
+
{
|
|
166
|
+
custTable.update();
|
|
167
|
+
}
|
|
168
|
+
catch
|
|
169
|
+
{
|
|
170
|
+
// tts is already broken — this does NOT help
|
|
171
|
+
}
|
|
172
|
+
ttscommit; // ← will crash: tts level mismatch`,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
related: ['set-based', 'error-handling'],
|
|
176
|
+
},
|
|
177
|
+
// ── Set-Based Operations ────────────────────────────────────────────────
|
|
178
|
+
{
|
|
179
|
+
id: 'set-based',
|
|
180
|
+
title: 'Set-Based Operations (insert_recordset, update_recordset, delete_from)',
|
|
181
|
+
keywords: ['set-based', 'insert_recordset', 'update_recordset', 'delete_from', 'recordinsertlist', 'bulk', 'performance', 'record by record'],
|
|
182
|
+
summary: 'Set-based operations execute in a single SQL statement instead of row-by-row. ' +
|
|
183
|
+
'They are 10-100x faster for bulk operations. D365FO adds RecordInsertList for batch inserts.',
|
|
184
|
+
migration: {
|
|
185
|
+
ax2012: 'while select + insert/update/delete in a loop (record-by-record)',
|
|
186
|
+
d365fo: 'insert_recordset / update_recordset / delete_from / RecordInsertList',
|
|
187
|
+
},
|
|
188
|
+
rules: [
|
|
189
|
+
'ALWAYS prefer set-based operations over while-select + DML loops',
|
|
190
|
+
'insert_recordset: bulk insert from one table to another with field mapping',
|
|
191
|
+
'update_recordset: bulk update with WHERE clause, no row-by-row fetch needed',
|
|
192
|
+
'delete_from: bulk delete with WHERE clause',
|
|
193
|
+
'RecordInsertList: use when constructing records in code (not from another table)',
|
|
194
|
+
'RecordInsertList.add() → insertDatabase() at the end — single round-trip',
|
|
195
|
+
'Set-based operations skip insert/update/delete overrides — call skipDatabaseLog, skipDataMethods, etc. only when safe',
|
|
196
|
+
'If table has overridden insert()/update()/delete(), set-based falls back to row-by-row unless you call skipDataMethods(true)',
|
|
197
|
+
'BP rule: BPCheckNestedLoopinCode — NEVER nest while-select inside another while-select; use joins instead',
|
|
198
|
+
],
|
|
199
|
+
examples: [
|
|
200
|
+
{
|
|
201
|
+
label: 'update_recordset',
|
|
202
|
+
code: `update_recordset custTable
|
|
203
|
+
setting CreditMax = 0
|
|
204
|
+
where custTable.Blocked == CustVendorBlocked::All;`,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
label: 'insert_recordset',
|
|
208
|
+
code: `insert_recordset tmpTable (AccountNum, Name)
|
|
209
|
+
select AccountNum, Name
|
|
210
|
+
from custTable
|
|
211
|
+
where custTable.CustGroup == 'DOM';`,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
label: 'RecordInsertList',
|
|
215
|
+
code: `RecordInsertList insertList = new RecordInsertList(tableNum(MyTmpTable));
|
|
216
|
+
MyTmpTable tmp;
|
|
217
|
+
|
|
218
|
+
while select custTable
|
|
219
|
+
where custTable.CustGroup == 'DOM'
|
|
220
|
+
{
|
|
221
|
+
tmp.clear();
|
|
222
|
+
tmp.AccountNum = custTable.AccountNum;
|
|
223
|
+
tmp.Name = custTable.Name;
|
|
224
|
+
insertList.add(tmp);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
insertList.insertDatabase();`,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
related: ['transactions', 'query-patterns'],
|
|
231
|
+
},
|
|
232
|
+
// ── Query Patterns ──────────────────────────────────────────────────────
|
|
233
|
+
{
|
|
234
|
+
id: 'query-patterns',
|
|
235
|
+
title: 'Query Patterns & Select Statements',
|
|
236
|
+
keywords: ['query', 'select', 'while select', 'join', 'exists join', 'notexists join', 'outer join', 'firstonly', 'firstfast', 'forceplaceholders', 'forceselectorder', 'index hint', 'crosscompany'],
|
|
237
|
+
summary: 'X++ select statements support joins, aggregates, and query hints. ' +
|
|
238
|
+
'Use exists join for filtering, outer join for optional data, firstonly for single records.',
|
|
239
|
+
rules: [
|
|
240
|
+
'Use firstonly when you need exactly one record — avoids full table scan',
|
|
241
|
+
'Use exists join (not inner join) when you only need to check existence from the joined table',
|
|
242
|
+
'Use notexists join for "does not exist" conditions',
|
|
243
|
+
'Avoid nested while-select loops — use joins in a single select instead',
|
|
244
|
+
'NEVER call functions directly in WHERE conditions — assign to a variable first (performance + readability)',
|
|
245
|
+
'crosscompany keyword: use for cross-company queries, pass container of companies',
|
|
246
|
+
'forceplaceholders: use in batch operations to get parameterized SQL plans',
|
|
247
|
+
'forceselectorder: use only when you know the optimizer picks a wrong plan',
|
|
248
|
+
'index hint: last resort — prefer letting the optimizer choose',
|
|
249
|
+
'SysQuery class: use for building dynamic query objects (QueryBuildRange, QueryBuildDataSource)',
|
|
250
|
+
'QueryRun: use for executing query objects, supports prompt() for user dialog',
|
|
251
|
+
],
|
|
252
|
+
examples: [
|
|
253
|
+
{
|
|
254
|
+
label: 'exists join',
|
|
255
|
+
code: `CustTable custTable;
|
|
256
|
+
CustTrans custTrans;
|
|
257
|
+
// ✅ Assign to variable before WHERE — never call functions directly in WHERE conditions
|
|
258
|
+
TransDate cutoffDate = DateTimeUtil::getSystemDate(DateTimeUtil::getUserPreferredTimeZone()) - 30;
|
|
259
|
+
|
|
260
|
+
while select AccountNum, Name from custTable
|
|
261
|
+
exists join custTrans
|
|
262
|
+
where custTrans.AccountNum == custTable.AccountNum
|
|
263
|
+
&& custTrans.TransDate >= cutoffDate
|
|
264
|
+
{
|
|
265
|
+
info(strFmt('%1 - %2', custTable.AccountNum, custTable.Name));
|
|
266
|
+
}`,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
label: 'SysQuery dynamic range',
|
|
270
|
+
code: `Query query = new Query(queryStr(CustTableListPage));
|
|
271
|
+
QueryBuildDataSource qbds = query.dataSourceTable(tableNum(CustTable));
|
|
272
|
+
SysQuery::findOrCreateRange(qbds, fieldNum(CustTable, CustGroup)).value('DOM');
|
|
273
|
+
QueryRun qr = new QueryRun(query);
|
|
274
|
+
|
|
275
|
+
while (qr.next())
|
|
276
|
+
{
|
|
277
|
+
CustTable custTable = qr.get(tableNum(CustTable));
|
|
278
|
+
// process record
|
|
279
|
+
}`,
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
related: ['set-based', 'performance'],
|
|
283
|
+
},
|
|
284
|
+
// ── Chain of Command ────────────────────────────────────────────────────
|
|
285
|
+
{
|
|
286
|
+
id: 'coc',
|
|
287
|
+
title: 'Chain of Command (CoC) Extensions',
|
|
288
|
+
keywords: ['coc', 'chain of command', 'extension', 'extensionof', 'next', 'wrapping', 'overlay', 'overlayer', 'overlayering'],
|
|
289
|
+
summary: 'CoC replaces overlayering (which is completely blocked in D365FO). ' +
|
|
290
|
+
'Extension classes wrap methods by calling next to invoke the original + other extensions.',
|
|
291
|
+
migration: {
|
|
292
|
+
ax2012: 'Overlayering: modify the original class/method directly in the same layer',
|
|
293
|
+
d365fo: 'CoC: [ExtensionOf(classStr(Original))] final class Original_Extension { method() { next method(); } }',
|
|
294
|
+
},
|
|
295
|
+
rules: [
|
|
296
|
+
'Extension class MUST be [ExtensionOf(classStr/tableStr/formStr(Target))]',
|
|
297
|
+
'Extension class MUST be final',
|
|
298
|
+
'Method signature MUST match the original exactly (use get_method(include="signature") tool)',
|
|
299
|
+
'ALWAYS call next <methodName>() — skipping it breaks the chain for other extensions',
|
|
300
|
+
'Cannot access private members of the original class',
|
|
301
|
+
'Can wrap public and protected methods — instance AND static (a static wrapper must repeat the "static" modifier); cannot wrap private methods or constructors. Forms cannot have static-method CoC',
|
|
302
|
+
'For form static methods or fire-and-forget scenarios where CoC is not possible, use [PostHandlerFor] / [PreHandlerFor] event handlers instead',
|
|
303
|
+
'For the strict wrapper non-negotiables (default parameters, next placement, [Wrappable]/[Hookable]) see the coc-authoring topic',
|
|
304
|
+
'Naming: <TargetClass>_<YourModel>_Extension (e.g. SalesTable_ContosoExt_Extension)',
|
|
305
|
+
'Form CoC: [ExtensionOf(formStr(CustTable))] — wraps form methods like init(), run()',
|
|
306
|
+
'Form datasource CoC: wrap datasource methods like init(), validateWrite()',
|
|
307
|
+
],
|
|
308
|
+
examples: [
|
|
309
|
+
{
|
|
310
|
+
label: 'Table method CoC',
|
|
311
|
+
code: `[ExtensionOf(tableStr(CustTable))]
|
|
312
|
+
final class CustTable_MyModel_Extension
|
|
313
|
+
{
|
|
314
|
+
/// <summary>
|
|
315
|
+
/// Adds custom validation for credit limit.
|
|
316
|
+
/// </summary>
|
|
317
|
+
public boolean validateWrite()
|
|
318
|
+
{
|
|
319
|
+
boolean ret = next validateWrite();
|
|
320
|
+
|
|
321
|
+
if (ret && this.CreditMax > 1000000)
|
|
322
|
+
{
|
|
323
|
+
ret = checkFailed("@MyModel:CreditLimitExceeded");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return ret;
|
|
327
|
+
}
|
|
328
|
+
}`,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
label: 'Class method CoC',
|
|
332
|
+
code: `[ExtensionOf(classStr(SalesFormLetter))]
|
|
333
|
+
final class SalesFormLetter_MyModel_Extension
|
|
334
|
+
{
|
|
335
|
+
/// <summary>
|
|
336
|
+
/// Extends posting logic with custom dimension validation.
|
|
337
|
+
/// </summary>
|
|
338
|
+
protected void postInvoice()
|
|
339
|
+
{
|
|
340
|
+
// Pre-processing
|
|
341
|
+
this.myValidateDimensions();
|
|
342
|
+
|
|
343
|
+
next postInvoice();
|
|
344
|
+
|
|
345
|
+
// Post-processing
|
|
346
|
+
this.myUpdateCustomStatus();
|
|
347
|
+
}
|
|
348
|
+
}`,
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
related: ['event-handlers', 'form-patterns', 'coc-authoring'],
|
|
352
|
+
},
|
|
353
|
+
// ── Event Handlers ──────────────────────────────────────────────────────
|
|
354
|
+
{
|
|
355
|
+
id: 'event-handlers',
|
|
356
|
+
title: 'Event Handlers & Delegates',
|
|
357
|
+
keywords: ['event', 'handler', 'delegate', 'dataeventhandler', 'subscribesto', 'prehandlerfor', 'posthandlerfor', 'on inserting', 'on inserted', 'on validating', 'on validated'],
|
|
358
|
+
summary: 'Event handlers subscribe to table data events, class delegates, or pre/post method events. ' +
|
|
359
|
+
'Use when CoC is not possible (static methods, or when you need fire-and-forget).',
|
|
360
|
+
rules: [
|
|
361
|
+
'Table data events: use [DataEventHandler(tableStr(X), DataEventType::Inserted)]',
|
|
362
|
+
'Data event types: Inserting, Inserted, Updating, Updated, Deleting, Deleted, ValidatedWrite, ValidatedDelete, ValidatingWrite, ValidatingDelete, etc.',
|
|
363
|
+
'Custom delegates: use [SubscribesTo(classStr(X), delegateStr(X, myDelegate))]',
|
|
364
|
+
'Pre/Post: use [PreHandlerFor(classStr(X), methodStr(X, myMethod))] or PostHandlerFor',
|
|
365
|
+
'Handler methods MUST be static void',
|
|
366
|
+
'DataEventHandler signature: static void handler(Common _sender, DataEventArgs _e)',
|
|
367
|
+
'Validating events: cast _e to ValidateEventArgs, call _e.parmValidateResult(false) to fail',
|
|
368
|
+
'NEVER use SubscribesTo + delegateStr for standard table data events — use DataEventHandler',
|
|
369
|
+
'REUSE BEFORE CREATING: call extension_info(mode="events") first — if a handler class for the target already exists in the custom model, add the new handler method there instead of creating a parallel class',
|
|
370
|
+
'Both _EH and _EventHandler handler-class naming styles exist — follow the style already used in the model; never introduce a feature-named handler class (<Form>_<Feature>_EH) unless the user explicitly asks for a separate class',
|
|
371
|
+
],
|
|
372
|
+
examples: [
|
|
373
|
+
{
|
|
374
|
+
label: 'Table data event handler',
|
|
375
|
+
code: `class CustTableEventHandler
|
|
376
|
+
{
|
|
377
|
+
[DataEventHandler(tableStr(CustTable), DataEventType::Inserting)]
|
|
378
|
+
public static void onInserting(Common _sender, DataEventArgs _e)
|
|
379
|
+
{
|
|
380
|
+
CustTable custTable = _sender as CustTable;
|
|
381
|
+
// Set default values before insert
|
|
382
|
+
if (!custTable.CreditMax)
|
|
383
|
+
{
|
|
384
|
+
custTable.CreditMax = 5000;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
[DataEventHandler(tableStr(CustTable), DataEventType::ValidatingWrite)]
|
|
389
|
+
public static void onValidatingWrite(Common _sender, DataEventArgs _e)
|
|
390
|
+
{
|
|
391
|
+
ValidateEventArgs validateArgs = _e as ValidateEventArgs;
|
|
392
|
+
CustTable custTable = _sender as CustTable;
|
|
393
|
+
|
|
394
|
+
if (custTable.CreditMax > 1000000)
|
|
395
|
+
{
|
|
396
|
+
validateArgs.parmValidateResult(
|
|
397
|
+
checkFailed("@MyModel:CreditLimitExceeded"));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}`,
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
related: ['coc'],
|
|
404
|
+
},
|
|
405
|
+
// ── Data Entities ───────────────────────────────────────────────────────
|
|
406
|
+
{
|
|
407
|
+
id: 'data-entities',
|
|
408
|
+
title: 'Data Entities & OData',
|
|
409
|
+
keywords: ['data entity', 'odata', 'integration', 'import', 'export', 'dmf', 'data management', 'aif', 'composite entity', 'staging'],
|
|
410
|
+
summary: 'Data entities replace AIF document services. They provide a single contract for import/export/OData. ' +
|
|
411
|
+
'Entity = virtual table backed by one or more real tables with field mappings.',
|
|
412
|
+
migration: {
|
|
413
|
+
ax2012: 'AIF Document Services (AxdSalesOrder), custom services',
|
|
414
|
+
d365fo: 'Data entities + OData endpoints + Data Management Framework (DMF)',
|
|
415
|
+
},
|
|
416
|
+
rules: [
|
|
417
|
+
'Data entity = view + insert/update/delete logic mapped to underlying tables',
|
|
418
|
+
'Primary data source: the "root" table (e.g. CustTable for CustCustomerV3Entity)',
|
|
419
|
+
'IsPublic = Yes: exposes as OData endpoint at /data/EntityNamePlural',
|
|
420
|
+
'Staging table: auto-generated for DMF import/export — name is <Entity>Staging',
|
|
421
|
+
'Entity category: Document (header+lines), Master (single table), Reference, Transaction, Parameter',
|
|
422
|
+
'Use AutoIdentification field group for natural key (maps to AlternateKey)',
|
|
423
|
+
'Mapping: entity fields map to data source fields — handle computed/unmapped columns via virtual fields + postLoad/mapEntityToDataSource',
|
|
424
|
+
'Composite entity: wraps multiple entities for header+lines import (e.g. SalesOrderHeaderV2Entity + SalesOrderLineV2Entity)',
|
|
425
|
+
'NEVER create AIF document services in D365FO — always use data entities',
|
|
426
|
+
],
|
|
427
|
+
related: ['query-patterns'],
|
|
428
|
+
},
|
|
429
|
+
// ── Temp Tables ─────────────────────────────────────────────────────────
|
|
430
|
+
{
|
|
431
|
+
id: 'temp-tables',
|
|
432
|
+
title: 'Temporary Tables (TempDB vs InMemory)',
|
|
433
|
+
keywords: ['temp', 'temporary', 'tempdb', 'inmemory', 'tmp', 'report', 'ssrs'],
|
|
434
|
+
summary: 'D365FO has two types of temp tables: TempDB (SQL Server tempdb) and InMemory (ISAM client-side). ' +
|
|
435
|
+
'TempDB is almost always preferred. InMemory is legacy from AX 2009.',
|
|
436
|
+
migration: {
|
|
437
|
+
ax2012: 'Table property Temporary=Yes → InMemory temp table',
|
|
438
|
+
d365fo: 'TableType=TempDB (preferred) or TableType=InMemory (legacy)',
|
|
439
|
+
},
|
|
440
|
+
rules: [
|
|
441
|
+
'TempDB: stored in SQL Server tempdb — supports efficient joins and set-based operations',
|
|
442
|
+
'InMemory: ISAM file on AOS tier — joins and set-based operations are SLOW',
|
|
443
|
+
'SSRS reports: ALWAYS use TempDB for report temp tables (SRSTmpTable pattern)',
|
|
444
|
+
'TempDB tables: scoped to the session/method — automatically dropped when no longer referenced',
|
|
445
|
+
'TempDB supports insert_recordset, update_recordset, delete_from — InMemory does NOT',
|
|
446
|
+
'To pass TempDB data between tiers: use container or RecordSortedList',
|
|
447
|
+
'TableType is NOT the same as TableGroup — TableType=TempDB, TableGroup=Main/Transaction/etc.',
|
|
448
|
+
'Default TableType is RegularTable (permanent) — omit from XML for regular tables',
|
|
449
|
+
],
|
|
450
|
+
examples: [
|
|
451
|
+
{
|
|
452
|
+
label: 'TempDB table for SSRS report',
|
|
453
|
+
code: `// Table definition: TableType = TempDB, TableGroup = Main
|
|
454
|
+
// Fields: ItemId (EDT: ItemId), ItemName (EDT: ItemName), Qty (EDT: Qty)
|
|
455
|
+
|
|
456
|
+
// In the DP class:
|
|
457
|
+
[SrsReportParameterAttribute(classStr(MyReportContract))]
|
|
458
|
+
class MyReportDP extends SrsReportDataProviderBase
|
|
459
|
+
{
|
|
460
|
+
MyReportTmp tmpTable;
|
|
461
|
+
|
|
462
|
+
[SRSReportDataSetAttribute(tableStr(MyReportTmp))]
|
|
463
|
+
public MyReportTmp getMyReportTmp()
|
|
464
|
+
{
|
|
465
|
+
select tmpTable;
|
|
466
|
+
return tmpTable;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
public void processReport()
|
|
470
|
+
{
|
|
471
|
+
MyReportContract contract = this.parmDataContract() as MyReportContract;
|
|
472
|
+
this.populateTmpTable(contract);
|
|
473
|
+
}
|
|
474
|
+
}`,
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
related: ['sysoperation', 'set-based'],
|
|
478
|
+
},
|
|
479
|
+
// ── Error Handling ──────────────────────────────────────────────────────
|
|
480
|
+
{
|
|
481
|
+
id: 'error-handling',
|
|
482
|
+
title: 'Error Handling Patterns',
|
|
483
|
+
keywords: ['error', 'exception', 'try', 'catch', 'throw', 'info', 'warning', 'checkfailed', 'infolog', 'global', 'clrcreatedexception'],
|
|
484
|
+
summary: 'X++ uses a structured exception model with mandatory labels for all user-facing messages.',
|
|
485
|
+
rules: [
|
|
486
|
+
'ALWAYS use label references in info(), warning(), error() — never hardcoded strings (BPErrorLabelIsText)',
|
|
487
|
+
'checkFailed(): posts error to infolog AND returns false — use in validateWrite/validateField',
|
|
488
|
+
'Return pattern: ret = ret && checkFailed("@Label:Message") — accumulates all errors before returning',
|
|
489
|
+
'Exception types: Error, Warning, Info, Deadlock, UpdateConflict, DuplicateKeyConflict, CLRError',
|
|
490
|
+
'Catch specific exceptions — avoid bare catch without type',
|
|
491
|
+
'CLR interop: catch(Exception::CLRError) then use CLRInterop::getLastException() for details',
|
|
492
|
+
'Global::error() = same as error() — both post to infolog',
|
|
493
|
+
'NEVER swallow exceptions silently — at minimum log them',
|
|
494
|
+
'After catching UpdateConflict: retry or throw UpdateConflictNotRecovered',
|
|
495
|
+
],
|
|
496
|
+
examples: [
|
|
497
|
+
{
|
|
498
|
+
label: 'validateWrite pattern',
|
|
499
|
+
code: `public boolean validateWrite()
|
|
500
|
+
{
|
|
501
|
+
boolean ret = super();
|
|
502
|
+
|
|
503
|
+
ret = ret && this.AccountNum
|
|
504
|
+
? true
|
|
505
|
+
: checkFailed("@MyModel:AccountNumRequired");
|
|
506
|
+
|
|
507
|
+
ret = ret && this.CreditMax >= 0
|
|
508
|
+
? true
|
|
509
|
+
: checkFailed("@MyModel:CreditMaxNegative");
|
|
510
|
+
|
|
511
|
+
return ret;
|
|
512
|
+
}`,
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
related: ['transactions', 'labels'],
|
|
516
|
+
},
|
|
517
|
+
// ── Labels ──────────────────────────────────────────────────────────────
|
|
518
|
+
{
|
|
519
|
+
id: 'labels',
|
|
520
|
+
title: 'Labels & Localization',
|
|
521
|
+
keywords: ['label', 'localization', 'translation', 'literalstr', 'strfmt', 'bperrorlabelistext', 'hardcoded'],
|
|
522
|
+
summary: 'Every user-visible string MUST be a label. D365FO enforces this via BP rule BPErrorLabelIsText.',
|
|
523
|
+
rules: [
|
|
524
|
+
'ALL user-facing text must use labels: @ModelName:LabelId',
|
|
525
|
+
'BP check BPErrorLabelIsText fires on any hardcoded string in info/warning/error/dialog',
|
|
526
|
+
'Label ID naming: describe the MEANING, no model prefix (e.g. CustomerName, not ContosoExtCustomerName)',
|
|
527
|
+
'Label file: the prefix comes from the file name (e.g. @ContosoExt:CustomerName)',
|
|
528
|
+
'Use strFmt() for parameterized messages: strFmt("@MyModel:ItemNotFound", itemId)',
|
|
529
|
+
'Use literalStr() when BP complains about strFmt argument not being a label — wraps non-label string safely',
|
|
530
|
+
'labels(action="search") before labels(action="create") — avoid duplicates',
|
|
531
|
+
'Provide translations for all required languages in labels(action="create")',
|
|
532
|
+
],
|
|
533
|
+
related: ['error-handling'],
|
|
534
|
+
},
|
|
535
|
+
// ── Deprecated APIs ─────────────────────────────────────────────────────
|
|
536
|
+
{
|
|
537
|
+
id: 'deprecated',
|
|
538
|
+
title: 'Deprecated APIs & Replacements',
|
|
539
|
+
keywords: ['deprecated', 'obsolete', 'sysobsolete', 'today', 'curext', 'infolog', 'fieldnum', 'aif'],
|
|
540
|
+
summary: 'D365FO deprecates many AX2012 APIs. Using deprecated APIs triggers BP warnings/errors.',
|
|
541
|
+
rules: [
|
|
542
|
+
'today() → DateTimeUtil::getToday(DateTimeUtil::getUserPreferredTimeZone()) — BPUpgradeCodeToday; NEVER use today() in new code (same replacement the bp-rules topic mandates)',
|
|
543
|
+
'NEVER call today() or any function directly in a WHERE condition — assign to a variable first',
|
|
544
|
+
'curext() → use Ledger::primaryForLegalEntity(CompanyInfo::findDataArea(curext()).RecId)',
|
|
545
|
+
'AIF services → Data entities + OData',
|
|
546
|
+
'RunBase → SysOperation framework',
|
|
547
|
+
'display/edit methods on forms → computed columns or data entity virtual fields',
|
|
548
|
+
'infolog.add() → info()/warning()/error() global functions',
|
|
549
|
+
'fieldnum(tableName, fieldName) → still valid but use fieldNum() macro for compile-time safety',
|
|
550
|
+
'[SysObsolete] attribute: ALWAYS read the message — it names the replacement',
|
|
551
|
+
'When get_method(include="source") returns a method with [SysObsolete], do NOT call it — use the stated replacement',
|
|
552
|
+
],
|
|
553
|
+
related: ['labels', 'data-entities', 'sysoperation'],
|
|
554
|
+
},
|
|
555
|
+
// ── Number Sequences ────────────────────────────────────────────────────
|
|
556
|
+
{
|
|
557
|
+
id: 'number-sequences',
|
|
558
|
+
title: 'Number Sequences',
|
|
559
|
+
keywords: ['number sequence', 'numberseq', 'numseq', 'voucher', 'continuous', 'scope', 'numbersequencereference'],
|
|
560
|
+
summary: 'Number sequences generate unique, configurable identifiers for master data and transactions. ' +
|
|
561
|
+
'They support scope (shared, company, legal entity) and format segments.',
|
|
562
|
+
rules: [
|
|
563
|
+
'Module class EXTENDS NumberSeqApplicationModule — exact name. ❌ NOT "NumberSequenceApplicationModule" (that class does not exist).',
|
|
564
|
+
'It is a subclass (extends), so override loadModule() and call super() at the top. ❌ NOT next() — next() is ONLY for [ExtensionOf] CoC classes, never for an extends subclass.',
|
|
565
|
+
'loadModule() registers each reference with NumberSeqDatatype::construct(), then parmDatatypeId(extendedTypeNum(MyEdt)) + parmWizardIsContinuous/parmWizardIsManual/parmWizardIsChangeDownAllowed/… , then this.create(datatype). ❌ Do NOT assign fields on a NumberSeqReference/NumberSequenceReference buffer (DataTypeId, WizardContinuous, AllowManual… are parm*() methods on NumberSeqDatatype, NOT table fields) and there is NO this.addModuleEntry().',
|
|
566
|
+
'Override numberSeqModule() to return your NumberSeqModule enum value.',
|
|
567
|
+
'A new module class is NOT auto-loaded: extend the NumberSeqModule enum and register the module via an event handler on NumberSeqGlobal (or CoC) so loadModule() runs.',
|
|
568
|
+
'Form auto-numbering: NumberSeqFormHandler::newForm(<ParametersTable>::numRef<Id>().NumberSequenceId, element, <datasource>, fieldNum(<Table>, <Id>)). First arg is a RefRecId via the .NumberSequenceId field. ❌ NOT .NumberSequence and ❌ NOT a string code.',
|
|
569
|
+
'Runtime fetch: NumberSeqReference::findReference(extendedTypeNum(MyId)) → NumberSeq::newGetNum(ref) → .num(); call .abort() to release on rollback.',
|
|
570
|
+
'Continuous (no gaps): perf cost — only when legally required. Non-continuous (default) allows gaps, faster for internal IDs.',
|
|
571
|
+
'Scope: DataArea (per-company), Global (cross-company), OperatingUnit.',
|
|
572
|
+
'Verify exact parm*() names against the SDK with get_object_info(objectType="class", name="NumberSeqDatatype") before relying on them.',
|
|
573
|
+
],
|
|
574
|
+
examples: [
|
|
575
|
+
{
|
|
576
|
+
label: 'Module class — register the reference in loadModule() (correct API)',
|
|
577
|
+
code: `public class NumberSeqModuleMyRent extends NumberSeqApplicationModule
|
|
578
|
+
{
|
|
579
|
+
protected void loadModule()
|
|
580
|
+
{
|
|
581
|
+
NumberSeqDatatype datatype = NumberSeqDatatype::construct();
|
|
582
|
+
datatype.parmDatatypeId(extendedTypeNum(MyRentEquipmentId));
|
|
583
|
+
datatype.parmReferenceHelp(literalStr("Equipment ID"));
|
|
584
|
+
datatype.parmWizardIsContinuous(false);
|
|
585
|
+
datatype.parmWizardIsManual(NoYes::No);
|
|
586
|
+
datatype.parmWizardIsChangeDownAllowed(NoYes::Yes);
|
|
587
|
+
datatype.parmWizardIsChangeUpAllowed(NoYes::Yes);
|
|
588
|
+
datatype.parmWizardHighest(0);
|
|
589
|
+
datatype.parmSortField(1);
|
|
590
|
+
datatype.addParameterType(NumberSeqParameterType::DataArea, true, false);
|
|
591
|
+
this.create(datatype); // NOT a NumberSeqReference field assignment
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
public NumberSeqModule numberSeqModule()
|
|
595
|
+
{
|
|
596
|
+
return NumberSeqModule::MyRent; // your NumberSeqModule enum value
|
|
597
|
+
}
|
|
598
|
+
}`,
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
label: 'Form auto-numbering handler',
|
|
602
|
+
code: `NumberSeqFormHandler numberSeqFormHandler; // form member
|
|
603
|
+
|
|
604
|
+
public NumberSeqFormHandler numberSeqFormHandler()
|
|
605
|
+
{
|
|
606
|
+
if (!numberSeqFormHandler)
|
|
607
|
+
{
|
|
608
|
+
numberSeqFormHandler = NumberSeqFormHandler::newForm(
|
|
609
|
+
MyRentParameters::numRefMyRentEquipmentId().NumberSequenceId, // RefRecId, not a string
|
|
610
|
+
element,
|
|
611
|
+
MyRentEquipmentTable_ds,
|
|
612
|
+
fieldNum(MyRentEquipmentTable, MyRentEquipmentId));
|
|
613
|
+
}
|
|
614
|
+
return numberSeqFormHandler;
|
|
615
|
+
}`,
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
label: 'Fetching next number at runtime',
|
|
619
|
+
code: `NumberSequenceReference numSeqRef =
|
|
620
|
+
NumberSeqReference::findReference(extendedTypeNum(MyRentEquipmentId));
|
|
621
|
+
|
|
622
|
+
NumberSeq numSeq = NumberSeq::newGetNum(numSeqRef);
|
|
623
|
+
MyRentEquipmentId newId = numSeq.num();
|
|
624
|
+
|
|
625
|
+
// If the insert is rolled back, release the number:
|
|
626
|
+
// numSeq.abort();`,
|
|
627
|
+
},
|
|
628
|
+
],
|
|
629
|
+
related: ['transactions'],
|
|
630
|
+
},
|
|
631
|
+
// ── Workflow Development ────────────────────────────────────────────────
|
|
632
|
+
{
|
|
633
|
+
id: 'workflow',
|
|
634
|
+
title: 'Workflow Development (WorkflowDocument, WorkflowType)',
|
|
635
|
+
keywords: ['workflow', 'workflowdocument', 'workflowtype', 'approval', 'task', 'submit', 'cansubmittoworkflow'],
|
|
636
|
+
summary: 'D365FO workflows are built from a Document (condition fields), a Type, Approvals/Tasks, ' +
|
|
637
|
+
'and event handlers. Structure: Document → Type → Approvals/Tasks → EventHandlers.',
|
|
638
|
+
rules: [
|
|
639
|
+
'Key X++ base classes: WorkflowDocument and WorkflowType — Approvals and Tasks are AOT elements (their code lives in generated event handlers), NOT X++ base classes (there is no WorkflowTask class, and WorkflowApproval is only a field)',
|
|
640
|
+
'WorkflowDocument subclass defines which table fields are available as workflow conditions',
|
|
641
|
+
'SubmitToWorkflowMenuItem action menu item provides the submit button on the form',
|
|
642
|
+
'canSubmitToWorkflow() method on the table controls when submit is enabled',
|
|
643
|
+
'Approval/Task event handlers use WorkflowWorkItemActionManager for complete/reject/delegate',
|
|
644
|
+
'Call search("WorkflowDocument", type="class") for real implementations to model after',
|
|
645
|
+
],
|
|
646
|
+
related: ['event-handlers', 'sysoperation'],
|
|
647
|
+
},
|
|
648
|
+
// ── Best Practice (BP) checker rules ────────────────────────────────────
|
|
649
|
+
{
|
|
650
|
+
id: 'bp-rules',
|
|
651
|
+
title: 'Best Practice (BP) Rules — Generated Code Must Be BP-Clean',
|
|
652
|
+
keywords: ['bp', 'best practice', 'bpupgradecodetoday', 'bperrorlabelistext', 'bperroredtnotmigrated', 'bpcheck', 'xmldoc', 'doc comment', 'alternate key', 'edt extension', 'stringsize', 'hardcoded string'],
|
|
653
|
+
summary: 'All generated X++ and metadata must pass the D365FO Best Practice checker without warnings. ' +
|
|
654
|
+
'These are the BP rules the offline validator (validate_code(mode="syntax")) and xppbp.exe enforce most often.',
|
|
655
|
+
rules: [
|
|
656
|
+
'BPUpgradeCodeToday: NEVER use today() — use DateTimeUtil::getToday(DateTimeUtil::getUserPreferredTimeZone()); applies to default parameters, comparisons, and queries',
|
|
657
|
+
'NEVER call a function inside a WHERE condition — assign to a local variable first, then use the variable',
|
|
658
|
+
'BPErrorLabelIsText: no literal strings in Info()/warning()/error() or labels — use @ModelName:LabelId; check labels(action="search") first, create with labels(action="create")',
|
|
659
|
+
'BPErrorUnknownLabel: labels(action="create") BEFORE referencing the label in code; labels adds AxLabelFile descriptors to the VS project automatically (addToProject=true)',
|
|
660
|
+
'BPErrorEDTNotMigrated: a field whose EDT carries an implicit relation (ItemId → InventTable) needs an explicit <AxTableRelation>; generate auto-detects these — manual field adds need a matching relation too',
|
|
661
|
+
'BPCheckNestedLoopinCode: never nest while select inside while select — use join, temp table, or Map pre-load; report DP classes use insert_recordset or a single joined query',
|
|
662
|
+
'BPCheckAlternateKeyAbsent: every table needs at least one index with <AlternateKey>Yes</AlternateKey> (generate adds it automatically)',
|
|
663
|
+
'BPXmlDocNoDocumentationComments: every public/protected class and method needs a MEANINGFUL /// <summary> — "MyClass class." or "validateWrite." fail BP review; describe what it does, parameters, and the semantic meaning of the return value',
|
|
664
|
+
'EDT extensions (AxEdtExtension, objectType="edt-extension") can ONLY change Label, HelpText, FormHelp, ConfigurationKey, HelpAlign, Alignment, NoOfDecimals, DecimalSeparator, SignDisplay — and only when the base EDT has IsExtensible=Yes',
|
|
665
|
+
'EDT extensions can NEVER change Extends (re-parenting) or StringSize/DisplayLength on a derived EDT — to widen a string, create a new EDT extending the existing one, or use a table extension modify-field with stringSize (mind databaseStringSize so data is not truncated)',
|
|
666
|
+
'The d365fo_file(action="modify") validator refuses illegal EDT-extension property changes up-front — relay the message verbatim, do not work around it',
|
|
667
|
+
],
|
|
668
|
+
related: ['labels', 'deprecated', 'set-based'],
|
|
669
|
+
},
|
|
670
|
+
// ── Form Patterns ───────────────────────────────────────────────────────
|
|
671
|
+
{
|
|
672
|
+
id: 'form-patterns',
|
|
673
|
+
title: 'Form Patterns & Form Extensions',
|
|
674
|
+
keywords: ['form', 'pattern', 'simplelist', 'simplelistdetails', 'detailsmaster', 'detailstransaction', 'listpage', 'dialog', 'lookup', 'formrun', 'formextension'],
|
|
675
|
+
summary: 'D365FO forms follow standard patterns enforced by the form pattern dialog. ' +
|
|
676
|
+
'Extensions add controls/overrides without modifying the original form.',
|
|
677
|
+
rules: [
|
|
678
|
+
'Standard patterns: SimpleList, SimpleListDetails, DetailsMaster, DetailsTransaction, Dialog, DropDialog, ListPage, TableOfContents, Lookup, Workspace — each defines REQUIRED containers in a REQUIRED order',
|
|
679
|
+
'NEW FORM workflow: object_patterns(domain="form", action="analyze", recommend={...}) → object_patterns(domain="form", action="spec", pattern) → generate_object(mode="scaffold", objectType="form", cloneFrom=referenceForm, tableMapping={...}) → object_patterns(domain="form", action="validate") → d365fo_file(action="create")',
|
|
680
|
+
'CLONING an existing reference form (CustGroup for SimpleList, CustTable for DetailsMaster, SalesTable for DetailsTransaction, PaymTerm for SimpleListDetails, CustParameters for TableOfContents) is the PREFERRED strategy — patterns and sub-patterns are preserved',
|
|
681
|
+
'Container sub-patterns (Pattern element on Group/TabPage): FieldsFieldGroups (fields + max 1 level of groups, NO static text/images), CustomAndQuickFilters (QuickFilter required), ToolbarAndList, SidePanel — validate with object_patterns(domain="form", action="validate")',
|
|
682
|
+
'Structural pattern violations BLOCK d365fo_file(action="create") while FORM_PATTERN_ENFORCE=true (default): wrong control order, missing required container, disallowed child type, unknown pattern/version',
|
|
683
|
+
'ALWAYS use form extensions — never modify standard forms (overlayering is blocked)',
|
|
684
|
+
'Form extension file: AxFormExtension XML — holds new controls, data sources, property overrides',
|
|
685
|
+
'Form extension class: [ExtensionOf(formStr(Target))] — holds CoC logic for form methods',
|
|
686
|
+
'Use get_object_info(objectType="form", name=formName, options={searchControl:"..."}) to find exact control names before extending',
|
|
687
|
+
'New controls: add via d365fo_file(action="modify", operation="add-control", parentControl="TabGeneral") — the control type is checked against the parent container\'s sub-pattern',
|
|
688
|
+
'Data sources: add via d365fo_file(action="modify", operation="add-data-source")',
|
|
689
|
+
'NEVER use PowerShell or read_file to inspect form XML — use get_object_info(objectType="form", name=...)',
|
|
690
|
+
'A user-provided example form is a PATTERN CONTRACT: read it with get_object_info(objectType="form", name=...), keep the same pattern family, and verify the generated form keeps the required scaffolding (datasources, design pattern/version, ActionPane/Body/Tab/FastTab/grid/QuickFilter) — missing pattern elements are a failed generation even if the XML is well-formed',
|
|
691
|
+
'Edits must be additive: never drop unrelated <Controls>, <DataSources>, <DataSourceModifications>, methods, or pattern metadata — use targeted d365fo_file(action="modify") operations and verify the diff with review_workspace_changes afterwards',
|
|
692
|
+
],
|
|
693
|
+
related: ['coc', 'event-handlers', 'formrun-lifecycle'],
|
|
694
|
+
},
|
|
695
|
+
// ── Security ────────────────────────────────────────────────────────────
|
|
696
|
+
{
|
|
697
|
+
id: 'security',
|
|
698
|
+
title: 'Security Model (Roles, Duties, Privileges)',
|
|
699
|
+
keywords: ['security', 'role', 'duty', 'privilege', 'entry point', 'permission', 'policy', 'xds', 'extensible data security'],
|
|
700
|
+
summary: 'D365FO uses Role → Duty → Privilege → Entry Point security model. ' +
|
|
701
|
+
'Privileges grant access to specific menu items (entry points).',
|
|
702
|
+
rules: [
|
|
703
|
+
'Hierarchy: Role contains Duties, Duty contains Privileges, Privilege contains Entry Points',
|
|
704
|
+
'Entry Point = menu item (Display, Output, Action) at a specific access level (Read, Update, Create, Delete)',
|
|
705
|
+
'Create separate privilege for each access level: MyFormView (Read), MyFormMaintain (Update)',
|
|
706
|
+
'Duty = business function: "Maintain customer records" → groups related privileges',
|
|
707
|
+
'Role = job function: "Accounts receivable clerk" → groups duties',
|
|
708
|
+
'Table permissions: set on the privilege entry point, cascading to related tables',
|
|
709
|
+
'XDS (Extensible Data Security): row-level security policies (AxSecurityPolicy) that filter records via a constrained query + policy context',
|
|
710
|
+
'Use security_info(mode="coverage") to check what covers a form/table/menu item',
|
|
711
|
+
'Use security_info(mode="artifact") to inspect a role/duty/privilege hierarchy',
|
|
712
|
+
'This topic is the conceptual overview — see security-privileges-duties for privilege/duty/role authoring (XML, generate_object, access levels)',
|
|
713
|
+
],
|
|
714
|
+
related: ['form-patterns', 'security-privileges-duties'],
|
|
715
|
+
},
|
|
716
|
+
// ── Performance ─────────────────────────────────────────────────────────
|
|
717
|
+
{
|
|
718
|
+
id: 'performance',
|
|
719
|
+
title: 'Performance Best Practices',
|
|
720
|
+
keywords: ['performance', 'cache', 'index', 'trace', 'sql trace', 'batch', 'async', 'recordinsertlist'],
|
|
721
|
+
summary: 'D365FO performance: use set-based operations, proper indexes, caching, and batch processing.',
|
|
722
|
+
rules: [
|
|
723
|
+
'Set-based > row-by-row: ALWAYS use insert_recordset/update_recordset/delete_from when possible',
|
|
724
|
+
'RecordInsertList: for batch insert of constructed records',
|
|
725
|
+
'CacheLookup: Found (most common), FoundAndEmpty, EntireTable (small reference tables only)',
|
|
726
|
+
'Index: every WHERE clause field should be covered by an index; check with SQL trace',
|
|
727
|
+
'firstonly/firstfast: use on single-record lookups — avoid scanning entire table',
|
|
728
|
+
'exists join over inner join: when you don\'t need columns from the joined table',
|
|
729
|
+
'Avoid nested while-select loops — flatten to a single select with joins',
|
|
730
|
+
'Batch parallelism: use SysOperationServiceController.parmExecutionMode(SysOperationExecutionMode::ScheduledBatch)',
|
|
731
|
+
'Use container or SysGlobalObjectCache for cross-call caching',
|
|
732
|
+
],
|
|
733
|
+
related: ['set-based', 'query-patterns'],
|
|
734
|
+
},
|
|
735
|
+
// ── Testing ─────────────────────────────────────────────────────────────
|
|
736
|
+
{
|
|
737
|
+
id: 'testing',
|
|
738
|
+
title: 'Unit Testing (SysTest Framework)',
|
|
739
|
+
keywords: ['test', 'unit test', 'systest', 'systestcase', 'assert', 'atl', 'acceptance test library', 'mock'],
|
|
740
|
+
summary: 'D365FO uses SysTestCase for unit tests and ATL (Acceptance Test Library) for integration tests.',
|
|
741
|
+
rules: [
|
|
742
|
+
'Test class: extends SysTestCase — must have methods starting with "test"',
|
|
743
|
+
'SysTestMethodAttribute: [SysTestMethod] on each test method',
|
|
744
|
+
'Assert methods: this.assertEquals(), this.assertTrue(), this.assertFalse(), this.assertNotNull()',
|
|
745
|
+
'setUp() / tearDown(): run before/after each test method',
|
|
746
|
+
'ATL (Acceptance Test Library): entry point is AtlDataRootNode::construct(); navigate via data.invent()/data.sales()/… and use the Creators/Commands/Queries/Specifications concepts (AtlCommand* family) — there is NO AtlScenario or AtlDataHelper class',
|
|
747
|
+
'Test data: use the ATL data root (AtlDataRootNode) creators or setUp() to create transient test records',
|
|
748
|
+
'Run with: run_systest_class MCP tool or Visual Studio Test Explorer',
|
|
749
|
+
'Naming: <TestedClass>Test (e.g. CustTableTest) — the repo systests use this suffix; pick ONE convention per model and keep it consistent',
|
|
750
|
+
'See the unit-testing topic for the detailed SysTestCase rules (transaction rollback, SysTestSuite, mocking)',
|
|
751
|
+
],
|
|
752
|
+
examples: [
|
|
753
|
+
{
|
|
754
|
+
label: 'Basic unit test',
|
|
755
|
+
code: `[SysTestTarget(classStr(MyHelper), MethodStr(MyHelper, calculateDiscount))]
|
|
756
|
+
class MyHelperTest extends SysTestCase
|
|
757
|
+
{
|
|
758
|
+
[SysTestMethod]
|
|
759
|
+
public void testCalculateDiscount_ZeroQty()
|
|
760
|
+
{
|
|
761
|
+
MyHelper helper = new MyHelper();
|
|
762
|
+
Amount result = helper.calculateDiscount(0, 100);
|
|
763
|
+
this.assertEquals(0, result, 'Discount should be 0 for zero quantity');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
[SysTestMethod]
|
|
767
|
+
public void testCalculateDiscount_LargeQty()
|
|
768
|
+
{
|
|
769
|
+
MyHelper helper = new MyHelper();
|
|
770
|
+
Amount result = helper.calculateDiscount(100, 50);
|
|
771
|
+
this.assertTrue(result > 0, 'Discount should be positive for large qty');
|
|
772
|
+
}
|
|
773
|
+
}`,
|
|
774
|
+
},
|
|
775
|
+
],
|
|
776
|
+
related: ['sysoperation'],
|
|
777
|
+
},
|
|
778
|
+
// ── Financial Dimensions ────────────────────────────────────────────────
|
|
779
|
+
{
|
|
780
|
+
id: 'financial-dimensions',
|
|
781
|
+
title: 'Financial Dimensions (DimensionAttributeValueSet)',
|
|
782
|
+
keywords: ['dimension', 'financial dimension', 'ledgerdimension', 'dimensionattributevalueset', 'dimensionattribute', 'defaultdimension', 'ledgerdimensionfacade', 'displayvalue', 'dimensiondefaultingcontroller'],
|
|
783
|
+
summary: 'Financial dimensions in D365FO are multi-part keys stored as RecId references to DimensionAttributeValueSet. ' +
|
|
784
|
+
'Never work with dimension strings directly — always use the LedgerDimensionFacade or DimensionAttributeValueSetStorage APIs.',
|
|
785
|
+
rules: [
|
|
786
|
+
'DefaultDimension field (Int64): RecId pointing to DimensionAttributeValueSet — stores the account structure combination',
|
|
787
|
+
'LedgerDimension field (Int64): RecId pointing to DimensionAttributeValue — full main account + dimensions combined',
|
|
788
|
+
'To read dimension values: use DimensionAttributeValue::find() and DimensionAttributeValueSetStorage',
|
|
789
|
+
'To create/update default dimensions: use DimensionAttributeValueSetStorage (find → addItem → save). There is no DimensionDefaultingService class',
|
|
790
|
+
'To merge two dimension sets: DimensionAttributeValueSetStorage.mergeValues()',
|
|
791
|
+
'To get the display string of a DefaultDimension: use DimensionAttributeValueSetStorage.toString()',
|
|
792
|
+
'To get the display string of a LedgerDimension: use DimensionAttributeValue::find(recId).getValue()',
|
|
793
|
+
'NEVER store dimension strings in custom fields — always use DefaultDimension (Int64 EDT) referencing DimensionAttributeValueSet',
|
|
794
|
+
'The Financial dimensions FastTab is a FORM CONTROL (DimensionEntryControl, added in the form design over the DefaultDimension field) — not a controller class you construct in init(). There is no DimensionDefaultingController',
|
|
795
|
+
'DimensionController is the abstract base behind the dimension entry controls (segment validation, account structure) — subclass it only for custom account-type controls',
|
|
796
|
+
'LedgerDimensionFacade: helper class for building/parsing ledger dimension combinations',
|
|
797
|
+
'Dimension attribute names are configurable per company — never hardcode names like "CostCenter", use DimensionAttribute::findByName()',
|
|
798
|
+
],
|
|
799
|
+
examples: [
|
|
800
|
+
{
|
|
801
|
+
label: 'Read dimension value from DefaultDimension',
|
|
802
|
+
code: `// Get all dimension values from a DefaultDimension RecId
|
|
803
|
+
DimensionAttributeValueSetStorage dimStorage =
|
|
804
|
+
DimensionAttributeValueSetStorage::find(myTable.DefaultDimension);
|
|
805
|
+
|
|
806
|
+
int dimCount = dimStorage.elements();
|
|
807
|
+
for (int i = 1; i <= dimCount; i++)
|
|
808
|
+
{
|
|
809
|
+
DimensionAttribute dimAttr = DimensionAttribute::find(dimStorage.getAttributeRecId(i));
|
|
810
|
+
DimensionAttributeValue dimVal = DimensionAttributeValue::find(dimStorage.getValueRecId(i));
|
|
811
|
+
|
|
812
|
+
info(strFmt('%1 = %2', dimAttr.Name, dimVal.getValue()));
|
|
813
|
+
}`,
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
label: 'Set a DefaultDimension value (merge pattern)',
|
|
817
|
+
code: `// Build a new dimension set with CostCenter = "100"
|
|
818
|
+
DimensionAttribute dimAttr = DimensionAttribute::findByName('CostCenter');
|
|
819
|
+
if (dimAttr.RecId)
|
|
820
|
+
{
|
|
821
|
+
DimensionAttributeValue dimAttrValue =
|
|
822
|
+
DimensionAttributeValue::findByDimensionAttributeAndValue(dimAttr, '100', false, true);
|
|
823
|
+
|
|
824
|
+
DimensionAttributeValueSetStorage dimStorage =
|
|
825
|
+
DimensionAttributeValueSetStorage::find(myTable.DefaultDimension);
|
|
826
|
+
dimStorage.addItem(dimAttrValue);
|
|
827
|
+
|
|
828
|
+
ttsbegin;
|
|
829
|
+
myTable.DefaultDimension = dimStorage.save();
|
|
830
|
+
myTable.update();
|
|
831
|
+
ttscommit;
|
|
832
|
+
}`,
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
label: 'Default a dimension value on write (CoC on the table)',
|
|
836
|
+
code: `// The Financial dimensions FastTab itself is a DimensionEntryControl placed
|
|
837
|
+
// in the form design; from X++ you only touch the DefaultDimension value.
|
|
838
|
+
[ExtensionOf(tableStr(MyTable))]
|
|
839
|
+
final class MyTable_MyModel_Extension
|
|
840
|
+
{
|
|
841
|
+
public void insert()
|
|
842
|
+
{
|
|
843
|
+
DimensionAttributeValueSetStorage dimStorage;
|
|
844
|
+
DimensionAttribute dimAttribute;
|
|
845
|
+
DimensionAttributeValue dimValue;
|
|
846
|
+
|
|
847
|
+
dimStorage = DimensionAttributeValueSetStorage::find(this.DefaultDimension);
|
|
848
|
+
|
|
849
|
+
// Never hardcode the attribute name in a literal used for logic —
|
|
850
|
+
// look it up so a renamed dimension fails loudly.
|
|
851
|
+
dimAttribute = DimensionAttribute::findByName('CostCenter');
|
|
852
|
+
|
|
853
|
+
if (dimAttribute.RecId && !dimStorage.containsDimensionAttribute(dimAttribute.RecId))
|
|
854
|
+
{
|
|
855
|
+
dimValue = DimensionAttributeValue::findByDimensionAttributeAndValue(
|
|
856
|
+
dimAttribute,
|
|
857
|
+
this.MyCostCentreCode,
|
|
858
|
+
false, // _forUpdate
|
|
859
|
+
true); // _createIfNecessary
|
|
860
|
+
|
|
861
|
+
dimStorage.addItem(dimValue);
|
|
862
|
+
this.DefaultDimension = dimStorage.save();
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
next insert();
|
|
866
|
+
}
|
|
867
|
+
}`,
|
|
868
|
+
},
|
|
869
|
+
],
|
|
870
|
+
related: ['coc', 'event-handlers'],
|
|
871
|
+
},
|
|
872
|
+
// ── Posting Engine ──────────────────────────────────────────────────────
|
|
873
|
+
{
|
|
874
|
+
id: 'posting-engine',
|
|
875
|
+
title: 'Posting Engine (LedgerVoucher / SubledgerJournalizer)',
|
|
876
|
+
keywords: ['posting', 'ledger', 'voucher', 'ledgervoucher', 'subledgerjournalizer', 'journalizer', 'accounting', 'ledgerpostingtype', 'axbc', 'subledger'],
|
|
877
|
+
summary: 'D365FO posting uses SubledgerJournalizer to create subledger entries that are transferred to General Ledger via the Accounting Framework. ' +
|
|
878
|
+
'Never insert into the GL entry tables directly — always go through the posting framework.',
|
|
879
|
+
rules: [
|
|
880
|
+
'NEVER insert into GeneralJournalEntry / GeneralJournalAccountEntry / SubledgerJournalAccountEntry directly — use SubledgerJournalizer or the LedgerVoucher API (the AX2012 LedgerTrans table no longer exists)',
|
|
881
|
+
'SubledgerJournalizer: modern API for creating accounting entries (replaces LedgerVoucher in new modules)',
|
|
882
|
+
'LedgerVoucher: legacy but still valid for most standard modules (SalesOrder, PurchOrder posting)',
|
|
883
|
+
'LedgerVoucher API shape: LedgerVoucher::newLedgerPost() → LedgerVoucherObject::newVoucher() → addVoucher() → LedgerVoucherTransObject::newTransactionAmountDefault() per line → addTrans() → end()',
|
|
884
|
+
'AxBC classes (AxSalesTable, AxSalesLine, etc.): business component wrappers for posting — extend via CoC, not direct modification',
|
|
885
|
+
'LedgerPostingType (base enum) selects which posting profile a voucher line hits — pass it to newTransactionAmountDefault()',
|
|
886
|
+
'Amounts are signed: a debit is a positive Money, the balancing credit the same amount negated. LedgerVoucher.end() fails if the voucher does not balance',
|
|
887
|
+
'Currency conversion goes through CurrencyExchangeHelper::newExchangeDate(Ledger::current(), transDate) — never hand-compute the accounting-currency amount',
|
|
888
|
+
'Always use ledgerDimension (LedgerDimensionAccount, not defaultDimension) for posting — it combines main account + dimensions',
|
|
889
|
+
'Posting validation: override validate() in AxBC class via CoC — return error() to stop posting',
|
|
890
|
+
],
|
|
891
|
+
examples: [
|
|
892
|
+
{
|
|
893
|
+
label: 'Create custom voucher with LedgerVoucher',
|
|
894
|
+
code: `LedgerVoucher ledgerVoucher;
|
|
895
|
+
LedgerVoucherObject voucherObj;
|
|
896
|
+
LedgerVoucherTransObject debit, credit;
|
|
897
|
+
CurrencyExchangeHelper exchangeHelper;
|
|
898
|
+
|
|
899
|
+
ttsbegin;
|
|
900
|
+
|
|
901
|
+
ledgerVoucher = LedgerVoucher::newLedgerPost(
|
|
902
|
+
DetailSummary::Detail,
|
|
903
|
+
SysModule::Ledger,
|
|
904
|
+
''); // voucher series: '' = module default
|
|
905
|
+
|
|
906
|
+
voucherObj = LedgerVoucherObject::newVoucher(
|
|
907
|
+
voucher, // Voucher number (from the number sequence)
|
|
908
|
+
transDate,
|
|
909
|
+
SysModule::Ledger,
|
|
910
|
+
LedgerTransType::None);
|
|
911
|
+
ledgerVoucher.addVoucher(voucherObj);
|
|
912
|
+
|
|
913
|
+
exchangeHelper = CurrencyExchangeHelper::newExchangeDate(Ledger::current(), transDate);
|
|
914
|
+
|
|
915
|
+
// Debit line — positive amount
|
|
916
|
+
debit = LedgerVoucherTransObject::newTransactionAmountDefault(
|
|
917
|
+
voucherObj,
|
|
918
|
+
LedgerPostingType::LedgerJournal,
|
|
919
|
+
ledgerDimension,
|
|
920
|
+
currencyCode,
|
|
921
|
+
amount,
|
|
922
|
+
exchangeHelper);
|
|
923
|
+
ledgerVoucher.addTrans(debit);
|
|
924
|
+
|
|
925
|
+
// Credit line — same amount, negated, so the voucher balances
|
|
926
|
+
credit = LedgerVoucherTransObject::newTransactionAmountDefault(
|
|
927
|
+
voucherObj,
|
|
928
|
+
LedgerPostingType::LedgerJournal,
|
|
929
|
+
offsetLedgerDimension,
|
|
930
|
+
currencyCode,
|
|
931
|
+
-amount,
|
|
932
|
+
exchangeHelper);
|
|
933
|
+
ledgerVoucher.addTrans(credit);
|
|
934
|
+
|
|
935
|
+
ledgerVoucher.end(); // posts the voucher
|
|
936
|
+
|
|
937
|
+
ttscommit;`,
|
|
938
|
+
},
|
|
939
|
+
],
|
|
940
|
+
related: ['transactions', 'financial-dimensions'],
|
|
941
|
+
},
|
|
942
|
+
// ── Multi-company ───────────────────────────────────────────────────────
|
|
943
|
+
{
|
|
944
|
+
id: 'multi-company',
|
|
945
|
+
title: 'Multi-company Queries & changeCompany()',
|
|
946
|
+
keywords: ['multi-company', 'crosscompany', 'changecompany', 'dataareaid', 'legalentity', 'virtualcompany', 'companyinfo', 'systemsequences'],
|
|
947
|
+
summary: 'D365FO supports cross-company data access via changeCompany() and crosscompany select. ' +
|
|
948
|
+
'Every table has DataAreaId — always consider company isolation in queries.',
|
|
949
|
+
rules: [
|
|
950
|
+
'Tables with SaveDataPerCompany=Yes: have DataAreaId field, data is company-specific (default)',
|
|
951
|
+
'Tables with SaveDataPerCompany=No: shared across all companies (e.g. DirPartyTable, RefRecId tables)',
|
|
952
|
+
'changeCompany("DAT") { ... }: switch company context for a code block — closes and re-opens connection',
|
|
953
|
+
'crosscompany select: use when querying data across multiple companies in one query',
|
|
954
|
+
'crosscompany containers: pass specific companies, e.g. while select crosscompany [co1, co2] from myTable',
|
|
955
|
+
'NEVER hardcode DataAreaId — always use curExt() or CompanyInfo::current().DataArea',
|
|
956
|
+
'changeCompany is expensive — avoid inside loops; batch operations cross-company instead',
|
|
957
|
+
'For reporting: use crosscompany select with a list of company IDs from a parameter',
|
|
958
|
+
'Inter-company transactions: use InterCompanyTradingRelationship — do not write cross-company manually',
|
|
959
|
+
],
|
|
960
|
+
examples: [
|
|
961
|
+
{
|
|
962
|
+
label: 'changeCompany block',
|
|
963
|
+
code: `// ✅ Change company for a code block
|
|
964
|
+
CustTable custTable;
|
|
965
|
+
changeCompany("DAT")
|
|
966
|
+
{
|
|
967
|
+
select firstonly custTable
|
|
968
|
+
where custTable.AccountNum == '1001';
|
|
969
|
+
}
|
|
970
|
+
info(custTable.Name); // data from DAT company`,
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
label: 'crosscompany select',
|
|
974
|
+
code: `CustTable custTable;
|
|
975
|
+
container companies = ["DAT", "USMF", "DEMF"];
|
|
976
|
+
|
|
977
|
+
// ✅ Query across multiple companies
|
|
978
|
+
while select crosscompany : companies
|
|
979
|
+
AccountNum, Name, DataAreaId from custTable
|
|
980
|
+
where custTable.CustGroup == 'DOM'
|
|
981
|
+
{
|
|
982
|
+
info(strFmt('%1 | %2 | %3',
|
|
983
|
+
custTable.DataAreaId,
|
|
984
|
+
custTable.AccountNum,
|
|
985
|
+
custTable.Name));
|
|
986
|
+
}`,
|
|
987
|
+
},
|
|
988
|
+
],
|
|
989
|
+
related: ['query-patterns', 'transactions'],
|
|
990
|
+
},
|
|
991
|
+
// ── Print Management ───────────────────────────────────────────────────
|
|
992
|
+
{
|
|
993
|
+
id: 'print-management',
|
|
994
|
+
title: 'Print Management (SrsPrintMgmtController)',
|
|
995
|
+
keywords: ['print management', 'printmgmt', 'srsprintmgmt', 'srsprintmgmtcontroller', 'printmgmtdoctype', 'printmgmtdocumenttype', 'printmgmtsettings', 'original copy'],
|
|
996
|
+
summary: 'Print management in D365FO controls report destinations (screen, printer, email, archive) per document type. ' +
|
|
997
|
+
'Use SrsPrintMgmtController for reports that integrate with the Print management setup form.',
|
|
998
|
+
rules: [
|
|
999
|
+
'Extend SrsPrintMgmtController (not SrsReportRunController) when the report supports Print management',
|
|
1000
|
+
'Register the document type in PrintMgmtDocType enum extension',
|
|
1001
|
+
'Override getDocumentName() and getDocumentTitle() in the controller class',
|
|
1002
|
+
'Override getOriginalPrintMgmtPrintSettingDetail() for the default print settings',
|
|
1003
|
+
'PrintMgmtDocumentType class: register your document type (link to module, table, report)',
|
|
1004
|
+
'To open the Print management setup: go to Accounts receivable → Setup → Print management',
|
|
1005
|
+
'For new document types: also add an entry in PrintMgmtReportFormat (links document type to report design)',
|
|
1006
|
+
'Original vs copy: the base enum is PrintCopyOriginal (Original/Copy), carried on the report contract as parmPrintCopyOriginal() — there is no PrintCopyType enum or parmPrintCopyType()',
|
|
1007
|
+
],
|
|
1008
|
+
related: ['ssrs-reports'],
|
|
1009
|
+
},
|
|
1010
|
+
// ── Unit Testing ─────────────────────────────────────────────────────────
|
|
1011
|
+
{
|
|
1012
|
+
id: 'unit-testing',
|
|
1013
|
+
title: 'X++ Unit Testing (SysTestCase / SysTestSuite)',
|
|
1014
|
+
keywords: ['unit test', 'systestcase', 'systestsuite', 'test', 'assert', 'testmethod', 'mock', 'stub', 'systestcasestub', 'testautomation'],
|
|
1015
|
+
summary: 'X++ unit tests extend SysTestCase. They run in a fresh database transaction that is always rolled back, ' +
|
|
1016
|
+
'ensuring tests are isolated. Run in Visual Studio → Test Explorer or via SysTestSuite.',
|
|
1017
|
+
rules: [
|
|
1018
|
+
'Test class: extends SysTestCase, must be in the same model as the code under test (or a test model)',
|
|
1019
|
+
'Test methods: public void testXxx() — method name MUST start with "test" (case-insensitive)',
|
|
1020
|
+
'Setup/teardown: override setUp() and tearDown() — called before/after EACH test method',
|
|
1021
|
+
'Assertions: assertEquals, assertNotNull, assertNull, assertTrue, assertFalse, fail()',
|
|
1022
|
+
'SysTestSuite: groups multiple SysTestCase classes for batch execution',
|
|
1023
|
+
'Transaction rollback: all DML in a test is rolled back after each test — no cleanup needed for DB state',
|
|
1024
|
+
'For methods that call ttsbegin internally: wrap test in try/catch and expect a clean state',
|
|
1025
|
+
'Mock dependencies: use delegation pattern or extract interfaces — X++ has no built-in mocking framework',
|
|
1026
|
+
'Naming convention: <ClassName>Test (e.g. MyServiceTest) — matches the repo systests and the testing topic; avoid mixing the <ClassName>_Test variant in the same model',
|
|
1027
|
+
'Attributes: [SysTestMethodAttribute] optional — but helps categorize tests',
|
|
1028
|
+
'Run tests: Visual Studio → Test → Run All Tests, or SysTestSuite.run() in a batch job',
|
|
1029
|
+
],
|
|
1030
|
+
examples: [
|
|
1031
|
+
{
|
|
1032
|
+
label: 'Basic SysTestCase',
|
|
1033
|
+
code: `/// <summary>
|
|
1034
|
+
/// Unit tests for MyService.
|
|
1035
|
+
/// </summary>
|
|
1036
|
+
class MyServiceTest extends SysTestCase
|
|
1037
|
+
{
|
|
1038
|
+
MyService service;
|
|
1039
|
+
|
|
1040
|
+
public void setUp()
|
|
1041
|
+
{
|
|
1042
|
+
super();
|
|
1043
|
+
service = new MyService();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
public void testCalculateDiscount_Zero()
|
|
1047
|
+
{
|
|
1048
|
+
// Arrange
|
|
1049
|
+
AmountMST amount = 1000;
|
|
1050
|
+
|
|
1051
|
+
// Act
|
|
1052
|
+
AmountMST discount = service.calculateDiscount(amount, 0);
|
|
1053
|
+
|
|
1054
|
+
// Assert
|
|
1055
|
+
this.assertEquals(0, discount,
|
|
1056
|
+
'Discount should be 0 when rate is 0');
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
public void testCalculateDiscount_TenPercent()
|
|
1060
|
+
{
|
|
1061
|
+
AmountMST discount = service.calculateDiscount(1000, 10);
|
|
1062
|
+
this.assertEquals(100, discount, '10% of 1000 = 100');
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
public void testCalculateDiscount_NegativeAmount()
|
|
1066
|
+
{
|
|
1067
|
+
// Negative amount should throw an exception
|
|
1068
|
+
try
|
|
1069
|
+
{
|
|
1070
|
+
service.calculateDiscount(-100, 10);
|
|
1071
|
+
this.fail('Expected an exception for negative amount');
|
|
1072
|
+
}
|
|
1073
|
+
catch (Exception::Error)
|
|
1074
|
+
{
|
|
1075
|
+
// Expected — test passes
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}`,
|
|
1079
|
+
},
|
|
1080
|
+
],
|
|
1081
|
+
related: ['transactions', 'error-handling', 'testing'],
|
|
1082
|
+
},
|
|
1083
|
+
// ── Telemetry & Logging ─────────────────────────────────────────────────
|
|
1084
|
+
{
|
|
1085
|
+
id: 'telemetry',
|
|
1086
|
+
title: 'Telemetry, Logging & SysInfoLog',
|
|
1087
|
+
keywords: ['telemetry', 'logging', 'sysinfolog', 'infolog', 'info', 'warning', 'error', 'checkfailed', 'eventlog', 'application insights', 'syscustomattribute'],
|
|
1088
|
+
summary: 'D365FO uses SysInfoLog for user-visible messages and Application Insights telemetry for monitoring. ' +
|
|
1089
|
+
'Structure log output carefully — Copilot and users read infolog messages to diagnose issues.',
|
|
1090
|
+
rules: [
|
|
1091
|
+
'info("message"): informational message shown to user in infolog',
|
|
1092
|
+
'warning("message"): amber warning — operation completed but user should be aware',
|
|
1093
|
+
'error("message"): red error — operation failed, return false from validate methods',
|
|
1094
|
+
'checkFailed("message"): same as error() but returns false — use in validateWrite()',
|
|
1095
|
+
'Global::error/warning/info: same as bare functions (Global:: prefix is valid but redundant)',
|
|
1096
|
+
'To capture infolog output programmatically (testing/logging): snapshot infolog.infologData() and walk it with SysInfologEnumerator::newData() — there is no SysInfoLogScope class',
|
|
1097
|
+
'NEVER use print statement — it only shows in job output, not infolog',
|
|
1098
|
+
'For Azure Application Insights telemetry: use Microsoft.ApplicationInsights NuGet — not available in standard X++ without NuGet reference',
|
|
1099
|
+
'Structured telemetry: use SysGlobalTelemetry (logTrace / logEvent / logMetric / logMetricWithCustomProperties) — there is NO SysTelemetry class; for richer App Insights logging use SysApplicationInsightsTelemetryLogger (Monitoring and Telemetry model)',
|
|
1100
|
+
'Batch job logging: use this.BatchHeader.addRuntimeTask() for progress feedback',
|
|
1101
|
+
'Infolog messages in batch: saved to BatchHistory — accessible via Batch jobs > History',
|
|
1102
|
+
'NEVER log sensitive data (passwords, connection strings, PII) — use masked/hashed values',
|
|
1103
|
+
],
|
|
1104
|
+
examples: [
|
|
1105
|
+
{
|
|
1106
|
+
label: 'Capture infolog output programmatically',
|
|
1107
|
+
code: `container beforeData = infolog.infologData();
|
|
1108
|
+
container produced;
|
|
1109
|
+
SysInfologEnumerator enumerator;
|
|
1110
|
+
SysInfologMessageStruct msgStruct;
|
|
1111
|
+
|
|
1112
|
+
myService.doSomething();
|
|
1113
|
+
|
|
1114
|
+
// Everything the service added since the snapshot
|
|
1115
|
+
produced = conDel(infolog.infologData(), 1, conLen(beforeData));
|
|
1116
|
+
enumerator = SysInfologEnumerator::newData(produced);
|
|
1117
|
+
|
|
1118
|
+
while (enumerator.moveNext())
|
|
1119
|
+
{
|
|
1120
|
+
msgStruct = SysInfologMessageStruct::construct(enumerator.currentMessage());
|
|
1121
|
+
|
|
1122
|
+
// currentException() returns the severity as an Exception value
|
|
1123
|
+
// (Exception::Info / Warning / Error), NOT a SysInfologLevel.
|
|
1124
|
+
info(strFmt('[%1] %2', enumerator.currentException(), msgStruct.message()));
|
|
1125
|
+
}`,
|
|
1126
|
+
},
|
|
1127
|
+
],
|
|
1128
|
+
related: ['error-handling', 'sysoperation'],
|
|
1129
|
+
},
|
|
1130
|
+
// ── Global Address Book (GAB) ───────────────────────────────────────────
|
|
1131
|
+
{
|
|
1132
|
+
id: 'global-address-book',
|
|
1133
|
+
title: 'Global Address Book (GAB) — DirPartyTable, DirPartyPostalAddress',
|
|
1134
|
+
keywords: ['gab', 'global address book', 'dirpartytable', 'dirperson', 'dirorganization', 'dirpartypostaladdress', 'logisticspostaladdress', 'dirpartylocation', 'address', 'party', 'contact'],
|
|
1135
|
+
summary: 'D365FO manages all parties (persons, organizations) through DirPartyTable. Every customer, vendor, worker etc. ' +
|
|
1136
|
+
'links to a DirPartyTable record via a Party field (RecId). Do NOT store addresses directly on your custom table — ' +
|
|
1137
|
+
'always use GAB APIs.',
|
|
1138
|
+
rules: [
|
|
1139
|
+
'Every entity with a real-world address (customer, vendor, worker) has a DirPartyTable record via a Party field',
|
|
1140
|
+
'To read postal address: use LogisticsPostalAddress joined through DirPartyLocation',
|
|
1141
|
+
'To read contact info (email, phone): use LogisticsElectronicAddress joined through DirPartyLocation',
|
|
1142
|
+
'DirPartyType enum: Person | Organization | Team — use dirPartyType() method to check',
|
|
1143
|
+
'To create a Party: use DirPartyTable::createNew(DirPartyType::Organization) or DirPersonName for persons',
|
|
1144
|
+
'NEVER insert into DirPartyTable directly — always use the DirPartyTable static helper methods',
|
|
1145
|
+
'To link your custom table to GAB: add a Party field (EDT: DirPartyRecId), set RefTableId, RefRecId',
|
|
1146
|
+
'DirPartyPostalAddressView is a convenient view for reading the primary address',
|
|
1147
|
+
'High-level create/update APIs live on the DirParty class (constructFromPartyRecId, constructFromCommon, createOrUpdatePostalAddress, createOrUpdateContactInfo) and DirPartyTable::createNew — there is NO GlobalAddressBookHelper or DirPartyService class',
|
|
1148
|
+
],
|
|
1149
|
+
examples: [
|
|
1150
|
+
{
|
|
1151
|
+
label: 'Read primary postal address for a party',
|
|
1152
|
+
code: `// Read primary postal address via DirPartyPostalAddressView
|
|
1153
|
+
DirPartyRecId partyRecId = custTable.Party;
|
|
1154
|
+
DirPartyPostalAddressView addrView;
|
|
1155
|
+
|
|
1156
|
+
select firstonly addrView
|
|
1157
|
+
where addrView.Party == partyRecId
|
|
1158
|
+
&& addrView.IsPrimary == NoYes::Yes;
|
|
1159
|
+
|
|
1160
|
+
str street = addrView.Street;
|
|
1161
|
+
str city = addrView.City;
|
|
1162
|
+
str country = addrView.CountryRegionId;`,
|
|
1163
|
+
},
|
|
1164
|
+
{
|
|
1165
|
+
label: 'Read primary email address',
|
|
1166
|
+
code: `// Read primary email using LogisticsElectronicAddress
|
|
1167
|
+
DirPartyRecId partyRecId = vendTable.Party;
|
|
1168
|
+
LogisticsElectronicAddress email;
|
|
1169
|
+
|
|
1170
|
+
select firstonly email
|
|
1171
|
+
join DirPartyLocation
|
|
1172
|
+
where DirPartyLocation.Party == partyRecId
|
|
1173
|
+
&& DirPartyLocation.IsPrimary == NoYes::Yes
|
|
1174
|
+
&& email.Location == DirPartyLocation.Location
|
|
1175
|
+
&& email.Type == LogisticsElectronicAddressMethodType::Email;
|
|
1176
|
+
|
|
1177
|
+
str emailAddr = email.Locator;`,
|
|
1178
|
+
},
|
|
1179
|
+
],
|
|
1180
|
+
related: ['data-entities', 'number-sequences'],
|
|
1181
|
+
},
|
|
1182
|
+
// ── SysExtension Framework ──────────────────────────────────────────────
|
|
1183
|
+
{
|
|
1184
|
+
id: 'sysextension',
|
|
1185
|
+
title: 'SysExtension Framework — plug-in pattern without if/else chains',
|
|
1186
|
+
keywords: ['sysextension', 'sysextensionappclassfactory', 'sysextensioniattribute', 'sysattribute', 'plugin', 'plug-in', 'factory', 'decorator', 'extensible enum', 'sysplugin'],
|
|
1187
|
+
summary: 'SysExtension allows registering and resolving implementations keyed by an extensible enum without modifying ' +
|
|
1188
|
+
'the base code. Replaces if/switch chains. Consists of: a base class or interface, an extensible enum, a ' +
|
|
1189
|
+
'factory ATTRIBUTE class (extends SysAttribute implements SysExtensionIAttribute) that each concrete class ' +
|
|
1190
|
+
'is decorated with, and a lookup via SysExtensionAppClassFactory.',
|
|
1191
|
+
rules: [
|
|
1192
|
+
'Define an interface (or abstract base class) for the strategy: interface IMyStrategy { void execute(); }',
|
|
1193
|
+
'Create an extensible enum (IsExtensible=Yes) with one value per strategy',
|
|
1194
|
+
'Write ONE factory attribute per strategy family: `class MyProcessorAttribute extends SysAttribute implements SysExtensionIAttribute`, taking the enum value in new() and returning a unique parmCacheKey()',
|
|
1195
|
+
'Decorate each concrete class with that attribute: [MyProcessorAttribute(MyProcessorType::Express)]',
|
|
1196
|
+
'Resolve at runtime: SysExtensionAppClassFactory::getClassFromSysAttribute(classStr(MyProcessorBase), new MyProcessorAttribute(_type))',
|
|
1197
|
+
'There is no ExportMetadataAttribute and no SysExtensionAppSuiteDecoratorForward class in D365FO — both are AX2012-era/MEF names that do not resolve',
|
|
1198
|
+
'SysPluginFactory::Instance(namespace, className, metadataCollection) is the .NET-plugin sibling — different mechanism, do not mix the two',
|
|
1199
|
+
'Adding a new strategy = new class + new enum value, ZERO changes to base code',
|
|
1200
|
+
'Use classStr() / enumStr() — never string literals — for refactor-safety',
|
|
1201
|
+
'Works for both class and table contexts; interface must be implemented on the class',
|
|
1202
|
+
'NEVER use this pattern for a single implementation — only when multiple strategies needed',
|
|
1203
|
+
],
|
|
1204
|
+
examples: [
|
|
1205
|
+
{
|
|
1206
|
+
label: 'SysExtension plug-in pattern',
|
|
1207
|
+
code: `// 1. Extensible enum (IsExtensible = Yes in XML)
|
|
1208
|
+
// enum MyProcessorType { Standard, Express, Overnight }
|
|
1209
|
+
|
|
1210
|
+
// 2. Abstract base the factory resolves against
|
|
1211
|
+
public abstract class MyProcessorBase
|
|
1212
|
+
{
|
|
1213
|
+
public abstract void process(MyTable _record);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// 3. Factory attribute — the registration mechanism
|
|
1217
|
+
class MyProcessorAttribute extends SysAttribute implements SysExtensionIAttribute
|
|
1218
|
+
{
|
|
1219
|
+
MyProcessorType processorType;
|
|
1220
|
+
|
|
1221
|
+
public void new(MyProcessorType _processorType)
|
|
1222
|
+
{
|
|
1223
|
+
super();
|
|
1224
|
+
processorType = _processorType;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
public str parmCacheKey()
|
|
1228
|
+
{
|
|
1229
|
+
return strFmt('%1;%2',
|
|
1230
|
+
classStr(MyProcessorAttribute),
|
|
1231
|
+
int2str(enum2int(processorType)));
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
public boolean useSingleton()
|
|
1235
|
+
{
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// 4. Concrete implementation, decorated with the enum value
|
|
1241
|
+
[MyProcessorAttribute(MyProcessorType::Express)]
|
|
1242
|
+
public class MyExpressProcessor extends MyProcessorBase
|
|
1243
|
+
{
|
|
1244
|
+
public void process(MyTable _record)
|
|
1245
|
+
{
|
|
1246
|
+
// Express processing logic
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// 5. Factory resolution — no if/switch needed
|
|
1251
|
+
public static void runProcessor(MyTable _record, MyProcessorType _type)
|
|
1252
|
+
{
|
|
1253
|
+
MyProcessorBase processor = SysExtensionAppClassFactory::getClassFromSysAttribute(
|
|
1254
|
+
classStr(MyProcessorBase),
|
|
1255
|
+
new MyProcessorAttribute(_type)) as MyProcessorBase;
|
|
1256
|
+
|
|
1257
|
+
if (processor)
|
|
1258
|
+
{
|
|
1259
|
+
processor.process(_record);
|
|
1260
|
+
}
|
|
1261
|
+
}`,
|
|
1262
|
+
},
|
|
1263
|
+
],
|
|
1264
|
+
related: ['coc-extensions', 'batch-jobs'],
|
|
1265
|
+
},
|
|
1266
|
+
// ── Currency / Exchange Rates ───────────────────────────────────────────
|
|
1267
|
+
{
|
|
1268
|
+
id: 'currency-exchange-rates',
|
|
1269
|
+
title: 'Currency & Exchange Rates — ExchangeRateHelper, CurrencyExchangeHelper',
|
|
1270
|
+
keywords: ['currency', 'exchange rate', 'exchangeratehelper', 'currencyexchangehelper', 'amount', 'convert', 'amountcur', 'amountmst', 'ledgercurrency', 'transactioncurrency', 'accountingcurrency'],
|
|
1271
|
+
summary: 'D365FO manages currency conversion through ExchangeRateHelper and CurrencyExchangeHelper. ' +
|
|
1272
|
+
'Never calculate exchange rates manually — always use the framework APIs to respect ' +
|
|
1273
|
+
'company exchange rate configuration.',
|
|
1274
|
+
rules: [
|
|
1275
|
+
'Use CurrencyExchangeHelper::newExchangeDate(Ledger::current(), rateDate) as the entry point for every conversion — the factory takes a LEDGER RecId + date, not a currency pair',
|
|
1276
|
+
'Convert with the calculate* methods on that helper: calculateTransactionToAccounting() (AmountCur → AmountMST), calculateAccountingToTransaction(), calculateTransactionToTransaction()',
|
|
1277
|
+
'ExchangeRateHelper is the read-side helper for the rate itself: getExchangeRate1_Static(ledger, currency, date) / getExchangeRate2_Static() — there is no plain getExchangeRate()',
|
|
1278
|
+
'Transaction currency (AmountCur) → Accounting currency (AmountMST): use CurrencyExchangeHelper',
|
|
1279
|
+
'Accounting currency is defined per legal entity: CompanyInfo::find().CurrencyCode',
|
|
1280
|
+
'Exchange rate types: Default, Budget, Cost accounting — always use the type from Ledger setup',
|
|
1281
|
+
'NEVER hard-code exchange rates or calculate manually',
|
|
1282
|
+
'For subledger transactions: hand the CurrencyExchangeHelper to the posting API (newTransactionAmountDefault), not manual arithmetic',
|
|
1283
|
+
'ExchangeRateType table holds the types; ExchangeRate table holds the actual rates',
|
|
1284
|
+
'When inserting subledger lines, let SubledgerJournalizer handle the currency conversion',
|
|
1285
|
+
],
|
|
1286
|
+
examples: [
|
|
1287
|
+
{
|
|
1288
|
+
label: 'Convert transaction currency amount to accounting currency',
|
|
1289
|
+
code: `// Convert an amount from transaction currency to accounting currency
|
|
1290
|
+
CurrencyExchangeHelper exchangeHelper;
|
|
1291
|
+
CurrencyCode fromCurrency = salesLine.CurrencyCode;
|
|
1292
|
+
TransDate rateDate = systemDateGet();
|
|
1293
|
+
ExchRate rate;
|
|
1294
|
+
AmountMST amountMST;
|
|
1295
|
+
|
|
1296
|
+
// The helper is bound to a ledger + a rate date, then reused for every amount
|
|
1297
|
+
exchangeHelper = CurrencyExchangeHelper::newExchangeDate(Ledger::current(), rateDate);
|
|
1298
|
+
|
|
1299
|
+
amountMST = exchangeHelper.calculateTransactionToAccounting(
|
|
1300
|
+
fromCurrency,
|
|
1301
|
+
salesLine.LineAmount,
|
|
1302
|
+
true); // _roundResult
|
|
1303
|
+
|
|
1304
|
+
// Read-side: the rate itself (e.g. to show it on a form)
|
|
1305
|
+
rate = ExchangeRateHelper::getExchangeRate1_Static(Ledger::current(), fromCurrency, rateDate);`,
|
|
1306
|
+
},
|
|
1307
|
+
],
|
|
1308
|
+
related: ['posting-engine', 'financial-dimensions'],
|
|
1309
|
+
},
|
|
1310
|
+
// ── Alerts / Business Events ────────────────────────────────────────────
|
|
1311
|
+
{
|
|
1312
|
+
id: 'alerts-business-events',
|
|
1313
|
+
title: 'Alerts & Business Events — BusinessEventsContract, EventRule',
|
|
1314
|
+
keywords: ['alert', 'business event', 'businesseventscontract', 'businesseventscatalog', 'eventrule', 'eventinbox', 'eventjobcud', 'notification', 'businessevent', 'businesseventsbase'],
|
|
1315
|
+
summary: 'D365FO supports two notification mechanisms: (1) Classic Alerts (user-defined rules on table changes) ' +
|
|
1316
|
+
'and (2) Business Events (developer-defined, publishable to Azure Service Bus / Logic Apps / Power Automate). ' +
|
|
1317
|
+
'Use Business Events for integration scenarios, Alerts for user-defined notifications.',
|
|
1318
|
+
rules: [
|
|
1319
|
+
'Business Events: create a class extending BusinessEventsBase with the [BusinessEvents(classStr(<Contract>), name, description, ModuleAxapta::…)] attribute — registration in the catalog is automatic from that attribute, do NOT hand-edit BusinessEventsCatalog',
|
|
1320
|
+
'BusinessEventsContract: data contract class with [DataContract] + [DataMember(...), BusinessEventsDataMember(...)] parm methods for payload',
|
|
1321
|
+
'Constructor is private new(); expose a static newFrom<Buffer>() factory and override [Wrappable(false), Replaceable(false)] buildContract()',
|
|
1322
|
+
'Trigger the event: MyBusinessEvent::newFrom<Buffer>(buffer).send() — never call the private new() directly',
|
|
1323
|
+
'Gating: BusinessEventsConfigurationReader::isBusinessEventEnabled controls whether an event is active for a legal entity',
|
|
1324
|
+
'Classic Alerts: driven by EventRule / EventRuleData / EventInbox tables, processed by the EventJobCUD batch class — users configure rules in UI, no code needed',
|
|
1325
|
+
'Business Events are visible in System administration > Business events catalog',
|
|
1326
|
+
'Enable/disable per legal entity in the catalog; endpoint configured there (Service Bus, etc.)',
|
|
1327
|
+
'For unit testing there is no BusinessEventsTestHelper — model against BusinessEventsTestEndpointContract and the SysTest framework',
|
|
1328
|
+
'NEVER use direct REST calls for integration — always prefer Business Events for D365FO outbound',
|
|
1329
|
+
],
|
|
1330
|
+
examples: [
|
|
1331
|
+
{
|
|
1332
|
+
label: 'Define and send a Business Event',
|
|
1333
|
+
code: `// 1. Contract class
|
|
1334
|
+
[DataContractAttribute]
|
|
1335
|
+
public final class MyBusinessEventContract extends BusinessEventsContract
|
|
1336
|
+
{
|
|
1337
|
+
private SalesId salesId;
|
|
1338
|
+
private AmountMST totalAmount;
|
|
1339
|
+
|
|
1340
|
+
public static MyBusinessEventContract newFromSalesTable(SalesTable _salesTable)
|
|
1341
|
+
{
|
|
1342
|
+
MyBusinessEventContract contract = new MyBusinessEventContract();
|
|
1343
|
+
contract.initialize(_salesTable);
|
|
1344
|
+
return contract;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
private void initialize(SalesTable _salesTable)
|
|
1348
|
+
{
|
|
1349
|
+
salesId = _salesTable.SalesId;
|
|
1350
|
+
totalAmount = _salesTable.SalesBalance;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
[DataMemberAttribute('SalesId'),
|
|
1354
|
+
BusinessEventsDataMember('@SYS22843')]
|
|
1355
|
+
public SalesId parmSalesId(SalesId _salesId = salesId)
|
|
1356
|
+
{
|
|
1357
|
+
salesId = _salesId;
|
|
1358
|
+
return salesId;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// 2. Business event class
|
|
1363
|
+
[BusinessEvents(classStr(MyBusinessEventContract),
|
|
1364
|
+
'My Sales Confirmed Event',
|
|
1365
|
+
'Raised when a sales order is confirmed',
|
|
1366
|
+
ModuleAxapta::SalesOrder)]
|
|
1367
|
+
public final class MySalesConfirmedBusinessEvent extends BusinessEventsBase
|
|
1368
|
+
{
|
|
1369
|
+
private MyBusinessEventContract contract;
|
|
1370
|
+
|
|
1371
|
+
public static MySalesConfirmedBusinessEvent newFromContract(
|
|
1372
|
+
MyBusinessEventContract _contract)
|
|
1373
|
+
{
|
|
1374
|
+
MySalesConfirmedBusinessEvent event = new MySalesConfirmedBusinessEvent();
|
|
1375
|
+
event.contract = _contract;
|
|
1376
|
+
return event;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
[Wrappable(false), Replaceable(false)]
|
|
1380
|
+
public BusinessEventsContract buildContract()
|
|
1381
|
+
{
|
|
1382
|
+
return contract;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// 3. Send the event (e.g. in postConfirm())
|
|
1387
|
+
MyBusinessEventContract contract = MyBusinessEventContract::newFromSalesTable(salesTable);
|
|
1388
|
+
MySalesConfirmedBusinessEvent::newFromContract(contract).send();`,
|
|
1389
|
+
},
|
|
1390
|
+
],
|
|
1391
|
+
related: ['coc-extensions', 'batch-jobs'],
|
|
1392
|
+
},
|
|
1393
|
+
// ── Electronic Reporting (ER) ───────────────────────────────────────────
|
|
1394
|
+
{
|
|
1395
|
+
id: 'electronic-reporting',
|
|
1396
|
+
title: 'Electronic Reporting (ER) — ERModelMapping, ERFormatMapping, X++ integration',
|
|
1397
|
+
keywords: ['er', 'electronic reporting', 'ermodelmapping', 'erformat', 'erformatmapping', 'erformatmappingrun', 'erconfiguration', 'er format', 'er model', 'data model', 'format mapping'],
|
|
1398
|
+
summary: 'Electronic Reporting (ER) is the D365FO framework for configurable business document generation ' +
|
|
1399
|
+
'(invoices, SEPA, VAT files). From X++ you can: (1) run an ER format programmatically, ' +
|
|
1400
|
+
'(2) extend an ER model mapping via CoC, (3) pass data from X++ to ER via a custom ER data source.',
|
|
1401
|
+
rules: [
|
|
1402
|
+
'Run ER format from X++: use ERObjectsFactory to get an ERIFormatMappingRun (note the ERI… prefix — there is no IERFormatMappingRun), then call run()',
|
|
1403
|
+
'Pass parameters to ER: build an ERModelDefinitionInputParametersAction (addParameter/applyTo) and hand it to withParameter()',
|
|
1404
|
+
'NEVER modify ER configurations in code — use ER designer in D365FO UI or import from LCS',
|
|
1405
|
+
'ER configurations are stored in ERSolutionTable / ERVendorTable — do NOT touch DB directly',
|
|
1406
|
+
'To extend ER model mapping: implement IERModelMappingExtension on your class (CoC not possible for ER)',
|
|
1407
|
+
'For custom data sources: create a class implementing ERIDataSourceProvider and register it',
|
|
1408
|
+
'ER format file path: System administration > Electronic reporting > Reporting configurations',
|
|
1409
|
+
'For testing: use ERObjectsFactory::createFormatMappingRunByFormatMappingId()',
|
|
1410
|
+
'Country-specific ER formats loaded via localization features — check ERSolutionRepositoryTable',
|
|
1411
|
+
],
|
|
1412
|
+
examples: [
|
|
1413
|
+
{
|
|
1414
|
+
label: 'Run an ER format from X++ code',
|
|
1415
|
+
code: `// Run an ER format programmatically and return the output as a file
|
|
1416
|
+
using Microsoft.Dynamics365.LocalizationFramework;
|
|
1417
|
+
|
|
1418
|
+
public static void runErFormat(ERFormatMappingId _formatMappingId, FilePath _outputPath)
|
|
1419
|
+
{
|
|
1420
|
+
ERIFormatMappingRun formatRun = ERObjectsFactory::createFormatMappingRunByFormatMappingId(
|
|
1421
|
+
_formatMappingId,
|
|
1422
|
+
_outputPath, // _fileName
|
|
1423
|
+
false, // _showPromptDialog — false = unattended
|
|
1424
|
+
false, // _showInfologMessage
|
|
1425
|
+
false); // _forceRunDraft
|
|
1426
|
+
|
|
1427
|
+
// Optionally push user-input parameter values into the model definition
|
|
1428
|
+
ERModelDefinitionInputParametersAction paramsAction = new ERModelDefinitionInputParametersAction();
|
|
1429
|
+
formatRun.withParameter(paramsAction);
|
|
1430
|
+
|
|
1431
|
+
// Run and get output
|
|
1432
|
+
formatRun.run();
|
|
1433
|
+
}`,
|
|
1434
|
+
},
|
|
1435
|
+
],
|
|
1436
|
+
related: ['ssrs-reports', 'print-management'],
|
|
1437
|
+
},
|
|
1438
|
+
// ── Security: Privileges / Duties granularity ───────────────────────────
|
|
1439
|
+
{
|
|
1440
|
+
id: 'security-privileges-duties',
|
|
1441
|
+
title: 'Security: Privileges, Duties, Roles — granular security chain',
|
|
1442
|
+
keywords: ['security', 'privilege', 'duty', 'role', 'securityprivilege', 'securityduty', 'securityrole', 'entrypoint', 'permission', 'access level', 'securyobject', 'hasappliedmenuitem', 'menuitem'],
|
|
1443
|
+
summary: 'D365FO security follows a 3-tier hierarchy: Role → Duty → Privilege → Entry Point (menu item/service/form). ' +
|
|
1444
|
+
'Always create BOTH View (read-only) and Maintain (full-access) privilege variants. ' +
|
|
1445
|
+
'Duties group related privileges by business function. Roles group duties by job function.',
|
|
1446
|
+
rules: [
|
|
1447
|
+
'Hierarchy: Role (job function) → Duty (business function) → Privilege (single operation) → Entry Point',
|
|
1448
|
+
'Always create two privilege variants: ViewMyObject (Read) and MaintainMyObject (Update+Create+Delete)',
|
|
1449
|
+
'Entry point on privilege = menu item name; access level: Read | Create | Update | Delete | Correct | View | NoAccess',
|
|
1450
|
+
'Duty: groups related privileges for a business task (e.g. "Maintain customer invoices")',
|
|
1451
|
+
'Role: assigned to user; groups duties for a complete job function (e.g. "Accounts receivable clerk")',
|
|
1452
|
+
'Use generate_object(mode="pattern", pattern="security-privilege") to generate both View and Maintain XML pairs',
|
|
1453
|
+
'Privilege XML: AxSecurityPrivilege folder; Duty XML: AxSecurityDuty; Role XML: AxSecurityRole',
|
|
1454
|
+
'NEVER use objectType="security-privilege" for duties — each maps to a different AOT folder',
|
|
1455
|
+
'To check user access in code: SecurityRights::hasMenuItemAccess(menuItemStr(X), MenuItemType::Display)',
|
|
1456
|
+
'For table-level security: use XDS (Extensible Data Security) policies — AxSecurityPolicy XML',
|
|
1457
|
+
'Table permissions on privilege define column-level access; use Field Permissions for column masking',
|
|
1458
|
+
],
|
|
1459
|
+
examples: [
|
|
1460
|
+
{
|
|
1461
|
+
label: 'Check security access in X++',
|
|
1462
|
+
code: `// Check if current user has access to a menu item
|
|
1463
|
+
if (SecurityRights::hasMenuItemAccess(
|
|
1464
|
+
menuItemStr(MyCustomForm),
|
|
1465
|
+
MenuItemType::Display))
|
|
1466
|
+
{
|
|
1467
|
+
// User has access
|
|
1468
|
+
element.design().visible(true);
|
|
1469
|
+
}
|
|
1470
|
+
else
|
|
1471
|
+
{
|
|
1472
|
+
element.design().visible(false);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// Check table-level access (read permission)
|
|
1476
|
+
if (SecurityRights::hasTableAccess(tableNum(MyCustomTable), AccessType::Read))
|
|
1477
|
+
{
|
|
1478
|
+
// Has at least read access to MyCustomTable
|
|
1479
|
+
}`,
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
label: 'Privilege XML structure (View variant)',
|
|
1483
|
+
code: `<?xml version="1.0" encoding="utf-8"?>
|
|
1484
|
+
<AxSecurityPrivilege xmlns:i="...">
|
|
1485
|
+
<Name>ViewMyCustomTable</Name>
|
|
1486
|
+
<Label>@MyModel:ViewMyCustomTable</Label>
|
|
1487
|
+
<EntryPoints>
|
|
1488
|
+
<AxSecurityEntryPointReference>
|
|
1489
|
+
<EntryPointName>MyCustomFormMenuItem</EntryPointName>
|
|
1490
|
+
<EntryPointType>MenuItemDisplay</EntryPointType>
|
|
1491
|
+
<PermissionGroup>Read</PermissionGroup>
|
|
1492
|
+
</AxSecurityEntryPointReference>
|
|
1493
|
+
</EntryPoints>
|
|
1494
|
+
</AxSecurityPrivilege>`,
|
|
1495
|
+
},
|
|
1496
|
+
],
|
|
1497
|
+
related: ['coc-extensions', 'data-entities', 'security'],
|
|
1498
|
+
},
|
|
1499
|
+
// ── SSRS Reports ────────────────────────────────────────────────────────
|
|
1500
|
+
{
|
|
1501
|
+
id: 'ssrs-reports',
|
|
1502
|
+
title: 'SSRS Reports (DP → TmpTable → RDL)',
|
|
1503
|
+
keywords: ['ssrs', 'report', 'rdl', 'dp class', 'data provider', 'srsreportdataproviderbase', 'contract', 'controller', 'design'],
|
|
1504
|
+
summary: 'D365FO SSRS reports use: TmpTable (TempDB) → DataContract → DP class → Controller → AxReport with RDL design.',
|
|
1505
|
+
rules: [
|
|
1506
|
+
'5 objects: TmpTable (TempDB), Contract (DataContractAttribute), DP (extends SrsReportDataProviderBase), Controller (extends SrsReportRunController), AxReport XML',
|
|
1507
|
+
'TmpTable: MUST be TableType=TempDB (NOT InMemory) — required for SSRS data connection',
|
|
1508
|
+
'DP class: [SrsReportParameterAttribute(classStr(MyContract))], processReport() fills TmpTable',
|
|
1509
|
+
'DP getter: [SRSReportDataSetAttribute(tableStr(MyTmp))] public MyTmp getMyTmp()',
|
|
1510
|
+
'Controller: sets report name via ssrsReportStr(), opens dialog, runs report',
|
|
1511
|
+
'AxReport XML: DataSet with DataSourceType=ReportDataProvider, Query=SELECT * FROM DPClass.TmpTable',
|
|
1512
|
+
'Use generate MCP tool to generate all 5 objects at once',
|
|
1513
|
+
'For existing reports, use get_object_info(objectType="report", name=...) — NEVER read report XML with PowerShell',
|
|
1514
|
+
],
|
|
1515
|
+
related: ['temp-tables', 'sysoperation'],
|
|
1516
|
+
},
|
|
1517
|
+
// ── Inventory Management ────────────────────────────────────────────────
|
|
1518
|
+
{
|
|
1519
|
+
id: 'inventory-management',
|
|
1520
|
+
title: 'Inventory Management (InventTrans, InventDim, On-hand)',
|
|
1521
|
+
keywords: ['inventory', 'inventtrans', 'inventdim', 'inventsum', 'inventonhand', 'reservation',
|
|
1522
|
+
'inventtransorigin', 'inventmov', 'inventupdate', 'on-hand', 'stock'],
|
|
1523
|
+
summary: 'D365FO inventory uses InventTrans (transactions), InventDim (dimension combinations), and InventSum ' +
|
|
1524
|
+
'(aggregated on-hand). The InventMovement class hierarchy handles business logic for creating/updating ' +
|
|
1525
|
+
'inventory transactions. Reservations flow through InventUpd_Reservation.',
|
|
1526
|
+
rules: [
|
|
1527
|
+
'InventTrans: one record per inventory lot/transaction; linked via InventTransOrigin to source docs',
|
|
1528
|
+
'InventDim: stores inventory dimensions (Site, Warehouse, Location, Batch, Serial, etc.) — NEVER create duplicates, use InventDim::findOrCreate()',
|
|
1529
|
+
'InventSum: aggregated on-hand per ItemId + InventDimId — do NOT update directly, it is maintained by the system',
|
|
1530
|
+
'InventOnHand: use InventOnHand class (not direct InventSum queries) for accurate on-hand calculations',
|
|
1531
|
+
'InventMovement: abstract class hierarchy for business rules on inventory transactions — each source doc type has its own subclass',
|
|
1532
|
+
'InventUpdate: updates InventTrans status (e.g. InventUpd_Physical for packing slip, InventUpd_Financial for invoice)',
|
|
1533
|
+
'Reservation: InventUpd_Reservation handles soft/hard reservation; respects reservation hierarchy (Site > Warehouse > Location > Batch > Serial)',
|
|
1534
|
+
'Dimensions: configuration keys control which dimensions are active — check InventDimSetup',
|
|
1535
|
+
'Use InventDimCtrl_Frm* classes to control dimension field visibility on forms',
|
|
1536
|
+
'For custom inventory dimensions: follow the extension pattern in Microsoft docs — add via model extension, NOT overlayering',
|
|
1537
|
+
],
|
|
1538
|
+
examples: [
|
|
1539
|
+
{
|
|
1540
|
+
label: 'Query on-hand',
|
|
1541
|
+
code: `// Query available on-hand for an item
|
|
1542
|
+
InventDim inventDim;
|
|
1543
|
+
InventOnHand inventOnHand;
|
|
1544
|
+
|
|
1545
|
+
inventDim.InventSiteId = 'Site1';
|
|
1546
|
+
inventDim.InventLocationId = 'WH1';
|
|
1547
|
+
inventDim = InventDim::findOrCreate(inventDim);
|
|
1548
|
+
|
|
1549
|
+
inventOnHand = InventOnHand::newItemDim(
|
|
1550
|
+
InventTable::find('ItemId'),
|
|
1551
|
+
inventDim,
|
|
1552
|
+
InventDimParm::activeDimFlag(inventDim));
|
|
1553
|
+
|
|
1554
|
+
Qty availPhysical = inventOnHand.availPhysical();`,
|
|
1555
|
+
},
|
|
1556
|
+
{
|
|
1557
|
+
label: 'Find or create InventDim',
|
|
1558
|
+
code: `// ALWAYS use findOrCreate — never insert raw InventDim records
|
|
1559
|
+
InventDim dim;
|
|
1560
|
+
dim.InventSiteId = 'Site1';
|
|
1561
|
+
dim.InventLocationId = 'WH-MAIN';
|
|
1562
|
+
dim = InventDim::findOrCreate(dim);
|
|
1563
|
+
// dim.inventDimId is now set`,
|
|
1564
|
+
},
|
|
1565
|
+
],
|
|
1566
|
+
related: ['query-patterns', 'set-based'],
|
|
1567
|
+
},
|
|
1568
|
+
// ── Feature Management ──────────────────────────────────────────────────
|
|
1569
|
+
{
|
|
1570
|
+
id: 'feature-management',
|
|
1571
|
+
title: 'Feature Management & Feature Flighting',
|
|
1572
|
+
keywords: ['feature management', 'feature class', 'feature flighting', 'featurestateprovider',
|
|
1573
|
+
'isfeatureenabled', 'feature toggle', 'feature attribute', 'ifeaturemetadata'],
|
|
1574
|
+
summary: 'D365FO Feature Management allows enabling/disabling features at runtime without redeployment. ' +
|
|
1575
|
+
'ISV/custom features register by implementing IFeatureMetadata and exporting the class via ' +
|
|
1576
|
+
'[ExportAttribute(...)]; they then appear in the Feature Management workspace. ' +
|
|
1577
|
+
'Code checks feature state via FeatureStateProvider::isFeatureEnabled(MyFeature::instance()).',
|
|
1578
|
+
rules: [
|
|
1579
|
+
'Register a custom feature: `[ExportAttribute(identifierStr(Microsoft.Dynamics.ApplicationPlatform.FeatureExposure.IFeatureMetadata))] public final class MyFeature implements IFeatureMetadata` — there is NO FeatureClassAttribute',
|
|
1580
|
+
'The feature class is a singleton: private new(), private static void TypeNew() assigning `instance`, and a public static instance() returning it',
|
|
1581
|
+
'IFeatureMetadata members are INSTANCE methods, all marked [Hookable(false)]: label(), summary(), module(), isEnabledByDefault(), canDisable(), learnMoreUrl(). The description shown in the workspace is summary() — there is no description()',
|
|
1582
|
+
'Check at runtime: FeatureStateProvider::isFeatureEnabled(MyFeature::instance()) — it takes the feature INSTANCE (IFeature), not classStr()',
|
|
1583
|
+
'Convention: wrap that call in a static isEnabled() on the feature class so callers never touch FeatureStateProvider directly',
|
|
1584
|
+
'NEVER call isFeatureEnabled() inside a tight loop — cache the result in a local variable',
|
|
1585
|
+
'Feature states: Enabled, Disabled, EnabledByDefault (user can still disable when canDisable() returns true)',
|
|
1586
|
+
'Always provide a meaningful summary() — it shows in the workspace and helps admins decide',
|
|
1587
|
+
'Use Feature Management for gradual rollout — don\'t use configuration keys for new features (CK are compile-time)',
|
|
1588
|
+
],
|
|
1589
|
+
examples: [
|
|
1590
|
+
{
|
|
1591
|
+
label: 'Feature class definition',
|
|
1592
|
+
code: `using System.ComponentModel.Composition;
|
|
1593
|
+
using Microsoft.Dynamics.ApplicationPlatform.FeatureExposure;
|
|
1594
|
+
|
|
1595
|
+
/// <summary>
|
|
1596
|
+
/// My custom feature that enables enhanced validation.
|
|
1597
|
+
/// </summary>
|
|
1598
|
+
[ExportAttribute(identifierStr(Microsoft.Dynamics.ApplicationPlatform.FeatureExposure.IFeatureMetadata))]
|
|
1599
|
+
public final class MyEnhancedValidationFeature implements IFeatureMetadata
|
|
1600
|
+
{
|
|
1601
|
+
private static MyEnhancedValidationFeature instance;
|
|
1602
|
+
|
|
1603
|
+
private void new()
|
|
1604
|
+
{
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
private static void TypeNew()
|
|
1608
|
+
{
|
|
1609
|
+
instance = new MyEnhancedValidationFeature();
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
[Hookable(false)]
|
|
1613
|
+
public static MyEnhancedValidationFeature instance()
|
|
1614
|
+
{
|
|
1615
|
+
return MyEnhancedValidationFeature::instance;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
[Hookable(false)]
|
|
1619
|
+
public FeatureLabelId label()
|
|
1620
|
+
{
|
|
1621
|
+
return literalStr("@MyModel:EnhancedValidationLabel");
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
[Hookable(false)]
|
|
1625
|
+
public FeatureLabelId summary()
|
|
1626
|
+
{
|
|
1627
|
+
return literalStr("@MyModel:EnhancedValidationDesc");
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
[Hookable(false)]
|
|
1631
|
+
public int module()
|
|
1632
|
+
{
|
|
1633
|
+
return FeatureModuleV0::SystemAdministration;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
[Hookable(false)]
|
|
1637
|
+
public boolean isEnabledByDefault()
|
|
1638
|
+
{
|
|
1639
|
+
return false;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
[Hookable(false)]
|
|
1643
|
+
public boolean canDisable()
|
|
1644
|
+
{
|
|
1645
|
+
return true;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// Convention: callers use this, never FeatureStateProvider directly.
|
|
1649
|
+
internal static boolean isEnabled()
|
|
1650
|
+
{
|
|
1651
|
+
return FeatureStateProvider::isFeatureEnabled(MyEnhancedValidationFeature::instance());
|
|
1652
|
+
}
|
|
1653
|
+
}`,
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
label: 'Runtime feature check',
|
|
1657
|
+
code: `// Branch logic based on feature state
|
|
1658
|
+
if (MyEnhancedValidationFeature::isEnabled()) // wraps FeatureStateProvider::isFeatureEnabled(instance())
|
|
1659
|
+
{
|
|
1660
|
+
// New enhanced validation path
|
|
1661
|
+
this.validateEnhanced();
|
|
1662
|
+
}
|
|
1663
|
+
else
|
|
1664
|
+
{
|
|
1665
|
+
// Legacy validation path
|
|
1666
|
+
this.validateLegacy();
|
|
1667
|
+
}`,
|
|
1668
|
+
},
|
|
1669
|
+
],
|
|
1670
|
+
related: ['sysextension', 'testing'],
|
|
1671
|
+
},
|
|
1672
|
+
// ── Dual-write ──────────────────────────────────────────────────────────
|
|
1673
|
+
{
|
|
1674
|
+
id: 'dual-write',
|
|
1675
|
+
title: 'Dual-write Integration (Dataverse ↔ F&O)',
|
|
1676
|
+
keywords: ['dual-write', 'dual write', 'dataverse', 'cds', 'common data service', 'virtual entity',
|
|
1677
|
+
'integration', 'synchronization', 'dualwriteentity'],
|
|
1678
|
+
summary: 'Dual-write provides near-real-time bidirectional synchronization between D365FO and Dataverse (Power Platform). ' +
|
|
1679
|
+
'It uses table maps (column mappings) and can be extended with custom logic via plug-ins on both sides. ' +
|
|
1680
|
+
'Virtual entities expose F&O data in Dataverse without data duplication.',
|
|
1681
|
+
rules: [
|
|
1682
|
+
'Dual-write operates on data entities — ensure entities are OData-enabled and public',
|
|
1683
|
+
'Table maps define column-level mappings between F&O entity fields and Dataverse table columns',
|
|
1684
|
+
'Initial sync: always run from the side with the most complete data set',
|
|
1685
|
+
'Error handling: dual-write has a retry mechanism — failed records go to an error queue',
|
|
1686
|
+
'Live sync: changes in one system propagate to the other in near-real-time (~seconds)',
|
|
1687
|
+
'Virtual entities: NO data copy — F&O data accessed via OData at runtime in Dataverse; read-only by default',
|
|
1688
|
+
'For custom entities: add DataManagementEnabled=Yes, IsPublic=Yes, PublicEntityName/CollectionName',
|
|
1689
|
+
'NEVER put complex business logic in dual-write transform — keep transforms simple (field mapping, default value)',
|
|
1690
|
+
'For custom pre/post processing: use business events + Power Automate instead of dual-write plug-ins',
|
|
1691
|
+
'Handle company (DataAreaId) filtering carefully — dual-write respects legal entity context',
|
|
1692
|
+
'Performance: avoid dual-write on high-volume transaction tables — use async integration (business events + Service Bus) instead',
|
|
1693
|
+
],
|
|
1694
|
+
related: ['data-entities', 'alerts-business-events'],
|
|
1695
|
+
},
|
|
1696
|
+
// ── Data Management Framework ───────────────────────────────────────────
|
|
1697
|
+
{
|
|
1698
|
+
id: 'data-management-framework',
|
|
1699
|
+
title: 'Data Management Framework (DMF / DIXF)',
|
|
1700
|
+
keywords: ['dmf', 'dixf', 'data import', 'data export', 'staging', 'data entity', 'data management',
|
|
1701
|
+
'composite entity', 'recurring integration', 'data package', 'data project'],
|
|
1702
|
+
summary: 'DMF (Data Import/Export Framework, formerly DIXF) is the standard mechanism for bulk data import/export in D365FO. ' +
|
|
1703
|
+
'It uses data entities with optional staging tables for transformation, validation, and error handling. ' +
|
|
1704
|
+
'Supports: file-based import, recurring integrations (queue-based), data packages, and composite entities.',
|
|
1705
|
+
rules: [
|
|
1706
|
+
'Data entities MUST have DataManagementEnabled=Yes to appear in Data Management workspace',
|
|
1707
|
+
'Staging table: auto-generated or custom — holds imported records before target table insertion',
|
|
1708
|
+
'Entity categories: Parameter, Reference, Master, Document, Transaction — controls import order in data packages',
|
|
1709
|
+
'Composite entities: group header + line entities for hierarchical import (e.g. Sales order with lines)',
|
|
1710
|
+
'Recurring integrations: REST API endpoint for automated queue-based import/export with external systems',
|
|
1711
|
+
'ALWAYS refresh entity list after deploying new/modified entities: Data Management > Framework Parameters > Refresh entity list',
|
|
1712
|
+
'Configuration keys: if entity/table/field config key is disabled, those elements are excluded from DMF',
|
|
1713
|
+
'validateWrite() and insert/update chain is called per-record during import — keep these performant',
|
|
1714
|
+
'For high-volume: use set-based processing via entity.insertEntityDataSource() where possible',
|
|
1715
|
+
'Error handling: staging records get DMFTransferStatus (NotStarted, Completed, Error) — use error log for troubleshooting',
|
|
1716
|
+
'Data packages: ZIP files containing multiple entity CSVs — used for ALM and environment configuration migration',
|
|
1717
|
+
],
|
|
1718
|
+
examples: [
|
|
1719
|
+
{
|
|
1720
|
+
label: 'Entity with staging table (key XML properties)',
|
|
1721
|
+
code: `<!-- AxDataEntityView key properties for DMF -->
|
|
1722
|
+
<IsPublic>Yes</IsPublic>
|
|
1723
|
+
<PublicEntityName>MyCustomer</PublicEntityName>
|
|
1724
|
+
<PublicCollectionName>MyCustomers</PublicCollectionName>
|
|
1725
|
+
<DataManagementEnabled>Yes</DataManagementEnabled>
|
|
1726
|
+
<DataManagementStagingTable>MyCustomerStaging</DataManagementStagingTable>
|
|
1727
|
+
<EntityCategory>Master</EntityCategory>
|
|
1728
|
+
<PrimaryKey>EntityKey</PrimaryKey>`,
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
label: 'Recurring integration API call',
|
|
1732
|
+
code: `// External system pushes data via REST API:
|
|
1733
|
+
// POST https://{env}.operations.dynamics.com/api/connector/enqueue/{DataProject}
|
|
1734
|
+
// Content-Type: application/json
|
|
1735
|
+
// Body: { "MessageId": "...", "Company": "USMF" }
|
|
1736
|
+
// + attach file as multipart form data
|
|
1737
|
+
//
|
|
1738
|
+
// External system pulls exported data via:
|
|
1739
|
+
// GET https://{env}.operations.dynamics.com/api/connector/dequeue/{DataProject}`,
|
|
1740
|
+
},
|
|
1741
|
+
],
|
|
1742
|
+
related: ['data-entities', 'set-based'],
|
|
1743
|
+
},
|
|
1744
|
+
// ── Warehouse Management ────────────────────────────────────────────────
|
|
1745
|
+
{
|
|
1746
|
+
id: 'warehouse-management',
|
|
1747
|
+
title: 'Warehouse Management (WHS / WMS)',
|
|
1748
|
+
keywords: ['warehouse', 'whs', 'wms', 'wave', 'work', 'location directive', 'whswork',
|
|
1749
|
+
'whsworktable', 'whsworkline', 'whswavetemplate', 'pick', 'put', 'replenishment'],
|
|
1750
|
+
summary: 'D365FO Warehouse Management (WHS) manages advanced warehouse operations: wave processing, ' +
|
|
1751
|
+
'work creation, pick/put execution, location directives, and mobile device flows. ' +
|
|
1752
|
+
'WHSWorkTable/WHSWorkLine are core tables. Extensions use CoC on wave/work processor classes.',
|
|
1753
|
+
rules: [
|
|
1754
|
+
'WHSWorkTable: header of warehouse work (pick, put, count, replenishment) — one per work order',
|
|
1755
|
+
'WHSWorkLine: detail lines (specific pick/put actions with from/to locations)',
|
|
1756
|
+
'Wave processing: WHSWaveTemplate defines steps (wave template) — allocate, create work, etc.',
|
|
1757
|
+
'Location directives: WHSLocDirTable rules determine where to pick from and put to',
|
|
1758
|
+
'Work templates: define the work action sequence (Pick → Put, Count, etc.)',
|
|
1759
|
+
'Mobile device: WHSMobileAppFlow — flows are customizable via extensions',
|
|
1760
|
+
'For custom wave steps: extend WHSWaveStepBase and register in wave template config',
|
|
1761
|
+
'NEVER directly update WHSWorkTable.WorkStatus — use the WHSWorkExecute class hierarchy',
|
|
1762
|
+
'Use WHSLocationProfile for zone/location type configuration',
|
|
1763
|
+
'Performance: wave processing is batch-capable — always use batch for large volumes',
|
|
1764
|
+
'For extensions: use CoC on WHSPostEngine* classes for custom post-processing logic',
|
|
1765
|
+
],
|
|
1766
|
+
related: ['inventory-management', 'sysoperation'],
|
|
1767
|
+
},
|
|
1768
|
+
// ── Trade Agreements ────────────────────────────────────────────────────
|
|
1769
|
+
{
|
|
1770
|
+
id: 'trade-agreements',
|
|
1771
|
+
title: 'Trade Agreements & Pricing (PriceDisc)',
|
|
1772
|
+
keywords: ['trade agreement', 'pricing', 'pricedisc', 'price', 'discount', 'sales price',
|
|
1773
|
+
'purchase price', 'line discount', 'multiline discount', 'total discount',
|
|
1774
|
+
'pricediscadmtrans', 'priceDiscTable'],
|
|
1775
|
+
summary: 'D365FO trade agreements define prices and discounts for sales/purchase. ' +
|
|
1776
|
+
'The PriceDisc class evaluates active agreements based on date, quantity, unit, dimensions, and customer/vendor hierarchy. ' +
|
|
1777
|
+
'Agreements are stored in PriceDiscAdmTrans (journal lines) and activated via posting to PriceDiscTable.',
|
|
1778
|
+
rules: [
|
|
1779
|
+
'Trade agreement types: Sales price, Purchase price, Line discount, Multiline discount, Total discount',
|
|
1780
|
+
'PriceDisc.findPrice() / findDisc(): core methods for price/discount evaluation — use these, NOT direct table queries',
|
|
1781
|
+
'Agreement evaluation order: specific (customer+item) → group (cust group+item) → all (all+item) → all+all',
|
|
1782
|
+
'Date effectivity: agreements have FromDate/ToDate — always pass the correct transaction date',
|
|
1783
|
+
'Quantity breaks: agreements can be quantity-tiered — PriceDisc considers the line quantity',
|
|
1784
|
+
'Dimension matching: agreements can be dimension-specific (color, size, config, style)',
|
|
1785
|
+
'Journal posting: PriceDiscAdmTrans → post (validate + transfer) → PriceDiscTable (active agreements)',
|
|
1786
|
+
'For custom pricing: extend PriceDisc via CoC on findPriceAgreement() or use pricing events',
|
|
1787
|
+
'NEVER hardcode prices in code — always use the trade agreement / pricing framework',
|
|
1788
|
+
'Supplementary items: PriceDiscAdmTrans can define supplementary items that auto-add to sales lines',
|
|
1789
|
+
],
|
|
1790
|
+
related: ['coc', 'query-patterns'],
|
|
1791
|
+
},
|
|
1792
|
+
// ── Configuration Keys ──────────────────────────────────────────────────
|
|
1793
|
+
{
|
|
1794
|
+
id: 'configuration-keys',
|
|
1795
|
+
title: 'Configuration Keys (Compile-time Feature Toggle)',
|
|
1796
|
+
keywords: ['configuration key', 'config key', 'license code', 'conditional compilation',
|
|
1797
|
+
'configurationkeynum', 'isconfiguationkeyenabled', 'sysconfigkey'],
|
|
1798
|
+
summary: 'Configuration keys in D365FO control compile-time visibility of tables, fields, menu items, ' +
|
|
1799
|
+
'and security. Unlike Feature Management (runtime), config keys require recompilation when changed. ' +
|
|
1800
|
+
'They are typically used for module licensing and major functional areas.',
|
|
1801
|
+
rules: [
|
|
1802
|
+
'Configuration keys are compile-time — changing them requires deployment/recompilation',
|
|
1803
|
+
'For runtime toggles, prefer Feature Management over config keys (no recompilation needed)',
|
|
1804
|
+
'Tables/fields with disabled config keys are excluded from the database schema',
|
|
1805
|
+
'Data entities respect config keys — disabled fields are excluded from DMF and OData',
|
|
1806
|
+
'Use isConfigurationKeyEnabled(configurationKeyNum(MyKey)) for runtime checks',
|
|
1807
|
+
'License codes: control which config keys are available — tied to ISV licensing',
|
|
1808
|
+
'Custom config keys: create AxConfigurationKey XML and assign to tables/fields/menu items',
|
|
1809
|
+
'Parent-child hierarchy: disabling a parent key disables all child keys',
|
|
1810
|
+
'ALWAYS test with your config keys disabled — ensure no compile errors in alternate configurations',
|
|
1811
|
+
'After config key changes: refresh entity list in Data Management workspace',
|
|
1812
|
+
],
|
|
1813
|
+
examples: [
|
|
1814
|
+
{
|
|
1815
|
+
label: 'Runtime config key check',
|
|
1816
|
+
code: `// Check if a configuration key is enabled at runtime
|
|
1817
|
+
if (isConfigurationKeyEnabled(configurationKeyNum(WHSAdvanced)))
|
|
1818
|
+
{
|
|
1819
|
+
// WHS advanced features are available
|
|
1820
|
+
this.processAdvancedWHS();
|
|
1821
|
+
}
|
|
1822
|
+
else
|
|
1823
|
+
{
|
|
1824
|
+
// Basic warehouse (WMS) path
|
|
1825
|
+
this.processBasicWMS();
|
|
1826
|
+
}`,
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
label: 'AxConfigurationKey XML',
|
|
1830
|
+
code: `<?xml version="1.0" encoding="utf-8"?>
|
|
1831
|
+
<AxConfigurationKey xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
|
|
1832
|
+
<Name>MyModuleKey</Name>
|
|
1833
|
+
<Label>@MyModel:ModuleLabel</Label>
|
|
1834
|
+
<Enabled>Yes</Enabled>
|
|
1835
|
+
<LicenseCode>MyModuleLicenseCode</LicenseCode>
|
|
1836
|
+
</AxConfigurationKey>`,
|
|
1837
|
+
},
|
|
1838
|
+
],
|
|
1839
|
+
related: ['feature-management', 'security-privileges-duties'],
|
|
1840
|
+
},
|
|
1841
|
+
// ── Power Platform Integration ──────────────────────────────────────────
|
|
1842
|
+
{
|
|
1843
|
+
id: 'power-platform-integration',
|
|
1844
|
+
title: 'Power Platform Integration (Virtual Entities, Dataverse)',
|
|
1845
|
+
keywords: ['power platform', 'power automate', 'power apps', 'virtual entity', 'dataverse',
|
|
1846
|
+
'finance operations connector', 'odata', 'business event', 'flow'],
|
|
1847
|
+
summary: 'D365FO integrates with Power Platform via: OData endpoints (data entities), Business Events (triggers for Power Automate), ' +
|
|
1848
|
+
'Virtual Entities (Dataverse tables backed by F&O data), and the Finance and Operations connector in Power Automate.',
|
|
1849
|
+
rules: [
|
|
1850
|
+
'OData endpoint: data entities with IsPublic=Yes are auto-exposed at {env}/data/{CollectionName}',
|
|
1851
|
+
'Business events: subscribe via Power Automate trigger "When a Business Event occurs" for real-time notifications',
|
|
1852
|
+
'Virtual entities: F&O tables/entities visible in Dataverse WITHOUT data duplication — queries route to F&O at runtime',
|
|
1853
|
+
'Virtual entity setup: enable in Dataverse admin, configure in Power Platform integration settings in F&O',
|
|
1854
|
+
'Finance and Operations connector: Power Automate actions for CRUD on data entities, running business events, executing batch jobs',
|
|
1855
|
+
'For custom triggers: create a custom business event class (extends BusinessEventsBase) → it auto-appears as a Power Automate trigger',
|
|
1856
|
+
'Authentication: virtual entities and OData use Azure AD (Entra ID) authentication — configure app registrations properly',
|
|
1857
|
+
'NEVER expose sensitive data via OData without proper security — configure security privileges on data entities',
|
|
1858
|
+
'Rate limiting: OData has throttling limits — use batch operations ($batch) for bulk CRUD',
|
|
1859
|
+
'For Canvas/Model-driven apps: use virtual entities for real-time F&O data, NOT dual-write (which is for bidirectional sync)',
|
|
1860
|
+
],
|
|
1861
|
+
related: ['data-entities', 'alerts-business-events', 'dual-write'],
|
|
1862
|
+
},
|
|
1863
|
+
// ── Select Statement Grammar ────────────────────────────────────────────
|
|
1864
|
+
{
|
|
1865
|
+
id: 'select-statement',
|
|
1866
|
+
title: 'X++ select Statement — Complete Grammar Reference',
|
|
1867
|
+
keywords: ['select', 'while select', 'findoption', 'firstonly', 'crosscompany', 'forupdate', 'join', 'outer join', 'exists join', 'notexists join', 'forceliterals', 'forceplaceholders', 'in operator', 'aggregate', 'sum', 'count', 'group by', 'validtimestate', 'index hint', 'grammar'],
|
|
1868
|
+
summary: 'Complete grammar reference for X++ select/while select. Statement order: [FindOptions] [FieldList from] tableBuffer [index] [order by / group by] [where …] [join … [where …]]. ' +
|
|
1869
|
+
'FindOptions go BETWEEN "select" and the table buffer. Each joined buffer has its own where clause immediately after it.',
|
|
1870
|
+
rules: [
|
|
1871
|
+
'FindOptions (crossCompany, firstOnly, forUpdate, forceNestedLoop, forceSelectOrder, forcePlaceholders, pessimisticLock, optimisticLock, repeatableRead, validTimeState, noFetch, reverse, firstFast) go BETWEEN "select" and the table buffer / field list',
|
|
1872
|
+
'crossCompany belongs on the OUTER (driving) buffer — never on a joined buffer. Optional container filter: select crossCompany : myContainer table …',
|
|
1873
|
+
'Each joined buffer gets its own "where" clause immediately after it; order by / group by appear after the full join chain',
|
|
1874
|
+
'"in" operator: "where field in container" — container = X++ container type; works with str/int/int64/real/enum/boolean/date/utcDateTime. NOT a Set, List class, or subquery',
|
|
1875
|
+
'forceLiterals is FORBIDDEN — SQL injection risk; use forcePlaceholders (default for non-join selects) or omit',
|
|
1876
|
+
'No function calls in WHERE — assign result to a local variable first (performance + BP compliance)',
|
|
1877
|
+
'outer join is LEFT OUTER only — no RIGHT outer, no "left" keyword; check joined buffer.RecId == 0 to detect "no match"',
|
|
1878
|
+
'Join criteria use "where", not "on" — X++ has no "on" keyword',
|
|
1879
|
+
'"index hint" requires buffer.allowIndexHint(true) to be called first; otherwise silently ignored — use only when measured',
|
|
1880
|
+
'Aggregates (sum/avg/count/minof/maxof): when sum would be null X++ returns NO row — guard with "if (buffer)" after the select',
|
|
1881
|
+
'Non-aggregated fields in select list must appear in "group by" when aggregates are used',
|
|
1882
|
+
'validTimeState(dateFrom, dateTo): use for date-effective tables (ValidTimeStateFieldType ≠ None)',
|
|
1883
|
+
'doInsert/doUpdate/doDelete bypass overridden methods and event handlers — reserved for data-fix/migration scenarios only',
|
|
1884
|
+
'For dynamic queries from user input: use executeQueryWithParameters API — NEVER concatenate into where clause',
|
|
1885
|
+
],
|
|
1886
|
+
examples: [
|
|
1887
|
+
{
|
|
1888
|
+
label: 'crossCompany — correct vs wrong placement',
|
|
1889
|
+
code: `// ✅ CORRECT — crossCompany on the driving buffer
|
|
1890
|
+
select crossCompany custTable
|
|
1891
|
+
join custInvoiceJour
|
|
1892
|
+
where custInvoiceJour.OrderAccount == custTable.AccountNum;
|
|
1893
|
+
|
|
1894
|
+
// ❌ WRONG — crossCompany on joined buffer
|
|
1895
|
+
select custTable
|
|
1896
|
+
join crossCompany custInvoiceJour where …;`,
|
|
1897
|
+
},
|
|
1898
|
+
{
|
|
1899
|
+
label: '"in" operator with container',
|
|
1900
|
+
code: `container statusFilter = [CustVendorBlocked::No, CustVendorBlocked::Invoice];
|
|
1901
|
+
CustTable custTable;
|
|
1902
|
+
while select AccountNum from custTable
|
|
1903
|
+
where custTable.Blocked in statusFilter
|
|
1904
|
+
{
|
|
1905
|
+
info(custTable.AccountNum);
|
|
1906
|
+
}`,
|
|
1907
|
+
},
|
|
1908
|
+
{
|
|
1909
|
+
label: 'Function in WHERE — wrong vs correct',
|
|
1910
|
+
code: `// ❌ WRONG — function call directly in WHERE
|
|
1911
|
+
select salesTable where salesTable.ShippingDateRequested == DateTimeUtil::getSystemDate(...);
|
|
1912
|
+
|
|
1913
|
+
// ✅ CORRECT — assign to variable first
|
|
1914
|
+
date cutoffDate = DateTimeUtil::getSystemDate(DateTimeUtil::getUserPreferredTimeZone());
|
|
1915
|
+
select salesTable where salesTable.ShippingDateRequested == cutoffDate;`,
|
|
1916
|
+
},
|
|
1917
|
+
],
|
|
1918
|
+
related: ['query-patterns', 'set-based', 'query-object-model'],
|
|
1919
|
+
},
|
|
1920
|
+
// ── CoC Authoring Non-negotiables ───────────────────────────────────────
|
|
1921
|
+
{
|
|
1922
|
+
id: 'coc-authoring',
|
|
1923
|
+
title: 'CoC Authoring Non-negotiables',
|
|
1924
|
+
keywords: ['coc', 'chain of command', 'next', 'default parameter', 'wrappable', 'hookable', 'final', 'extensionof', 'wrapper', 'form coc', 'formdatasourcestr', 'static coc', 'replaceable', 'pre', 'post', 'wrap'],
|
|
1925
|
+
summary: 'Strict rules for authoring CoC wrappers. The most common mistake is copying default parameter values. ' +
|
|
1926
|
+
'next must always be called at first-level scope. Always use get_method(include="signature") before writing any wrapper.',
|
|
1927
|
+
rules: [
|
|
1928
|
+
'NEVER copy default parameter values into the wrapper signature — wrapper uses bare parameter types only',
|
|
1929
|
+
'next must be at first-level statement scope: NOT inside if/while/for, NOT after return, NOT inside a logical expression. PU21+: permitted inside try/catch/finally',
|
|
1930
|
+
'Wrapper must always call next — except on [Replaceable] methods',
|
|
1931
|
+
'Signature otherwise matches base exactly: return type, param types and order, static modifier',
|
|
1932
|
+
'Static method wrappers must repeat "static". Forms cannot have static-method CoC',
|
|
1933
|
+
'Cannot wrap constructors; new parameterless public methods on extension class become the extension\'s own constructor',
|
|
1934
|
+
'Extension class shape: [ExtensionOf(<Str>(...))] final class <Target>_Extension — MUST be final',
|
|
1935
|
+
'[Hookable(false)] blocks CoC entirely. [Wrappable(false)] blocks wrapping; final methods need [Wrappable(true)] to allow wrapping',
|
|
1936
|
+
'Form-nested wrapping uses formdatasourcestr, formdatafieldstr, formControlStr. Cannot ADD new methods via CoC — only wrap existing ones (init, validateWrite, clicked, …)',
|
|
1937
|
+
'Wrappers can read/call protected members of the augmented class (PU9+); cannot reach private',
|
|
1938
|
+
'Pre-processing: call business logic before next. Post-processing: call next first, then business logic. Wrap: call next inside the logic',
|
|
1939
|
+
'Use get_method(include="signature") tool to get exact parameter types before writing the wrapper',
|
|
1940
|
+
'REUSE BEFORE CREATING: if a CoC extension class for the target already exists in the custom model (prepare(mode="change") / extension_info(mode="coc") lists them), add the wrapper there — never create a parallel feature-named class (<Target>_<Feature>_Extension) unless the user explicitly requests separation',
|
|
1941
|
+
'The class suffix comes from EXTENSION_NAMING_STYLE and existing related artifacts — never from feature names, tickets, or customer names; if it cannot be derived, ask the user',
|
|
1942
|
+
],
|
|
1943
|
+
examples: [
|
|
1944
|
+
{
|
|
1945
|
+
label: 'Default parameter — wrong vs correct',
|
|
1946
|
+
code: `// Base method
|
|
1947
|
+
public void salute(str message = "Hi") { … }
|
|
1948
|
+
|
|
1949
|
+
// ✅ CORRECT — no default value in wrapper
|
|
1950
|
+
public void salute(str message)
|
|
1951
|
+
{
|
|
1952
|
+
next salute(message);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// ❌ WRONG — copying the default breaks the CoC contract
|
|
1956
|
+
public void salute(str message = "Hi")
|
|
1957
|
+
{
|
|
1958
|
+
next salute(message); // compile error in strict mode
|
|
1959
|
+
}`,
|
|
1960
|
+
},
|
|
1961
|
+
{
|
|
1962
|
+
label: 'next placement — correct vs wrong scope',
|
|
1963
|
+
code: `// ✅ CORRECT — next at first-level scope (post-processing)
|
|
1964
|
+
public boolean validateWrite()
|
|
1965
|
+
{
|
|
1966
|
+
boolean ret = next validateWrite(); // first-level ✅
|
|
1967
|
+
if (this.CreditMax > 1000000)
|
|
1968
|
+
ret = checkFailed("@MyModel:CreditLimitExceeded");
|
|
1969
|
+
return ret;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// ❌ WRONG — next inside an if block
|
|
1973
|
+
public void post()
|
|
1974
|
+
{
|
|
1975
|
+
if (this.shouldPost())
|
|
1976
|
+
next post(); // NOT first-level scope ❌
|
|
1977
|
+
}`,
|
|
1978
|
+
},
|
|
1979
|
+
],
|
|
1980
|
+
related: ['coc', 'event-handlers'],
|
|
1981
|
+
},
|
|
1982
|
+
// ── X++ Class & Method Rules ─────────────────────────────────────────────
|
|
1983
|
+
{
|
|
1984
|
+
id: 'xpp-class-rules',
|
|
1985
|
+
title: 'X++ Class & Method Rules',
|
|
1986
|
+
keywords: ['class', 'method', 'access modifier', 'public', 'protected', 'private', 'internal', 'final', 'abstract', 'static', 'constructor', 'new', 'construct', 'parm', 'this', 'extension method', 'override', 'optional parameter', 'pass by value', 'var', 'const', 'macro'],
|
|
1987
|
+
summary: 'X++ class and method rules: access defaults, constructor pattern, modifier order, this usage, extension methods, optional parameters, and pass-by-value semantics.',
|
|
1988
|
+
rules: [
|
|
1989
|
+
'Class default access = public. Removing "public" does NOT make a class non-public. Use internal, final, abstract deliberately',
|
|
1990
|
+
'Instance fields default = protected — NEVER make them public; expose via parmFoo() accessors',
|
|
1991
|
+
'Constructor pattern: new() is protected, public static construct() factory; init() for post-construction setup',
|
|
1992
|
+
'Method modifier order: [edit|display] [public|protected|private|internal] [static|abstract|final]',
|
|
1993
|
+
'Override visibility: must be at least as accessible as the base method. private is not overridable',
|
|
1994
|
+
'Optional parameters must come after required ones; all preceding parameters must be supplied. Use prmIsDefault(_x) to detect "was this passed"',
|
|
1995
|
+
'All parameters are pass-by-value — mutating a parameter does NOT affect the caller\'s variable',
|
|
1996
|
+
'"this" is required for instance method calls; cannot qualify class-declaration member variables (use bare name); cannot be used in static methods; cannot qualify static methods (use ClassName::method())',
|
|
1997
|
+
'Extension methods (target Class/Table/View/Map): extension class must be static, name ends _Extension; methods are public static; first param is the target type, supplied by runtime',
|
|
1998
|
+
'Constants over macros: public const str FOO = "bar"; at class scope; reference via ClassName::FOO or unqualified inside the class',
|
|
1999
|
+
'"var" keyword only when the type is obvious from initialization; skip when ambiguous',
|
|
2000
|
+
'Declare variables close to first use, smallest scope; compiler rejects shadowing',
|
|
2001
|
+
],
|
|
2002
|
+
related: ['coc-authoring', 'coc'],
|
|
2003
|
+
},
|
|
2004
|
+
// ── SysDa Framework ─────────────────────────────────────────────────────
|
|
2005
|
+
{
|
|
2006
|
+
id: 'sysda',
|
|
2007
|
+
title: 'SysDa Framework — Fluent Query API',
|
|
2008
|
+
keywords: ['sysda', 'sysdaqueryobject', 'sysdafindstatement', 'sysdafindobj', 'sysdaupdatestatement', 'sysdaupdateobject', 'sysdainsertstatement', 'sysdadeletestatement', 'sysdaequalsexpression', 'sysdafieldexpression', 'sysdavalueexpression', 'fluent query', 'dynamic query', 'sysdajoinkind'],
|
|
2009
|
+
summary: 'SysDa is the modern X++ fluent/object-oriented query API. Use for dynamic queries where shape depends on runtime conditions. ' +
|
|
2010
|
+
'Use "select/while select" for static, known-at-compile-time queries (cleaner, faster to read, compile-time field validation).',
|
|
2011
|
+
rules: [
|
|
2012
|
+
'SysDaQueryObject: root query builder — set table buffer via constructor: new SysDaQueryObject(custTable)',
|
|
2013
|
+
'SysDaSearchObject / SysDaSearchStatement: execute query and populate buffers in a while loop via nextRecord()',
|
|
2014
|
+
'SysDaFindObject / SysDaFindStatement: firstOnly equivalent — returns true/false, populates buffer',
|
|
2015
|
+
'SysDaUpdateObject / SysDaUpdateStatement: set-based update without row-by-row fetch',
|
|
2016
|
+
'SysDaInsertObject / SysDaInsertStatement: set-based insert from another query result',
|
|
2017
|
+
'SysDaDeleteObject / SysDaDeleteStatement: set-based delete',
|
|
2018
|
+
'Joins: qe.joinClause(SysDaJoinKind::InnerJoin, joinQe) — supports Inner, Outer, Exists, NotExists',
|
|
2019
|
+
'Where clause: qe.whereClause(new SysDaEqualsExpression(new SysDaFieldExpression(...), new SysDaValueExpression(...)))',
|
|
2020
|
+
'Use SysDa when: query shape depends on runtime conditions, building framework/reusable logic, dynamic field selection',
|
|
2021
|
+
'Use "select/while select" when: static queries, compile-time field validation, clarity is preferred',
|
|
2022
|
+
],
|
|
2023
|
+
examples: [
|
|
2024
|
+
{
|
|
2025
|
+
label: 'Basic SysDa search',
|
|
2026
|
+
code: `CustTable custTable;
|
|
2027
|
+
var qe = new SysDaQueryObject(custTable);
|
|
2028
|
+
qe.whereClause(new SysDaEqualsExpression(
|
|
2029
|
+
new SysDaFieldExpression(custTable, fieldStr(CustTable, AccountNum)),
|
|
2030
|
+
new SysDaValueExpression('US-001')
|
|
2031
|
+
));
|
|
2032
|
+
var so = new SysDaSearchStatement();
|
|
2033
|
+
while (so.nextRecord(qe))
|
|
2034
|
+
{
|
|
2035
|
+
info(custTable.AccountNum);
|
|
2036
|
+
}`,
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
label: 'SysDa inner join',
|
|
2040
|
+
code: `CustTable custTable;
|
|
2041
|
+
CustTrans custTrans;
|
|
2042
|
+
var qMain = new SysDaQueryObject(custTable);
|
|
2043
|
+
var qJoin = new SysDaQueryObject(custTrans);
|
|
2044
|
+
qJoin.whereClause(new SysDaEqualsExpression(
|
|
2045
|
+
new SysDaFieldExpression(custTrans, fieldStr(CustTrans, AccountNum)),
|
|
2046
|
+
new SysDaFieldExpression(custTable, fieldStr(CustTable, AccountNum))
|
|
2047
|
+
));
|
|
2048
|
+
qMain.joinClause(SysDaJoinKind::InnerJoin, qJoin);
|
|
2049
|
+
|
|
2050
|
+
var so = new SysDaSearchStatement();
|
|
2051
|
+
while (so.nextRecord(qMain))
|
|
2052
|
+
{
|
|
2053
|
+
info(custTable.AccountNum);
|
|
2054
|
+
}`,
|
|
2055
|
+
},
|
|
2056
|
+
],
|
|
2057
|
+
related: ['query-patterns', 'query-object-model'],
|
|
2058
|
+
},
|
|
2059
|
+
// ── AOT Query Object Model ──────────────────────────────────────────────
|
|
2060
|
+
{
|
|
2061
|
+
id: 'query-object-model',
|
|
2062
|
+
title: 'AOT Query Object Model (Query / QueryRun)',
|
|
2063
|
+
keywords: ['query', 'queryrun', 'querybuilddsource', 'querybuildatasouce', 'querybuildrange', 'queryvalue', 'sysquery', 'findorcreaterange', 'adddatasource', 'addrange', 'addsortfield', 'joinmode', 'allowcrosscompany', 'addcompanyrange'],
|
|
2064
|
+
summary: 'The Query/QueryRun classes execute AOT-defined or runtime-built queries. Use for form/report data binding, ' +
|
|
2065
|
+
'when users dynamically modify filters (SysQueryForm), or when the same query is reused across multiple consumers.',
|
|
2066
|
+
rules: [
|
|
2067
|
+
'Query: defines structure (data sources, ranges, sorting, joins)',
|
|
2068
|
+
'QueryBuildDataSource (QBDS): one table in the query — add via query.addDataSource(tableNum(T))',
|
|
2069
|
+
'QueryBuildRange: filter — qbds.addRange(fieldNum(T, Field)).value(queryValue("X"))',
|
|
2070
|
+
'QueryRun: executes the query and iterates results via next() and get(tableNum(T))',
|
|
2071
|
+
'SysQuery::findOrCreateRange(qbds, fieldNum): idempotent range addition — use instead of addRange to avoid duplicate ranges',
|
|
2072
|
+
'QueryBuildDataSource::addDataSource(): nested join (child data source within parent DS)',
|
|
2073
|
+
'qbds.joinMode(JoinMode::ExistsJoin): set join type at runtime — ExistsJoin, NotExistsJoin, OuterJoin, InnerJoin',
|
|
2074
|
+
'query.allowCrossCompany(true) + query.addCompanyRange("dat"): cross-company at Query level',
|
|
2075
|
+
'Use AOT Query objects when: forms/reports bind to them, reusable across multiple consumers',
|
|
2076
|
+
'Use runtime Query when: user can dynamically modify filters (SysQueryRun), batch dialog filtering needed',
|
|
2077
|
+
'Use "select" for: inline data access where no dynamic filter UI is needed',
|
|
2078
|
+
],
|
|
2079
|
+
examples: [
|
|
2080
|
+
{
|
|
2081
|
+
label: 'Runtime Query with range and sorting',
|
|
2082
|
+
code: `Query query = new Query();
|
|
2083
|
+
QueryBuildDataSource qbds = query.addDataSource(tableNum(CustTable));
|
|
2084
|
+
qbds.addRange(fieldNum(CustTable, CustGroup)).value(queryValue('10'));
|
|
2085
|
+
qbds.addSortField(fieldNum(CustTable, AccountNum));
|
|
2086
|
+
QueryRun qr = new QueryRun(query);
|
|
2087
|
+
while (qr.next())
|
|
2088
|
+
{
|
|
2089
|
+
CustTable ct = qr.get(tableNum(CustTable));
|
|
2090
|
+
info(ct.AccountNum);
|
|
2091
|
+
}`,
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
label: 'SysQuery::findOrCreateRange — idempotent pattern',
|
|
2095
|
+
code: `// Use in form init() or executeQuery() CoC — safe to call multiple times
|
|
2096
|
+
QueryBuildDataSource qbds = this.queryBuildDataSource();
|
|
2097
|
+
SysQuery::findOrCreateRange(
|
|
2098
|
+
qbds,
|
|
2099
|
+
fieldNum(CustTable, CustGroup)).value('DOM');`,
|
|
2100
|
+
},
|
|
2101
|
+
],
|
|
2102
|
+
related: ['query-patterns', 'sysda'],
|
|
2103
|
+
},
|
|
2104
|
+
// ── FormRun Lifecycle ───────────────────────────────────────────────────
|
|
2105
|
+
{
|
|
2106
|
+
id: 'formrun-lifecycle',
|
|
2107
|
+
title: 'FormRun Lifecycle & Form Extension Points',
|
|
2108
|
+
keywords: ['formrun', 'form lifecycle', 'form init', 'form run', 'executequery', 'formdatasource', 'active', 'validatewrite', 'clicked', 'modified', 'form extension', 'research', 'element.args', 'element.design', 'formcontrol', 'formletterservicecontroller'],
|
|
2109
|
+
summary: 'D365FO forms follow a strict initialization sequence. Extensions use CoC on lifecycle methods. ' +
|
|
2110
|
+
'Never guess control names — use get_object_info(objectType="form", name=formName, options={searchControl:"..."}) before extending.',
|
|
2111
|
+
rules: [
|
|
2112
|
+
'Initialization sequence: form.init() → FormDataSource.init() per DS → form.run() → FormDataSource.executeQuery()',
|
|
2113
|
+
'form.init(): form structure loaded, data sources NOT yet active — safe for: adding ranges, modifying query before first run',
|
|
2114
|
+
'FormDataSource.init(): each data source initializes — add default ranges here, link types resolved',
|
|
2115
|
+
'FormDataSource.executeQuery(): fires on each refresh — modify query dynamically here (e.g., based on active record)',
|
|
2116
|
+
'FormDataSource.active(): fires when cursor moves to a new record — update dependent data sources or UI state',
|
|
2117
|
+
'FormDataSource.validateWrite(): custom validation before save — return false to prevent save',
|
|
2118
|
+
'FormDataSource.write(): post-save logic — record is already committed when this fires',
|
|
2119
|
+
'FormControl.clicked(): button click handler. FormControl.modified(): field value changed handler',
|
|
2120
|
+
'FormDataSource.research(retainPosition: true): refresh grid keeping cursor position (preferred over executeQuery for UI refresh)',
|
|
2121
|
+
'element.args(): access caller context (menu item, record, enum parameter passed via Args)',
|
|
2122
|
+
'FormDataSource.queryBuildDataSource(): access underlying QueryBuildDataSource for runtime range manipulation',
|
|
2123
|
+
'element.design().controlName(formControlStr(MyForm, MyControl)): access control instance by name at runtime',
|
|
2124
|
+
'NEVER guess control names — they differ from field names and are often prefixed; use get_object_info(objectType="form", name=formName, options={searchControl:"..."})',
|
|
2125
|
+
'Use [ExtensionOf(formStr(...))] for form-level CoC; forms cannot have static-method CoC',
|
|
2126
|
+
'Add data sources via d365fo_file(action="modify", operation="add-data-source")',
|
|
2127
|
+
'Add controls via d365fo_file(action="modify", operation="add-control", parentControl="TabGeneral")',
|
|
2128
|
+
'Typical overrides per pattern — SimpleList/setup: DS initValue + validateWrite; DetailsMaster: form init + DS active/validateWrite; DetailsTransaction: lines DS initValue (defaults from header) + header DS active; Dialog: form init (read element.args()) + closeOk; Lookup: form init + DS executeQuery (caller-context filter)',
|
|
2129
|
+
'generate_object(mode="scaffold", objectType="form", includeMethodStubs=true) injects these pattern-appropriate stubs automatically; object_patterns(domain="form", action="spec", pattern) lists them',
|
|
2130
|
+
],
|
|
2131
|
+
related: ['coc', 'form-patterns'],
|
|
2132
|
+
},
|
|
2133
|
+
// ── Reading Excel / CSV files in X++ ──────────────────────────────────────
|
|
2134
|
+
{
|
|
2135
|
+
id: 'file-readers',
|
|
2136
|
+
title: 'Reading Excel (XLSX) & CSV Files in X++',
|
|
2137
|
+
keywords: ['excel', 'xlsx', 'csv', 'openxml', 'spreadsheet', 'sysexcel', 'commaio', 'asciiio', 'file upload', 'fileupload', 'import file', 'read file', 'streamreader', 'fileuploadtemporarystoragestrategy', 'office', 'epplus'],
|
|
2138
|
+
summary: 'Reading uploaded Excel/CSV in cloud D365FO must be STREAM-based: the AOS is sandboxed (no Office, no arbitrary file-system access). ' +
|
|
2139
|
+
'Use the OpenXML SDK (DocumentFormat.OpenXml) for XLSX and a stream reader for CSV, both fed from a FileUpload stream — never COM Excel or file-path readers.',
|
|
2140
|
+
migration: {
|
|
2141
|
+
ax2012: 'SysExcelApplication / SysExcelWorksheet (COM), or CommaIo("C:\\\\file.csv") / AsciiIo file-path readers',
|
|
2142
|
+
d365fo: 'DocumentFormat.OpenXml.Packaging.SpreadsheetDocument over a System.IO.Stream (XLSX); CommaTextStreamIo / System.IO.StreamReader over a stream (CSV)',
|
|
2143
|
+
},
|
|
2144
|
+
rules: [
|
|
2145
|
+
'XLSX: use the OpenXML SDK — DocumentFormat.OpenXml.Packaging.SpreadsheetDocument::Open(stream, false). It is the only server-side-supported reader.',
|
|
2146
|
+
'⛔ NEVER use SysExcelApplication / SysExcelWorksheet / Microsoft.Office.Interop.Excel — COM Office is NOT installed on cloud AOS; it throws at runtime.',
|
|
2147
|
+
'CSV: read from a System.IO.Stream via CommaTextStreamIo (X++) or System.IO.StreamReader (.NET). ⛔ NEVER use file-path CommaIo / AsciiIo — the AOS has no access to a client/server file path.',
|
|
2148
|
+
'Get the stream from a FileUpload control: FileUploadTemporaryStorageStrategy.uploadResultFileName() → File::UseFileFromURL / openFileUploadDialog returns the stream. In a SysOperation, accept the storage URL as a contract member.',
|
|
2149
|
+
'CommaTextStreamIo.parmDelimiter / parmRecordDelimiter to set separators; check inFieldDelimiter for semicolon-separated regional CSVs.',
|
|
2150
|
+
'Always wrap .NET interop in try/catch and dispose: use System.IO streams in a try/finally and call package.Dispose()/Close().',
|
|
2151
|
+
'Encoding matters for CSV: read with the right System.Text.Encoding (UTF-8 vs ANSI/1252) or accented characters corrupt.',
|
|
2152
|
+
'For large imports prefer the Data Management Framework (DMF) with a file entity — hand-rolled readers are for ad-hoc/lightweight cases.',
|
|
2153
|
+
],
|
|
2154
|
+
examples: [
|
|
2155
|
+
{
|
|
2156
|
+
label: 'XLSX — OpenXML SDK over an uploaded stream',
|
|
2157
|
+
code: `using DocumentFormat.OpenXml.Packaging;
|
|
2158
|
+
using DocumentFormat.OpenXml.Spreadsheet;
|
|
2159
|
+
|
|
2160
|
+
public void readExcel(System.IO.Stream _stream)
|
|
2161
|
+
{
|
|
2162
|
+
SpreadsheetDocument doc = SpreadsheetDocument::Open(_stream, false);
|
|
2163
|
+
try
|
|
2164
|
+
{
|
|
2165
|
+
WorkbookPart wbPart = doc.get_WorkbookPart();
|
|
2166
|
+
WorksheetPart wsPart = wbPart.get_WorksheetParts().get_Item(0);
|
|
2167
|
+
DocumentFormat.OpenXml.OpenXmlReader reader =
|
|
2168
|
+
DocumentFormat.OpenXml.OpenXmlReader::Create(wsPart);
|
|
2169
|
+
|
|
2170
|
+
while (reader.Read())
|
|
2171
|
+
{
|
|
2172
|
+
if (reader.get_ElementType() == typeof(Row))
|
|
2173
|
+
{
|
|
2174
|
+
// read cells of the row …
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
finally
|
|
2179
|
+
{
|
|
2180
|
+
doc.Dispose();
|
|
2181
|
+
}
|
|
2182
|
+
}`,
|
|
2183
|
+
},
|
|
2184
|
+
{
|
|
2185
|
+
label: 'CSV — stream-based reader (cloud-safe)',
|
|
2186
|
+
code: `public void readCsv(System.IO.Stream _stream)
|
|
2187
|
+
{
|
|
2188
|
+
CommaTextStreamIo io = CommaTextStreamIo::constructForRead(_stream);
|
|
2189
|
+
io.inFieldDelimiter(';'); // regional CSV
|
|
2190
|
+
container line = io.read(); // header
|
|
2191
|
+
while (io.status() == IO_Status::Ok)
|
|
2192
|
+
{
|
|
2193
|
+
line = io.read();
|
|
2194
|
+
if (io.status() != IO_Status::Ok) break;
|
|
2195
|
+
str itemId = conPeek(line, 1);
|
|
2196
|
+
// …
|
|
2197
|
+
}
|
|
2198
|
+
}`,
|
|
2199
|
+
},
|
|
2200
|
+
],
|
|
2201
|
+
related: ['data-management-framework', 'error-handling'],
|
|
2202
|
+
},
|
|
2203
|
+
// ── Parallel / multi-threaded batch ───────────────────────────────────────
|
|
2204
|
+
{
|
|
2205
|
+
id: 'parallel-batch',
|
|
2206
|
+
title: 'Parallel (Multi-threaded) Batch Processing',
|
|
2207
|
+
keywords: ['parallel batch', 'multithread', 'multi-threaded', 'batchheader', 'addruntimetask', 'runtime task', 'batch task', 'fan out', 'partition', 'threads', 'batch bundling', 'addtask', 'adddependency', 'concurrent'],
|
|
2208
|
+
summary: 'To process large workloads in parallel, fan the work out into independent batch tasks added to one BatchHeader — the batch engine runs them concurrently across batch threads/AOS instances. ' +
|
|
2209
|
+
'A single run() method is single-threaded; ⛔ never use System.Threading inside batch.',
|
|
2210
|
+
rules: [
|
|
2211
|
+
'Fan-out pattern: in the controller/operation, partition the work (e.g. by a key range) and add one runtime task per partition to a BatchHeader via batchHeader.addRuntimeTask(task, server).',
|
|
2212
|
+
'Each task is its own RunBaseBatch (or SysOperation service) that processes ONE partition with its own ttsbegin/ttscommit scope — never one giant transaction across all data.',
|
|
2213
|
+
'⛔ NEVER use System.Threading.Thread / Tasks inside batch code — the batch framework owns threading; manual threads are unsupported and unsafe with the session/company context.',
|
|
2214
|
+
'Concurrency is controlled by the batch group + the AOS "Maximum batch threads" setting — not by your code. Size partitions so each task is a few minutes of work.',
|
|
2215
|
+
'Use BatchHeader::getCurrentBatchHeader() when adding tasks from within an already-running batch (self-spawning); construct a fresh BatchHeader when scheduling from a UI controller.',
|
|
2216
|
+
'Only add addDependency() between tasks when ordering is required — independent tasks with no dependencies maximise parallelism.',
|
|
2217
|
+
'Make each task idempotent/resumable: a parallel task may be retried after a transient failure, so guard against double-processing (e.g. status flag, RecId watermark).',
|
|
2218
|
+
'For data-parallel set work, also consider SysOperation with parmExecutionMode(SysOperationExecutionMode::ScheduledBatch) plus task fan-out — but the BatchHeader.addRuntimeTask split is the canonical approach.',
|
|
2219
|
+
],
|
|
2220
|
+
examples: [
|
|
2221
|
+
{
|
|
2222
|
+
label: 'Fan out independent tasks to one BatchHeader',
|
|
2223
|
+
code: `public void scheduleParallel(List _partitions)
|
|
2224
|
+
{
|
|
2225
|
+
BatchHeader batchHeader = BatchHeader::construct();
|
|
2226
|
+
ListEnumerator le = _partitions.getEnumerator();
|
|
2227
|
+
|
|
2228
|
+
while (le.moveNext())
|
|
2229
|
+
{
|
|
2230
|
+
MyPartitionTask task = new MyPartitionTask(); // extends RunBaseBatch
|
|
2231
|
+
task.parmPartitionKey(le.current());
|
|
2232
|
+
batchHeader.addRuntimeTask(task, this.parmCurrentBatch().RecId);
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
batchHeader.parmCaption("@MyModel:ParallelImport");
|
|
2236
|
+
batchHeader.save(); // tasks now run concurrently across batch threads
|
|
2237
|
+
}`,
|
|
2238
|
+
},
|
|
2239
|
+
],
|
|
2240
|
+
related: ['sysoperation', 'transactions', 'performance'],
|
|
2241
|
+
},
|
|
2242
|
+
// ── Direct SQL execution ──────────────────────────────────────────────────
|
|
2243
|
+
{
|
|
2244
|
+
id: 'direct-sql',
|
|
2245
|
+
title: 'Direct SQL Execution (Connection / Statement)',
|
|
2246
|
+
keywords: ['direct sql', 'connection', 'userconnection', 'statement', 'resultset', 'sqlstatementexecutepermission', 'executequery', 'executeupdate', 'ado', 'raw sql', 'sqlsystem', 'forceliterals sql'],
|
|
2247
|
+
summary: 'Direct SQL (Connection + Statement + ResultSet) bypasses the X++ data layer for performance-critical reads. ' +
|
|
2248
|
+
'It REQUIRES an explicit SqlStatementExecutePermission assert, and must use parameters — never string-concatenate user input.',
|
|
2249
|
+
rules: [
|
|
2250
|
+
'Always assert before executing: new SqlStatementExecutePermission(sql).assert(); run the statement; CodeAccessPermission::revertAssert(); — without the assert you get a CAS runtime error.',
|
|
2251
|
+
'⛔ NEVER concatenate user input into the SQL string — SQL injection. Build parameterised statements; treat any external value as hostile.',
|
|
2252
|
+
'Prefer X++ set-based operations (insert_recordset / update_recordset / delete_from) first — direct SQL is a last resort for reads X++ cannot express efficiently.',
|
|
2253
|
+
'Use Connection for the current company/partition DB; UserConnection when you need an explicit, separate transaction scope.',
|
|
2254
|
+
'Qualify by DataAreaId AND Partition in the WHERE clause — direct SQL does NOT apply the automatic company/partition filter that X++ select does.',
|
|
2255
|
+
'Field/table names in raw SQL are the SQL names (e.g. RECID, DATAAREAID) — use fieldId2name/tableId2name or dbg names, not necessarily the AOT label-cased names.',
|
|
2256
|
+
'Dispose/close ResultSet and Statement; keep the asserted scope as narrow as possible (assert immediately before execute, revert immediately after).',
|
|
2257
|
+
'Direct DDL, cross-database and use of forceLiterals are restricted/forbidden on cloud — keep direct SQL to parameterised SELECTs against the AX database.',
|
|
2258
|
+
],
|
|
2259
|
+
examples: [
|
|
2260
|
+
{
|
|
2261
|
+
label: 'SELECT with the required permission assert',
|
|
2262
|
+
code: `Connection conn = new Connection();
|
|
2263
|
+
Statement stmt = conn.createStatement();
|
|
2264
|
+
// Filter values must be validated/whitelisted, never raw user input.
|
|
2265
|
+
str custGroup = this.getValidatedCustGroup();
|
|
2266
|
+
str sql = strFmt(
|
|
2267
|
+
"SELECT RECID, ACCOUNTNUM FROM CUSTTABLE "
|
|
2268
|
+
+ "WHERE DATAAREAID = '%1' AND CUSTGROUP = '%2'",
|
|
2269
|
+
curext(), custGroup);
|
|
2270
|
+
|
|
2271
|
+
// REQUIRED — assert immediately before execute, revert immediately after
|
|
2272
|
+
new SqlStatementExecutePermission(sql).assert();
|
|
2273
|
+
try
|
|
2274
|
+
{
|
|
2275
|
+
ResultSet rs = stmt.executeQuery(sql);
|
|
2276
|
+
while (rs.next())
|
|
2277
|
+
{
|
|
2278
|
+
int64 recId = rs.getInt64(1);
|
|
2279
|
+
str accountNum = rs.getString(2);
|
|
2280
|
+
// …
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
finally
|
|
2284
|
+
{
|
|
2285
|
+
CodeAccessPermission::revertAssert();
|
|
2286
|
+
}`,
|
|
2287
|
+
},
|
|
2288
|
+
],
|
|
2289
|
+
related: ['set-based', 'select-statement', 'performance'],
|
|
2290
|
+
},
|
|
2291
|
+
// ── Menus & submenu nesting ────────────────────────────────────────────────
|
|
2292
|
+
{
|
|
2293
|
+
id: 'menu-navigation',
|
|
2294
|
+
title: 'Menus, Menu Items & Submenu Nesting',
|
|
2295
|
+
keywords: ['menu', 'submenu', 'sub-menu', 'navigation', 'axmenu', 'axmenuelement', 'menu item', 'nesting', 'inquiries and reports'],
|
|
2296
|
+
summary: 'An AxMenu is a flat <Elements> collection of AxMenuElement entries. A menu ITEM reference ' +
|
|
2297
|
+
'(display/action/output) is an AxMenuElementMenuItem. A NESTED SUBMENU (another AxMenu shown ' +
|
|
2298
|
+
'as a folder inside this one, e.g. "Inquiries and reports") is a DIFFERENT element type — ' +
|
|
2299
|
+
'AxMenuElementSubMenu — with its own field name, not a menu-item reference.',
|
|
2300
|
+
rules: [
|
|
2301
|
+
'Menu-item reference: <AxMenuElement i:type="AxMenuElementMenuItem"><Name>X</Name><MenuItemName>X</MenuItemName></AxMenuElement>',
|
|
2302
|
+
'Submenu reference (nesting another AxMenu inside this one): <AxMenuElement i:type="AxMenuElementSubMenu"><Name>X</Name><SubMenu>X</SubMenu></AxMenuElement> — ' +
|
|
2303
|
+
'the field is <SubMenu>, NOT <MenuName> and NOT <MenuItemName>. Verified against the real ' +
|
|
2304
|
+
'Microsoft.Dynamics.AX.Metadata.dll type names: AxMenuElementMenuItem, AxMenuElementSubMenu, ' +
|
|
2305
|
+
'AxMenuElementSeparator, AxMenuElementTile, AxMenuElementMenuReference (a different, legacy concept — not the one you want for a plain submenu).',
|
|
2306
|
+
'❌ There is no tool operation to add a submenu automatically: add-menu-item-to-menu\'s menuItemToAddType only accepts "display"/"action"/"output" (menu ITEMS). To nest a submenu you must hand-author the <AxMenuElementSubMenu> element into the parent menu\'s XML (d365fo_file create/modify with overwrite) — this is a genuine, current tool gap, not a workaround to avoid.',
|
|
2307
|
+
'A submenu is itself just a normal AxMenu object (create it with d365fo_file(action="create", objectType="menu")) — build its own items first, THEN add the AxMenuElementSubMenu reference to it from the parent menu.',
|
|
2308
|
+
'This applies both to brand-new standalone menus AND to menu-extensions (AxMenuExtension) nesting into a standard menu (e.g. under "Inquiries and reports") — neither path has a dedicated add-submenu tool operation.',
|
|
2309
|
+
'Always verify with build_d365fo_project after hand-authoring: a wrong element type name (e.g. the plausible-looking but nonexistent "AxMenuElementMenu"/"MenuName") is NOT caught by xppc itself — it only surfaces when the separate GenerateMetadata runtime-metadata step tries to deserialize the file ("cannot be deserialized as AxMenu ... no knowledge of any type that maps to this name").',
|
|
2310
|
+
],
|
|
2311
|
+
examples: [
|
|
2312
|
+
{
|
|
2313
|
+
label: 'Parent menu with two items and one nested submenu',
|
|
2314
|
+
code: `<AxMenu xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="Microsoft.Dynamics.AX.Metadata.V1">
|
|
2315
|
+
<Name>MyModule</Name>
|
|
2316
|
+
<Label>@MyModel:MyModule</Label>
|
|
2317
|
+
<Elements>
|
|
2318
|
+
<AxMenuElement xmlns="" i:type="AxMenuElementMenuItem">
|
|
2319
|
+
<Name>MyEquipment</Name>
|
|
2320
|
+
<MenuItemName>MyEquipment</MenuItemName>
|
|
2321
|
+
</AxMenuElement>
|
|
2322
|
+
<AxMenuElement xmlns="" i:type="AxMenuElementMenuItem">
|
|
2323
|
+
<Name>MyAgreement</Name>
|
|
2324
|
+
<MenuItemName>MyAgreement</MenuItemName>
|
|
2325
|
+
</AxMenuElement>
|
|
2326
|
+
<!-- Nested submenu: MySetup is itself a separate AxMenu object -->
|
|
2327
|
+
<AxMenuElement xmlns="" i:type="AxMenuElementSubMenu">
|
|
2328
|
+
<Name>MySetup</Name>
|
|
2329
|
+
<SubMenu>MySetup</SubMenu>
|
|
2330
|
+
</AxMenuElement>
|
|
2331
|
+
</Elements>
|
|
2332
|
+
</AxMenu>`,
|
|
2333
|
+
},
|
|
2334
|
+
],
|
|
2335
|
+
related: ['security', 'form-patterns'],
|
|
2336
|
+
},
|
|
2337
|
+
// ── Custom Services & OData Actions ─────────────────────────────────────
|
|
2338
|
+
{
|
|
2339
|
+
id: 'custom-services',
|
|
2340
|
+
title: 'Custom Services & OData Actions (SysEntryPointAttribute, Service Groups)',
|
|
2341
|
+
keywords: ['custom service', 'service', 'service group', 'odata action', 'sysentrypointattribute', 'sysentrypoint', 'axservice', 'axservicegroup', 'api services', 'service operation', 'json endpoint', 'integration endpoint', 'bound action', 'unbound action'],
|
|
2342
|
+
summary: 'Custom services expose X++ business logic as callable REST/SOAP operations. A service class holds the ' +
|
|
2343
|
+
'operation methods, an AxService names them, and an AxServiceGroup publishes them at /api/services. ' +
|
|
2344
|
+
'OData actions are the entity-bound alternative for verbs that do not fit CRUD.',
|
|
2345
|
+
rules: [
|
|
2346
|
+
'Service class: a normal X++ class whose PUBLIC methods become operations; each parameter/return type is a [DataContract] class or a primitive',
|
|
2347
|
+
'Authorization: every externally callable operation MUST carry [SysEntryPointAttribute(true)] (checkAccessRights=true) — without it the call is rejected/insecure',
|
|
2348
|
+
'AxService object: <Name>, <Class> (the service class), and <Operations> listing the exposed method names',
|
|
2349
|
+
'AxServiceGroup object: groups one or more services; its name is the URL segment — endpoint is /api/services/<ServiceGroup>/<Service>/<Operation>',
|
|
2350
|
+
'Data contract parameters: use [DataContractAttribute] classes with [DataMemberAttribute] parm methods — same contract style as SysOperation',
|
|
2351
|
+
'OData actions (entity-bound verbs): add a public static method on the data entity decorated with [SysODataActionAttribute("ActionName", true)]; first parameter type controls bound (entity) vs unbound (collection) — call at /data/Entities/Microsoft.Dynamics.DataEntities.ActionName',
|
|
2352
|
+
'Return a strongly-typed contract or a container — never raw text; keep operations idempotent where possible',
|
|
2353
|
+
'NEVER put long-running work in a synchronous service operation — schedule a SysOperation batch and return a job reference',
|
|
2354
|
+
'Custom services run under the caller\'s security context — do not bypass SysEntryPointAttribute checks',
|
|
2355
|
+
],
|
|
2356
|
+
examples: [
|
|
2357
|
+
{
|
|
2358
|
+
label: 'Service class + operation with SysEntryPointAttribute',
|
|
2359
|
+
code: `// 1. Data contract for the request payload
|
|
2360
|
+
[DataContractAttribute]
|
|
2361
|
+
class MyPriceRequestContract
|
|
2362
|
+
{
|
|
2363
|
+
ItemId itemId;
|
|
2364
|
+
|
|
2365
|
+
[DataMemberAttribute('ItemId')]
|
|
2366
|
+
public ItemId parmItemId(ItemId _itemId = itemId)
|
|
2367
|
+
{
|
|
2368
|
+
itemId = _itemId;
|
|
2369
|
+
return itemId;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// 2. Service class — the operation method
|
|
2374
|
+
class MyPriceService
|
|
2375
|
+
{
|
|
2376
|
+
/// <summary>
|
|
2377
|
+
/// Returns the current sales price for an item.
|
|
2378
|
+
/// </summary>
|
|
2379
|
+
[SysEntryPointAttribute(true)]
|
|
2380
|
+
public MyPriceResponseContract getPrice(MyPriceRequestContract _request)
|
|
2381
|
+
{
|
|
2382
|
+
MyPriceResponseContract response = new MyPriceResponseContract();
|
|
2383
|
+
response.parmPrice(MyPriceProvider::salesPrice(_request.parmItemId()));
|
|
2384
|
+
return response;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// 3. AxService lists getPrice; AxServiceGroup publishes it at
|
|
2389
|
+
// /api/services/<Group>/MyPriceService/getPrice`,
|
|
2390
|
+
},
|
|
2391
|
+
{
|
|
2392
|
+
label: 'OData action bound to a data entity',
|
|
2393
|
+
code: `// Public static method on the entity, callable via /data
|
|
2394
|
+
[SysODataActionAttribute('Recalculate', true)]
|
|
2395
|
+
public static str recalculate(str _entityKey)
|
|
2396
|
+
{
|
|
2397
|
+
// ... custom verb that does not map to insert/update/delete
|
|
2398
|
+
return 'OK';
|
|
2399
|
+
}`,
|
|
2400
|
+
},
|
|
2401
|
+
],
|
|
2402
|
+
related: ['data-entities', 'sysoperation', 'security-privileges-duties'],
|
|
2403
|
+
},
|
|
2404
|
+
// ── Table Inheritance ───────────────────────────────────────────────────
|
|
2405
|
+
{
|
|
2406
|
+
id: 'table-inheritance',
|
|
2407
|
+
title: 'Table Inheritance (SupportInheritance, Extends, InstanceRelationType)',
|
|
2408
|
+
keywords: ['table inheritance', 'supportinheritance', 'table extends', 'instancerelationtype', 'derived table', 'base table', 'polymorphic table', 'discriminator', 'relationtype'],
|
|
2409
|
+
summary: 'Table inheritance lets a base table share fields with derived tables while each row has a concrete type ' +
|
|
2410
|
+
'(like class inheritance for data). Microsoft uses it for e.g. the business-events endpoint hierarchy. ' +
|
|
2411
|
+
'It is a design-time table feature, NOT a runtime cast.',
|
|
2412
|
+
rules: [
|
|
2413
|
+
'Base table: set SupportInheritance = Yes and choose an InstanceRelationType field (the discriminator that stores which derived table each row belongs to)',
|
|
2414
|
+
'Derived table: set Extends = <BaseTable>; it inherits all base fields and adds its own',
|
|
2415
|
+
'The InstanceRelationType field on the base holds the tableId of the concrete (leaf) table for each record — the kernel uses it for polymorphic selects',
|
|
2416
|
+
'A select on the base table returns rows of ALL derived types; use the .isFormDataSource()/instanceof-style checks or select the specific derived buffer to narrow',
|
|
2417
|
+
'Common fields live ONCE on the base — do not duplicate them on derived tables',
|
|
2418
|
+
'You cannot change SupportInheritance or Extends on a shipped table via extension — inheritance is fixed at base-table design time',
|
|
2419
|
+
'Prefer inheritance only for genuine is-a hierarchies with shared behavior/fields; for optional add-on data a related table + relation is simpler',
|
|
2420
|
+
'Abstract base: mark the base table Abstract = Yes when it should never hold rows of its own type',
|
|
2421
|
+
],
|
|
2422
|
+
examples: [
|
|
2423
|
+
{
|
|
2424
|
+
label: 'Base + derived table shape (metadata summary)',
|
|
2425
|
+
code: `// Base table MyPartyBase:
|
|
2426
|
+
// SupportInheritance = Yes
|
|
2427
|
+
// InstanceRelationType = <MyPartyBase discriminator field>
|
|
2428
|
+
// Abstract = Yes
|
|
2429
|
+
// Fields: PartyId, Name
|
|
2430
|
+
|
|
2431
|
+
// Derived table MyPerson:
|
|
2432
|
+
// Extends = MyPartyBase
|
|
2433
|
+
// Fields: FirstName, LastName (PartyId/Name inherited)
|
|
2434
|
+
|
|
2435
|
+
// Derived table MyOrganization:
|
|
2436
|
+
// Extends = MyPartyBase
|
|
2437
|
+
// Fields: OrgNumber (PartyId/Name inherited)
|
|
2438
|
+
|
|
2439
|
+
// Polymorphic select — returns Person AND Organization rows:
|
|
2440
|
+
MyPartyBase party;
|
|
2441
|
+
while select party
|
|
2442
|
+
{
|
|
2443
|
+
if (party is MyPerson)
|
|
2444
|
+
{
|
|
2445
|
+
MyPerson person = party; // narrow to the leaf type
|
|
2446
|
+
info(person.FirstName);
|
|
2447
|
+
}
|
|
2448
|
+
}`,
|
|
2449
|
+
},
|
|
2450
|
+
],
|
|
2451
|
+
related: ['data-entities', 'query-patterns'],
|
|
2452
|
+
},
|
|
2453
|
+
// ── Async & Retryable Batch ─────────────────────────────────────────────
|
|
2454
|
+
{
|
|
2455
|
+
id: 'async-retryable-batch',
|
|
2456
|
+
title: 'Async & Retryable Batch (BatchRetryable, runAsync, SysOperationSandbox)',
|
|
2457
|
+
keywords: ['batchretryable', 'isretryable', 'runasync', 'sysoperationsandbox', 'retryable', 'async batch', 'transient fault', 'batch retry', 'reliable async'],
|
|
2458
|
+
summary: 'Batch tasks can opt into automatic retry on transient faults and run work asynchronously off the caller ' +
|
|
2459
|
+
'thread. Retryable tasks must be idempotent because the framework may re-run them.',
|
|
2460
|
+
rules: [
|
|
2461
|
+
'Retryable task: implement the BatchRetryable interface on your batch/SysOperation service class and return true from isRetryable() so the batch engine retries on transient faults (deadlock, SQL timeout)',
|
|
2462
|
+
'RetryCount / retry policy: the batch framework decides how many times to re-run — your isRetryable() only opts in; make the task IDEMPOTENT (safe to run twice)',
|
|
2463
|
+
'runAsync: use SysOperationSandbox / the async execution mode to run a unit of work off the current session thread when you must not block the caller (e.g. from a form)',
|
|
2464
|
+
'SysOperationSandbox runs a static method reliably in the background and surfaces infolog back to the user session — verify the exact entry-point method with get_object_info before relying on it',
|
|
2465
|
+
'Do NOT hold open transactions across an async boundary — start ttsbegin/ttscommit inside the async unit, not around it',
|
|
2466
|
+
'A retryable task must not accumulate side effects on partial failure — either make each run fully idempotent or guard with a completed-marker record',
|
|
2467
|
+
'For genuine parallelism (partitioning a large workload into independent tasks) combine this with the parallel-batch bundling pattern',
|
|
2468
|
+
'Never swallow the transient exception yourself if you opted into retry — let it propagate so the engine can re-run the task',
|
|
2469
|
+
],
|
|
2470
|
+
examples: [
|
|
2471
|
+
{
|
|
2472
|
+
label: 'Retryable SysOperation service',
|
|
2473
|
+
code: `class MyReconciliationService extends SysOperationServiceBase implements BatchRetryable
|
|
2474
|
+
{
|
|
2475
|
+
/// <summary>
|
|
2476
|
+
/// Opt into automatic retry on transient faults. Must be idempotent.
|
|
2477
|
+
/// </summary>
|
|
2478
|
+
public boolean isRetryable()
|
|
2479
|
+
{
|
|
2480
|
+
return true;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
public void run(MyReconciliationContract _contract)
|
|
2484
|
+
{
|
|
2485
|
+
ttsbegin;
|
|
2486
|
+
// idempotent work — safe if the engine re-runs after a transient fault
|
|
2487
|
+
this.reconcile(_contract.parmFromDate());
|
|
2488
|
+
ttscommit;
|
|
2489
|
+
}
|
|
2490
|
+
}`,
|
|
2491
|
+
},
|
|
2492
|
+
{
|
|
2493
|
+
label: 'Schedule background work off the caller thread',
|
|
2494
|
+
code: `// Run a SysOperation service in the background (ScheduledBatch) so the
|
|
2495
|
+
// caller (e.g. a form button) is not blocked while the work runs.
|
|
2496
|
+
MyReconciliationController controller = new MyReconciliationController();
|
|
2497
|
+
controller.parmExecutionMode(SysOperationExecutionMode::ScheduledBatch);
|
|
2498
|
+
controller.startOperation();`,
|
|
2499
|
+
},
|
|
2500
|
+
],
|
|
2501
|
+
related: ['sysoperation', 'parallel-batch', 'transactions'],
|
|
2502
|
+
},
|
|
2503
|
+
// ── Optimistic Concurrency & Unit of Work ───────────────────────────────
|
|
2504
|
+
{
|
|
2505
|
+
id: 'occ-unitofwork',
|
|
2506
|
+
title: 'Optimistic Concurrency & UnitOfWork (OccEnabled, RecVersion, UpdateConflict)',
|
|
2507
|
+
keywords: ['occ', 'optimistic concurrency', 'occenabled', 'recversion', 'updateconflict', 'updateconflictnotrecovered', 'unitofwork', 'pessimisticlock', 'optimisticlock', 'selectforupdate', 'concurrency', 'reread'],
|
|
2508
|
+
summary: 'Optimistic Concurrency Control (OCC) lets multiple sessions read the same record without locking and ' +
|
|
2509
|
+
'detects conflicts at write time via the RecVersion column. UnitOfWork batches related inserts/updates/' +
|
|
2510
|
+
'deletes into one coordinated, referential-integrity-aware commit.',
|
|
2511
|
+
rules: [
|
|
2512
|
+
'OCC is controlled by the table property OccEnabled (default Yes). With OCC on, a select does NOT hold an update lock; the lock is taken only at update time',
|
|
2513
|
+
'RecVersion: a hidden column the kernel bumps on every update; if it changed between your select and update, the kernel throws Exception::UpdateConflict',
|
|
2514
|
+
'forupdate + optimisticlock: default modern pattern — select forupdate optimisticlock, then update; re-read (reread()) inside the catch on UpdateConflict',
|
|
2515
|
+
'pessimisticlock: takes the lock at select time (blocks other writers) — use only for genuine hotspots where retry churn is worse than blocking',
|
|
2516
|
+
'UpdateConflict handling: catch (Exception::UpdateConflict) → reread() the record, re-apply your change, retry; if retries are exhausted the kernel throws UpdateConflictNotRecovered',
|
|
2517
|
+
'NEVER disable OccEnabled to \"avoid\" conflicts — it serialises writers and hurts throughput; fix the retry logic instead',
|
|
2518
|
+
'UnitOfWork: use new UnitOfWork(); register buffers with insertOnSaveChanges/updateOnSaveChanges/deleteOnSaveChanges, then saveChanges() commits them in dependency order within one transaction',
|
|
2519
|
+
'UnitOfWork resolves foreign-key order for you (parent inserted before child) — prefer it over hand-ordered inserts across related tables',
|
|
2520
|
+
'Do the whole read-modify-write of an OCC record inside one ttsbegin/ttscommit; keep the transaction short to shrink the conflict window',
|
|
2521
|
+
],
|
|
2522
|
+
examples: [
|
|
2523
|
+
{
|
|
2524
|
+
label: 'OCC update with UpdateConflict retry',
|
|
2525
|
+
code: `#OCCRetryCount
|
|
2526
|
+
CustTable custTable;
|
|
2527
|
+
|
|
2528
|
+
try
|
|
2529
|
+
{
|
|
2530
|
+
ttsbegin;
|
|
2531
|
+
select forupdate optimisticlock custTable
|
|
2532
|
+
where custTable.AccountNum == _accountNum;
|
|
2533
|
+
|
|
2534
|
+
custTable.CreditMax += 1000;
|
|
2535
|
+
custTable.update();
|
|
2536
|
+
ttscommit;
|
|
2537
|
+
}
|
|
2538
|
+
catch (Exception::UpdateConflict)
|
|
2539
|
+
{
|
|
2540
|
+
if (appl.ttsLevel() == 0)
|
|
2541
|
+
{
|
|
2542
|
+
if (xSession::currentRetryCount() >= #RetryNum)
|
|
2543
|
+
{
|
|
2544
|
+
throw Exception::UpdateConflictNotRecovered;
|
|
2545
|
+
}
|
|
2546
|
+
else
|
|
2547
|
+
{
|
|
2548
|
+
retry; // kernel re-reads and re-runs the tts block
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
else
|
|
2552
|
+
{
|
|
2553
|
+
throw Exception::UpdateConflict;
|
|
2554
|
+
}
|
|
2555
|
+
}`,
|
|
2556
|
+
},
|
|
2557
|
+
{
|
|
2558
|
+
label: 'UnitOfWork for related inserts',
|
|
2559
|
+
code: `UnitOfWork uow = new UnitOfWork();
|
|
2560
|
+
|
|
2561
|
+
MyHeader header = new MyHeader();
|
|
2562
|
+
header.OrderId = 'ORD-001';
|
|
2563
|
+
|
|
2564
|
+
MyLine line = new MyLine();
|
|
2565
|
+
line.ItemId = 'A-001';
|
|
2566
|
+
|
|
2567
|
+
// header inserted before line — UnitOfWork orders by relation
|
|
2568
|
+
uow.insertOnSaveChanges(header);
|
|
2569
|
+
uow.insertOnSaveChanges(line);
|
|
2570
|
+
uow.saveChanges();`,
|
|
2571
|
+
},
|
|
2572
|
+
],
|
|
2573
|
+
related: ['transactions', 'set-based', 'error-handling'],
|
|
2574
|
+
},
|
|
2575
|
+
// ── Caching (deep) ──────────────────────────────────────────────────────
|
|
2576
|
+
{
|
|
2577
|
+
id: 'caching',
|
|
2578
|
+
title: 'Caching — CacheLookup, SysGlobalObjectCache, RecordViewCache',
|
|
2579
|
+
keywords: ['cache', 'cachelookup', 'found', 'foundandempty', 'entiretable', 'notintts', 'sysglobalobjectcache', 'recordviewcache', 'flush', 'record cache', 'global cache', 'display method cache'],
|
|
2580
|
+
summary: 'D365FO has several caching layers. Table record caching (CacheLookup) is automatic and keyed by the ' +
|
|
2581
|
+
'primary/unique index; SysGlobalObjectCache is an explicit server-side key/value cache; RecordViewCache ' +
|
|
2582
|
+
'pre-loads a record set for repeated in-memory reads.',
|
|
2583
|
+
rules: [
|
|
2584
|
+
'CacheLookup table property values: None, NotInTTS, Found, FoundAndEmpty, EntireTable',
|
|
2585
|
+
'Found: caches records that were found by a unique-index lookup (most common for master/reference tables)',
|
|
2586
|
+
'FoundAndEmpty: like Found but ALSO caches \"not found\" results — use when many lookups miss (avoids repeat round-trips), at the cost of remembering absences',
|
|
2587
|
+
'EntireTable: loads the whole table into a per-AOS cache — ONLY for small, rarely-changing reference tables; a single insert/update/delete FLUSHES the entire-table cache cluster-wide',
|
|
2588
|
+
'NotInTTS: re-reads from DB (not cache) inside a transaction to guarantee a fresh row before update — cache is bypassed within ttsbegin/ttscommit',
|
|
2589
|
+
'Record caching only works for selects on the WHOLE primary/unique index (all key fields with ==) — a partial-key or range select bypasses the cache',
|
|
2590
|
+
'SysGlobalObjectCache (kernel class): explicit cross-session server cache — set(owner, key, value, scope) / find(owner, key, value, scope); scope controls DataArea vs Global; call clear/remove to invalidate. Use for expensive computed/config values, never for volatile transactional data',
|
|
2591
|
+
'RecordViewCache (kernel class): construct with a select-forupdate buffer to pre-load a set of records into memory once, then repeated while-select/find on the same criteria read from memory — ideal for tight loops re-reading the same working set',
|
|
2592
|
+
'Display/edit method caching: mark expensive display methods with [SysClientCacheDataMethodAttribute(true)] so the client caches the value instead of round-tripping per repaint',
|
|
2593
|
+
'NEVER cache security-sensitive or per-user data in SysGlobalObjectCache with a Global scope — leaks across companies/users',
|
|
2594
|
+
],
|
|
2595
|
+
examples: [
|
|
2596
|
+
{
|
|
2597
|
+
label: 'SysGlobalObjectCache read-through',
|
|
2598
|
+
code: `SysGlobalObjectCache goc = classfactory.globalObjectCache();
|
|
2599
|
+
container result = goc.find('MyModule', [_configKey]);
|
|
2600
|
+
|
|
2601
|
+
if (!result)
|
|
2602
|
+
{
|
|
2603
|
+
// Miss — compute the expensive value once and cache it
|
|
2604
|
+
MyValue value = MyExpensiveCalc::run(_configKey);
|
|
2605
|
+
result = [value];
|
|
2606
|
+
goc.insert('MyModule', [_configKey], result);
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
MyValue cached = conPeek(result, 1);`,
|
|
2610
|
+
},
|
|
2611
|
+
{
|
|
2612
|
+
label: 'RecordViewCache for a repeated working set',
|
|
2613
|
+
code: `InventDim inventDim;
|
|
2614
|
+
inventDim.InventLocationId = _warehouse;
|
|
2615
|
+
|
|
2616
|
+
// Pre-load all matching InventDim rows once into memory
|
|
2617
|
+
RecordViewCache cache = new RecordViewCache(inventDim);
|
|
2618
|
+
|
|
2619
|
+
// Subsequent finds on the same criteria read from the cache, not SQL
|
|
2620
|
+
InventDim lookup;
|
|
2621
|
+
select firstonly lookup
|
|
2622
|
+
where lookup.InventLocationId == _warehouse
|
|
2623
|
+
&& lookup.InventSiteId == _site;`,
|
|
2624
|
+
},
|
|
2625
|
+
],
|
|
2626
|
+
related: ['performance', 'set-based', 'transactions'],
|
|
2627
|
+
},
|
|
2628
|
+
];
|
|
2629
|
+
// ─── Search Logic ───────────────────────────────────────────────────────────
|
|
2630
|
+
function scoreEntry(entry, queryTokens) {
|
|
2631
|
+
let score = 0;
|
|
2632
|
+
const titleLower = entry.title.toLowerCase();
|
|
2633
|
+
const summaryLower = entry.summary.toLowerCase();
|
|
2634
|
+
for (const token of queryTokens) {
|
|
2635
|
+
// Exact keyword match (highest weight)
|
|
2636
|
+
if (entry.keywords.some(k => k === token))
|
|
2637
|
+
score += 10;
|
|
2638
|
+
// Partial keyword match
|
|
2639
|
+
else if (entry.keywords.some(k => k.includes(token) || token.includes(k)))
|
|
2640
|
+
score += 5;
|
|
2641
|
+
// Title match
|
|
2642
|
+
if (titleLower.includes(token))
|
|
2643
|
+
score += 3;
|
|
2644
|
+
// Summary match
|
|
2645
|
+
if (summaryLower.includes(token))
|
|
2646
|
+
score += 1;
|
|
2647
|
+
// ID match
|
|
2648
|
+
if (entry.id === token)
|
|
2649
|
+
score += 15;
|
|
2650
|
+
}
|
|
2651
|
+
return score;
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* Tokenize a topic query. Splits on whitespace/comma/semicolon/slash, then for
|
|
2655
|
+
* every token that itself contains a hyphen or underscore ALSO emits the split
|
|
2656
|
+
* sub-words. The original (joined) token is kept so entry-ID matches like
|
|
2657
|
+
* `set-based` still hit `entry.id === token`, while hyphenated multi-word
|
|
2658
|
+
* queries like `number-sequence` also match word-level keywords/titles (which
|
|
2659
|
+
* store the words separated by spaces, e.g. keyword "number sequence").
|
|
2660
|
+
*/
|
|
2661
|
+
function tokenize(topic) {
|
|
2662
|
+
const base = topic
|
|
2663
|
+
.toLowerCase()
|
|
2664
|
+
.replace(/[^a-z0-9áčďéěíňóřšťúůýž_\-/\s]/g, '')
|
|
2665
|
+
.split(/[\s,;/]+/)
|
|
2666
|
+
.filter(t => t.length > 1);
|
|
2667
|
+
const out = new Set();
|
|
2668
|
+
for (const tok of base) {
|
|
2669
|
+
out.add(tok);
|
|
2670
|
+
if (tok.includes('-') || tok.includes('_')) {
|
|
2671
|
+
for (const part of tok.split(/[-_]+/).filter(p => p.length > 1)) {
|
|
2672
|
+
out.add(part);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
return [...out];
|
|
2677
|
+
}
|
|
2678
|
+
/**
|
|
2679
|
+
* Minimum top score for a query to count as a confident match. Below this,
|
|
2680
|
+
* only incidental summary-substring overlap landed (no title/keyword/ID hit),
|
|
2681
|
+
* so results are surfaced as low-confidence suggestions, not authoritative answers.
|
|
2682
|
+
*/
|
|
2683
|
+
const CONFIDENT_SCORE = 3;
|
|
2684
|
+
function searchKnowledge(topic) {
|
|
2685
|
+
const tokens = tokenize(topic);
|
|
2686
|
+
if (tokens.length === 0) {
|
|
2687
|
+
// Return all entries sorted alphabetically
|
|
2688
|
+
return {
|
|
2689
|
+
entries: [...KNOWLEDGE_BASE].sort((a, b) => a.title.localeCompare(b.title)),
|
|
2690
|
+
topScore: 0,
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
const scored = KNOWLEDGE_BASE
|
|
2694
|
+
.map(entry => ({ entry, score: scoreEntry(entry, tokens) }))
|
|
2695
|
+
.filter(item => item.score > 0)
|
|
2696
|
+
.sort((a, b) => b.score - a.score);
|
|
2697
|
+
return {
|
|
2698
|
+
entries: scored.map(s => s.entry),
|
|
2699
|
+
topScore: scored.length > 0 ? scored[0].score : 0,
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
// ─── Formatters ─────────────────────────────────────────────────────────────
|
|
2703
|
+
function formatConcise(entries) {
|
|
2704
|
+
if (entries.length === 0) {
|
|
2705
|
+
return '❌ No matching knowledge entries found.\n\nAvailable topics:\n' +
|
|
2706
|
+
KNOWLEDGE_BASE.map(e => `- \`${e.id}\`: ${e.title}`).join('\n');
|
|
2707
|
+
}
|
|
2708
|
+
const parts = [];
|
|
2709
|
+
for (const entry of entries.slice(0, 5)) {
|
|
2710
|
+
parts.push(`## ${entry.title}\n`);
|
|
2711
|
+
parts.push(`${entry.summary}\n`);
|
|
2712
|
+
if (entry.migration) {
|
|
2713
|
+
parts.push(`**AX2012:** ${entry.migration.ax2012}`);
|
|
2714
|
+
parts.push(`**D365FO:** ${entry.migration.d365fo}\n`);
|
|
2715
|
+
}
|
|
2716
|
+
parts.push('**Rules:**');
|
|
2717
|
+
for (const rule of entry.rules) {
|
|
2718
|
+
parts.push(`- ${rule}`);
|
|
2719
|
+
}
|
|
2720
|
+
if (entry.related && entry.related.length > 0) {
|
|
2721
|
+
parts.push(`\n_Related: ${entry.related.join(', ')}_`);
|
|
2722
|
+
}
|
|
2723
|
+
parts.push('');
|
|
2724
|
+
}
|
|
2725
|
+
if (entries.length > 5) {
|
|
2726
|
+
parts.push(`_...and ${entries.length - 5} more entries. Use a more specific query to narrow results._`);
|
|
2727
|
+
}
|
|
2728
|
+
return parts.join('\n');
|
|
2729
|
+
}
|
|
2730
|
+
function formatDetailed(entries) {
|
|
2731
|
+
if (entries.length === 0) {
|
|
2732
|
+
return '❌ No matching knowledge entries found.\n\nAvailable topics:\n' +
|
|
2733
|
+
KNOWLEDGE_BASE.map(e => `- \`${e.id}\`: ${e.title}`).join('\n');
|
|
2734
|
+
}
|
|
2735
|
+
const parts = [];
|
|
2736
|
+
for (const entry of entries.slice(0, 3)) {
|
|
2737
|
+
parts.push(`# ${entry.title}\n`);
|
|
2738
|
+
parts.push(`${entry.summary}\n`);
|
|
2739
|
+
if (entry.migration) {
|
|
2740
|
+
parts.push('## AX2012 → D365FO Migration\n');
|
|
2741
|
+
parts.push(`| AX2012 (legacy) | D365FO (correct) |`);
|
|
2742
|
+
parts.push(`|---|---|`);
|
|
2743
|
+
parts.push(`| ${entry.migration.ax2012} | ${entry.migration.d365fo} |\n`);
|
|
2744
|
+
}
|
|
2745
|
+
parts.push('## Rules\n');
|
|
2746
|
+
for (const rule of entry.rules) {
|
|
2747
|
+
parts.push(`- ${rule}`);
|
|
2748
|
+
}
|
|
2749
|
+
parts.push('');
|
|
2750
|
+
if (entry.examples && entry.examples.length > 0) {
|
|
2751
|
+
parts.push('## Code Examples\n');
|
|
2752
|
+
for (const ex of entry.examples) {
|
|
2753
|
+
parts.push(`### ${ex.label}\n`);
|
|
2754
|
+
parts.push('```xpp');
|
|
2755
|
+
parts.push(ex.code);
|
|
2756
|
+
parts.push('```\n');
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
if (entry.related && entry.related.length > 0) {
|
|
2760
|
+
const relatedTitles = entry.related
|
|
2761
|
+
.map(id => KNOWLEDGE_BASE.find(e => e.id === id))
|
|
2762
|
+
.filter(Boolean)
|
|
2763
|
+
.map(e => `\`${e.id}\` (${e.title})`);
|
|
2764
|
+
parts.push(`**Related topics:** ${relatedTitles.join(', ')}\n`);
|
|
2765
|
+
}
|
|
2766
|
+
parts.push('---\n');
|
|
2767
|
+
}
|
|
2768
|
+
if (entries.length > 3) {
|
|
2769
|
+
parts.push(`_${entries.length - 3} more entries matched. Use a more specific query._`);
|
|
2770
|
+
}
|
|
2771
|
+
return parts.join('\n');
|
|
2772
|
+
}
|
|
2773
|
+
// ─── Tool Handler ───────────────────────────────────────────────────────────
|
|
2774
|
+
export async function xppKnowledgeTool(request) {
|
|
2775
|
+
try {
|
|
2776
|
+
const args = XppKnowledgeArgsSchema.parse(request.params.arguments);
|
|
2777
|
+
const { entries, topScore } = searchKnowledge(args.topic);
|
|
2778
|
+
// Empty topic → compact table of contents listing ALL entries
|
|
2779
|
+
const isListAll = args.topic.trim() === '';
|
|
2780
|
+
let formatted;
|
|
2781
|
+
if (isListAll) {
|
|
2782
|
+
formatted =
|
|
2783
|
+
'# X++ Knowledge Base — All Topics\n\n' +
|
|
2784
|
+
entries.map(e => `- \`${e.id}\`: **${e.title}**`).join('\n') +
|
|
2785
|
+
'\n\n_Query a specific topic with \`get_knowledge(kind="knowledge")\` for rules and code examples._';
|
|
2786
|
+
}
|
|
2787
|
+
else {
|
|
2788
|
+
formatted = args.format === 'detailed'
|
|
2789
|
+
? formatDetailed(entries)
|
|
2790
|
+
: formatConcise(entries);
|
|
2791
|
+
// Low-confidence guard: when something matched but only weakly (incidental
|
|
2792
|
+
// substring overlap, no title/keyword/ID hit), warn so the caller doesn't
|
|
2793
|
+
// treat unrelated content as authoritative.
|
|
2794
|
+
if (entries.length > 0 && topScore < CONFIDENT_SCORE) {
|
|
2795
|
+
formatted =
|
|
2796
|
+
`⚠️ No strong match for "${args.topic}" — showing the closest entries below, which may be ` +
|
|
2797
|
+
`unrelated. Browse the full list with \`get_knowledge(kind="knowledge")\` and an empty topic, ` +
|
|
2798
|
+
`or refine your query.\n\n${formatted}`;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
return {
|
|
2802
|
+
content: [{ type: 'text', text: formatted }],
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
catch (error) {
|
|
2806
|
+
return {
|
|
2807
|
+
content: [{
|
|
2808
|
+
type: 'text',
|
|
2809
|
+
text: `Error in get_knowledge(kind="knowledge"): ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2810
|
+
}],
|
|
2811
|
+
isError: true,
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
//# sourceMappingURL=xppKnowledge.js.map
|