├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .git2gus └── config.json ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── no-response.yml └── workflows │ ├── automerge.yml │ ├── create-github-release.yml │ ├── devScripts.yml │ ├── failureNotifications.yml │ ├── notify-slack-on-pr-open.yml │ ├── onRelease.yml │ ├── test.yml │ └── validate-pr.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .images └── vscodeScreenshot.png ├── .lintstagedrc.cjs ├── .mocharc.json ├── .nycrc ├── .prettierrc.json ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── NewTreeCommandsPlan.md ├── README.md ├── SECURITY.md ├── bin ├── dev.cmd ├── dev.js ├── run.cmd └── run.js ├── command-snapshot.json ├── commitlint.config.cjs ├── messages ├── batcher.md ├── bulk.delete.md ├── bulk.delete.resume.md ├── bulk.status.md ├── bulk.upsert.md ├── bulk.upsert.resume.md ├── bulkIngest.md ├── bulkv2.delete.md ├── bulkv2.upsert.md ├── data.bulk.results.md ├── data.create.file.md ├── data.export.bulk.md ├── data.export.resume.md ├── data.import.bulk.md ├── data.import.resume.md ├── data.resume.md ├── data.search.md ├── data.update.bulk.md ├── data.update.resume.md ├── exportApi.md ├── importApi.md ├── messages.md ├── record.create.md ├── record.delete.md ├── record.get.md ├── record.update.md ├── soql.query.md ├── tree.export.md └── tree.import.md ├── package.json ├── schema └── dataImportPlanSchema.json ├── src ├── api │ ├── data │ │ └── tree │ │ │ ├── exportApi.ts │ │ │ ├── functions.ts │ │ │ ├── importCommon.ts │ │ │ ├── importFiles.ts │ │ │ ├── importPlan.ts │ │ │ └── importTypes.ts │ └── file │ │ └── fileToContentVersion.ts ├── batcher.ts ├── bulkDataRequestCache.ts ├── bulkIngest.ts ├── bulkUtils.ts ├── commands │ ├── data │ │ ├── bulk │ │ │ └── results.ts │ │ ├── create │ │ │ ├── file.ts │ │ │ └── record.ts │ │ ├── delete │ │ │ ├── bulk.ts │ │ │ ├── record.ts │ │ │ └── resume.ts │ │ ├── export │ │ │ ├── bulk.ts │ │ │ ├── resume.ts │ │ │ └── tree.ts │ │ ├── get │ │ │ └── record.ts │ │ ├── import │ │ │ ├── bulk.ts │ │ │ ├── resume.ts │ │ │ └── tree.ts │ │ ├── query.ts │ │ ├── resume.ts │ │ ├── search.ts │ │ ├── update │ │ │ ├── bulk.ts │ │ │ ├── record.ts │ │ │ └── resume.ts │ │ └── upsert │ │ │ ├── bulk.ts │ │ │ └── resume.ts │ └── force │ │ └── data │ │ └── bulk │ │ ├── delete.ts │ │ ├── status.ts │ │ └── upsert.ts ├── dataUtils.ts ├── export.ts ├── flags.ts ├── index.ts ├── queryUtils.ts ├── reporters │ ├── query │ │ ├── csvReporter.ts │ │ ├── humanReporter.ts │ │ └── reporters.ts │ └── search │ │ ├── csvSearchReporter.ts │ │ ├── humanSearchReporter.ts │ │ └── reporter.ts ├── searchUtils.ts ├── types.ts └── ux │ └── bulkIngestStages.ts ├── test ├── .eslintrc.cjs ├── api │ └── data │ │ └── tree │ │ ├── export.test.ts │ │ ├── exportApi.test.ts │ │ ├── importFiles.test.ts │ │ ├── importPlan.test.ts │ │ └── test-files │ │ ├── accounts-contacts-plan.json │ │ ├── accounts-contacts-tree.json │ │ ├── accounts-only.json │ │ ├── contacts-only-1.json │ │ ├── contacts-only-2.json │ │ ├── contacts-only-2.sdx │ │ └── travel-expense.json ├── bulkUtils.test.ts ├── commands │ ├── batcher.test.ts │ ├── data │ │ ├── bulk │ │ │ └── results.nut.ts │ │ ├── create │ │ │ ├── file.nut.ts │ │ │ └── record.test.ts │ │ ├── dataBulk.nut.ts │ │ ├── dataCommand.test.ts │ │ ├── delete │ │ │ └── record.test.ts │ │ ├── export │ │ │ ├── bulk.nut.ts │ │ │ ├── resume.nut.ts │ │ │ └── tree.test.ts │ │ ├── get │ │ │ └── record.test.ts │ │ ├── import │ │ │ ├── bulk.nut.ts │ │ │ └── resume.nut.ts │ │ ├── query.test.ts │ │ ├── query │ │ │ └── query.nut.ts │ │ ├── record │ │ │ └── dataRecord.nut.ts │ │ ├── search.nut.ts │ │ ├── tree │ │ │ ├── dataTree.nut.ts │ │ │ ├── dataTreeCommonChild.nut.ts │ │ │ ├── dataTreeDeep.nut.ts │ │ │ ├── dataTreeDeepBeta.nut.ts │ │ │ ├── dataTreeJunction.nut.ts │ │ │ ├── dataTreeMissingRef.nut.ts │ │ │ ├── dataTreeMoreThan200.nut.ts │ │ │ └── dataTreeSelfReferencing.nut.ts │ │ └── update │ │ │ ├── bulk.nut.ts │ │ │ ├── record.test.ts │ │ │ └── resume.nut.ts │ └── force │ │ └── data │ │ └── bulk │ │ ├── dataBulk.nut.ts │ │ └── upsert.test.ts ├── queryFields.test.ts ├── reporters │ ├── csv.test.ts │ ├── csvSearchReporter.test.ts │ ├── humanSearchReporter.test.ts │ ├── reporter.test.ts │ └── reporters.test.ts ├── test-files │ ├── csv │ │ ├── backquote.csv │ │ ├── caret.csv │ │ ├── comma.csv │ │ ├── comma_wrapped_values.csv │ │ ├── pipe.csv │ │ ├── semicolon.csv │ │ ├── single-column.csv │ │ └── tab.csv │ ├── data-project │ │ ├── config │ │ │ └── project-scratch-def.json │ │ ├── data │ │ │ ├── accounts-contacts-plan.json │ │ │ ├── accounts-contacts-tree.json │ │ │ ├── accounts-contacts-tree2.json │ │ │ ├── accounts-only.json │ │ │ ├── bulkUpsert.csv │ │ │ ├── bulkUpsertBackquote.csv │ │ │ ├── bulkUpsertLarge.csv │ │ │ ├── commonChild │ │ │ │ ├── Account-Opportunity-Task-Case-plan.json │ │ │ │ ├── Account.json │ │ │ │ ├── Case.json │ │ │ │ ├── Opportunity.json │ │ │ │ └── Task.json │ │ │ ├── contacts-only-1.json │ │ │ ├── contacts-only-2.json │ │ │ ├── contacts-only-2.sdx │ │ │ ├── deep │ │ │ │ ├── accounts-contacts-plan.json │ │ │ │ ├── accounts-only.json │ │ │ │ ├── contacts-only-1.json │ │ │ │ └── contacts-only-2.json │ │ │ ├── junction │ │ │ │ ├── Account-AccountContactRelation-Contact-plan.json │ │ │ │ ├── Account.json │ │ │ │ ├── AccountContactRelation.json │ │ │ │ └── Contact.json │ │ │ ├── missingRef │ │ │ │ ├── Account-Opportunity-plan.json │ │ │ │ ├── Account.json │ │ │ │ └── Opportunity.json │ │ │ ├── missingSelfRef │ │ │ │ ├── Account-plan.json │ │ │ │ └── Account.json │ │ │ ├── moreThan200 │ │ │ │ ├── Account-plan.json │ │ │ │ └── Account.json │ │ │ └── self-referencing │ │ │ │ ├── Account-plan.json │ │ │ │ └── Account.json │ │ ├── force-app │ │ │ └── main │ │ │ │ └── default │ │ │ │ ├── classes │ │ │ │ ├── MyClass.cls │ │ │ │ ├── MyClass.cls-meta.xml │ │ │ │ ├── MyClassTest.cls │ │ │ │ └── MyClassTest.cls-meta.xml │ │ │ │ ├── objects │ │ │ │ └── Test_Object__c │ │ │ │ │ ├── Test_Object__c.object-meta.xml │ │ │ │ │ └── fields │ │ │ │ │ └── Bool__c.field-meta.xml │ │ │ │ └── permissionsets │ │ │ │ └── TestPerm.permissionset-meta.xml │ │ └── sfdx-project.json │ ├── queryFields.exemplars.ts │ └── soqlQuery.exemplars.ts ├── testUtil.ts └── tsconfig.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.cjs/ 2 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint-config-salesforce-typescript', 'eslint-config-salesforce-license', 'plugin:sf-plugin/recommended'], 3 | }; 4 | -------------------------------------------------------------------------------- /.git2gus/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "productTag": "a1aB00000004Bx8IAE", 3 | "defaultBuild": "offcore.tooling.56", 4 | "issueTypeLabels": { 5 | "feature": "USER STORY", 6 | "regression": "BUG P1", 7 | "bug": "BUG P3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | 9 | 10 | 13 | 14 | ### Summary 15 | 16 | _Short summary of what is going on or to provide context_. 17 | 18 | ### Steps To Reproduce: 19 | 20 | 1. This is step 1. 21 | 1. This is step 2. All steps should start with '1.' 22 | 23 | ### Expected result 24 | 25 | _Describe what should have happened_. 26 | 27 | ### Actual result 28 | 29 | _Describe what actually happened instead_. 30 | 31 | ### Additional information 32 | 33 | _Feel free to attach a screenshot_. 34 | 35 | **VS Code Version**: 36 | 37 | **SF CLI Version**: 38 | 39 | **OS and version**: 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What does this PR do? 2 | 3 | ### What issues does this PR fix or reference? 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | day: 'saturday' 8 | versioning-strategy: 'increase' 9 | labels: 10 | - 'dependencies' 11 | open-pull-requests-limit: 5 12 | pull-request-branch-name: 13 | separator: '-' 14 | commit-message: 15 | # cause a release for non-dev-deps 16 | prefix: fix(deps) 17 | # no release for dev-deps 18 | prefix-development: chore(dev-deps) 19 | ignore: 20 | - dependency-name: '@salesforce/dev-scripts' 21 | - dependency-name: '*' 22 | update-types: ['version-update:semver-major'] 23 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | daysUntilClose: 7 4 | responseRequiredLabel: 'more information required' 5 | closeComment: > 6 | This issue has been automatically closed because there has been no response 7 | to our request for more information from the original author. Currently, there 8 | is not enough information provided for us to take action. Please reply and 9 | reopen this issue if you need additional assistance. 10 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: automerge 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '42 2,5,8,11 * * *' 6 | 7 | jobs: 8 | automerge: 9 | uses: salesforcecli/github-workflows/.github/workflows/automerge.yml@main 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /.github/workflows/create-github-release.yml: -------------------------------------------------------------------------------- 1 | name: create-github-release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - prerelease/** 8 | tags-ignore: 9 | - '*' 10 | workflow_dispatch: 11 | inputs: 12 | prerelease: 13 | type: string 14 | description: 'Name to use for the prerelease: beta, dev, etc. NOTE: If this is already set in the package.json, it does not need to be passed in here.' 15 | 16 | jobs: 17 | release: 18 | uses: salesforcecli/github-workflows/.github/workflows/create-github-release.yml@main 19 | secrets: inherit 20 | with: 21 | prerelease: ${{ inputs.prerelease }} 22 | # If this is a push event, we want to skip the release if there are no semantic commits 23 | # However, if this is a manual release (workflow_dispatch), then we want to disable skip-on-empty 24 | # This helps recover from forgetting to add semantic commits ('fix:', 'feat:', etc.) 25 | skip-on-empty: ${{ github.event_name == 'push' }} 26 | # docs: 27 | # # Most repos won't use this 28 | # # Depends on the 'release' job to avoid git collisions, not for any functionality reason 29 | # needs: release 30 | # secrets: inherit 31 | # if: ${{ github.ref_name == 'main' }} 32 | # uses: salesforcecli/github-workflows/.github/workflows/publishTypedoc.yml@main 33 | -------------------------------------------------------------------------------- /.github/workflows/devScripts.yml: -------------------------------------------------------------------------------- 1 | name: devScripts 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '50 6 * * 0' 6 | 7 | jobs: 8 | update: 9 | uses: salesforcecli/github-workflows/.github/workflows/devScriptsUpdate.yml@main 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /.github/workflows/failureNotifications.yml: -------------------------------------------------------------------------------- 1 | name: failureNotifications 2 | on: 3 | workflow_run: 4 | workflows: 5 | - publish 6 | - create-github-release 7 | types: 8 | - completed 9 | jobs: 10 | failure-notify: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.event.workflow_run.conclusion == 'failure' }} 13 | steps: 14 | - name: Announce Failure 15 | id: slack 16 | uses: slackapi/slack-github-action@v1.26.0 17 | env: 18 | # for non-CLI-team-owned plugins, you can send this anywhere you like 19 | SLACK_WEBHOOK_URL: ${{ secrets.CLI_ALERTS_SLACK_WEBHOOK }} 20 | SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK 21 | with: 22 | # Payload can be visually tested here: https://app.slack.com/block-kit-builder/T01GST6QY0G#%7B%22blocks%22:%5B%5D%7D 23 | # Only copy over the "blocks" array to the Block Kit Builder 24 | payload: | 25 | { 26 | "text": "Workflow \"${{ github.event.workflow_run.name }}\" failed in ${{ github.event.workflow_run.repository.name }}", 27 | "blocks": [ 28 | { 29 | "type": "header", 30 | "text": { 31 | "type": "plain_text", 32 | "text": ":bh-alert: Workflow \"${{ github.event.workflow_run.name }}\" failed in ${{ github.event.workflow_run.repository.name }} :bh-alert:" 33 | } 34 | }, 35 | { 36 | "type": "section", 37 | "text": { 38 | "type": "mrkdwn", 39 | "text": "*Repo:* ${{ github.event.workflow_run.repository.html_url }}\n*Workflow name:* `${{ github.event.workflow_run.name }}`\n*Job url:* ${{ github.event.workflow_run.html_url }}" 40 | } 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/notify-slack-on-pr-open.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Slack Notification 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Notify Slack on PR open 12 | env: 13 | WEBHOOK_URL : ${{ secrets.CLI_TEAM_SLACK_WEBHOOK_URL }} 14 | PULL_REQUEST_AUTHOR_ICON_URL : ${{ github.event.pull_request.user.avatar_url }} 15 | PULL_REQUEST_AUTHOR_NAME : ${{ github.event.pull_request.user.login }} 16 | PULL_REQUEST_AUTHOR_PROFILE_URL: ${{ github.event.pull_request.user.html_url }} 17 | PULL_REQUEST_BASE_BRANCH_NAME : ${{ github.event.pull_request.base.ref }} 18 | PULL_REQUEST_COMPARE_BRANCH_NAME : ${{ github.event.pull_request.head.ref }} 19 | PULL_REQUEST_NUMBER : ${{ github.event.pull_request.number }} 20 | PULL_REQUEST_REPO: ${{ github.event.pull_request.head.repo.name }} 21 | PULL_REQUEST_TITLE : ${{ github.event.pull_request.title }} 22 | PULL_REQUEST_URL : ${{ github.event.pull_request.html_url }} 23 | uses: salesforcecli/github-workflows/.github/actions/prNotification@main 24 | -------------------------------------------------------------------------------- /.github/workflows/onRelease.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | release: 4 | # both release and prereleases 5 | types: [published] 6 | # support manual release in case something goes wrong and needs to be repeated or tested 7 | workflow_dispatch: 8 | inputs: 9 | tag: 10 | description: github tag that needs to publish 11 | type: string 12 | required: true 13 | jobs: 14 | getDistTag: 15 | outputs: 16 | tag: ${{ steps.distTag.outputs.tag }} 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | ref: ${{ github.event.release.tag_name || inputs.tag }} 22 | - uses: salesforcecli/github-workflows/.github/actions/getPreReleaseTag@main 23 | id: distTag 24 | npm: 25 | uses: salesforcecli/github-workflows/.github/workflows/npmPublish.yml@main 26 | needs: [getDistTag] 27 | with: 28 | ctc: true 29 | sign: true 30 | tag: ${{ needs.getDistTag.outputs.tag || 'latest' }} 31 | githubTag: ${{ github.event.release.tag_name || inputs.tag }} 32 | 33 | secrets: inherit 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches-ignore: [main] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | yarn-lockfile-check: 9 | uses: salesforcecli/github-workflows/.github/workflows/lockFileCheck.yml@main 10 | # Since the Windows unit tests take much longer, we run the linux unit tests first and then run the windows unit tests in parallel with NUTs 11 | linux-unit-tests: 12 | needs: yarn-lockfile-check 13 | uses: salesforcecli/github-workflows/.github/workflows/unitTestsLinux.yml@main 14 | windows-unit-tests: 15 | needs: linux-unit-tests 16 | uses: salesforcecli/github-workflows/.github/workflows/unitTestsWindows.yml@main 17 | nuts: 18 | needs: linux-unit-tests 19 | uses: salesforcecli/github-workflows/.github/workflows/nut.yml@main 20 | secrets: inherit 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, windows-latest] 24 | command: 25 | - 'yarn test:nuts:bulk:export' 26 | - 'yarn test:nuts:bulk:import' 27 | - 'yarn test:nuts:bulk:update' 28 | - 'yarn test:nuts:data:bulk-upsert-delete' 29 | - 'yarn test:nuts:data:create' 30 | - 'yarn test:nuts:data:query' 31 | - 'yarn test:nuts:data:record' 32 | - 'yarn test:nuts:data:search' 33 | - 'yarn test:nuts:data:tree' 34 | - 'yarn test:nuts:force:data:bulk-upsert-delete-status' 35 | fail-fast: false 36 | with: 37 | os: ${{ matrix.os }} 38 | command: ${{ matrix.command }} 39 | -------------------------------------------------------------------------------- /.github/workflows/validate-pr.yml: -------------------------------------------------------------------------------- 1 | name: pr-validation 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, edited] 6 | # only applies to PRs that want to merge to main 7 | branches: [main] 8 | 9 | jobs: 10 | pr-validation: 11 | uses: salesforcecli/github-workflows/.github/workflows/validatePR.yml@main 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -- CLEAN 2 | tmp/ 3 | # use yarn by default, so ignore npm 4 | package-lock.json 5 | 6 | # never checkin npm config 7 | .npmrc 8 | 9 | # debug logs 10 | npm-error.log 11 | yarn-error.log 12 | 13 | 14 | # compile source 15 | lib 16 | 17 | # test artifacts 18 | *xunit.xml 19 | *checkstyle.xml 20 | *unitcoverage 21 | .nyc_output 22 | coverage 23 | test_session* 24 | 25 | # generated docs 26 | docs 27 | 28 | # ignore sfdx-trust files 29 | *.tgz 30 | *.sig 31 | package.json.bak. 32 | 33 | 34 | npm-shrinkwrap.json 35 | oclif.manifest.json 36 | oclif.lock 37 | 38 | # -- CLEAN ALL 39 | *.tsbuildinfo 40 | .eslintcache 41 | .wireit 42 | node_modules 43 | 44 | # -- 45 | # put files here you don't want cleaned with sf-clean 46 | 47 | # os specific files 48 | .DS_Store 49 | .idea 50 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint && yarn pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn build && yarn test 5 | -------------------------------------------------------------------------------- /.images/vscodeScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforcecli/plugin-data/3315fc7af42db4c1883925ecf1e4573ed67aa226/.images/vscodeScreenshot.png -------------------------------------------------------------------------------- /.lintstagedrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '**/*.{js,json,md}?(x)': () => 'npm run reformat' 3 | }; 4 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "ts-node/register" 4 | ], 5 | "watch-extensions": "ts", 6 | "watch-files": [ 7 | "src", 8 | "test" 9 | ], 10 | "extensions": [ 11 | ".ts", 12 | "json" 13 | ], 14 | "recursive": true, 15 | "reporter": "spec", 16 | "timeout": 10000, 17 | "node-option": [ 18 | "loader=ts-node/esm" 19 | ] 20 | } -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "nyc": { 3 | "extends": "@salesforce/dev-config/nyc" 4 | } 5 | } -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@salesforce/prettier-config" 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach", 11 | "port": 9229, 12 | "skipFiles": ["/**"] 13 | }, 14 | { 15 | "name": "Run All Tests", 16 | "type": "node", 17 | "request": "launch", 18 | "protocol": "inspector", 19 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 20 | "args": ["--inspect", "--no-timeouts", "--colors", "test/**/*.test.ts"], 21 | "env": { 22 | "NODE_ENV": "development", 23 | "SFDX_ENV": "development" 24 | }, 25 | "sourceMaps": true, 26 | "smartStep": true, 27 | "internalConsoleOptions": "openOnSessionStart", 28 | "preLaunchTask": "Compile" 29 | }, 30 | { 31 | "type": "node", 32 | "request": "launch", 33 | "name": "Run Current Test", 34 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 35 | "args": ["--inspect", "--no-timeouts", "--colors", "${file}"], 36 | "env": { 37 | "NODE_ENV": "development", 38 | "SFDX_ENV": "development" 39 | }, 40 | "sourceMaps": true, 41 | "smartStep": true, 42 | "internalConsoleOptions": "openOnSessionStart", 43 | "preLaunchTask": "Compile" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true 8 | }, 9 | "search.exclude": { 10 | "**/lib": true, 11 | "**/bin": true 12 | }, 13 | "editor.tabSize": 2, 14 | "editor.formatOnSave": true, 15 | "rewrap.wrappingColumn": 80 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "problemMatcher": "$tsc-watch", 4 | "tasks": [ 5 | { 6 | "label": "Compile", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "command": "yarn", 12 | "type": "shell", 13 | "presentation": { 14 | "focus": false, 15 | "panel": "dedicated" 16 | }, 17 | "args": ["run", "prepack"], 18 | "isBackground": false, 19 | "problemMatcher": { 20 | "owner": "typescript", 21 | "fileLocation": "relative", 22 | "pattern": { 23 | "regexp": "^(.*\\.ts):(\\d*):(\\d*)(\\s*-\\s*)(error|warning|info)\\s*(TS\\d*):\\s*(.*)$", 24 | "file": 1, 25 | "line": 2, 26 | "column": 3, 27 | "severity": 5, 28 | "code": 6, 29 | "message": 7 30 | } 31 | } 32 | }, 33 | { 34 | "label": "Lint", 35 | "command": "yarn", 36 | "type": "shell", 37 | "presentation": { 38 | "focus": false, 39 | "panel": "dedicated" 40 | }, 41 | "args": ["run", "lint"], 42 | "isBackground": false, 43 | "problemMatcher": { 44 | "owner": "typescript", 45 | "fileLocation": "relative", 46 | "pattern": { 47 | "regexp": "^(ERROR|WARNING|INFO):\\s*(.*\\.ts):(\\d*):(\\d*)(\\s*-\\s*)(.*)$", 48 | "file": 2, 49 | "line": 3, 50 | "column": 4, 51 | "severity": 1, 52 | "message": 6 53 | } 54 | } 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Techical writers will be added as reviewers on markdown changes. 2 | *.md @salesforcecli/cli-docs 3 | 4 | # Comment line immediately above ownership line is reserved for related other information. Please be careful while editing. 5 | #ECCN:Open Source 5D002 -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This library limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. 8 | -------------------------------------------------------------------------------- /bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %* 4 | -------------------------------------------------------------------------------- /bin/dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node --loader ts-node/esm --no-warnings=ExperimentalWarning 2 | // eslint-disable-next-line node/shebang 3 | async function main() { 4 | const {execute} = await import('@oclif/core') 5 | await execute({development: true, dir: import.meta.url}) 6 | } 7 | 8 | await main() 9 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* -------------------------------------------------------------------------------- /bin/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // eslint-disable-next-line node/shebang 4 | async function main() { 5 | const {execute} = await import('@oclif/core') 6 | await execute({dir: import.meta.url}) 7 | } 8 | 9 | await main() 10 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /messages/batcher.md: -------------------------------------------------------------------------------- 1 | # BulkBatch 2 | 3 | Batch #%s 4 | 5 | # BulkError 6 | 7 | Upsert errors 8 | 9 | # BulkJobStatus 10 | 11 | Job Status 12 | 13 | # BatchStatus 14 | 15 | Batch Status 16 | 17 | # PollingInfo 18 | 19 | Will poll the batch statuses every %s seconds. 20 | To fetch the status on your own, press CTRL+C and use the command: 21 | sf force data bulk status -i %s -b %s 22 | 23 | # ExternalIdRequired 24 | 25 | An External ID is required on %s to perform an upsert. 26 | 27 | # TimeOut 28 | 29 | The operation timed out. Check the status with command: 30 | sf force data bulk status -i %s -b %s 31 | 32 | # CheckStatusCommand 33 | 34 | Check batch #%s’s status with the command: 35 | sf force data bulk status -i %s -b %s 36 | 37 | # BatchQueued 38 | 39 | Batch #%s queued (Batch ID: %s). 40 | -------------------------------------------------------------------------------- /messages/bulk.delete.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Bulk delete records from an org using a CSV file. Uses Bulk API 1.0. 4 | 5 | # description 6 | 7 | The CSV file must have only one column ("Id") and then the list of record IDs you want to delete, one ID per line. 8 | 9 | When you execute this command, it starts a job and one or more batches, displays their IDs, and then immediately returns control of the terminal to you by default. If you prefer to wait, set the --wait flag to the number of minutes; if it times out, the command outputs the IDs. Use the job and batch IDs to check the status of the job with the "<%= config.bin %> force data bulk status" command. A single job can contain many batches, depending on the length of the CSV file. 10 | 11 | # examples 12 | 13 | - Bulk delete Account records from your default org using the list of IDs in the "files/delete.csv" file: 14 | 15 | <%= config.bin %> <%= command.id %> --sobject Account --file files/delete.csv 16 | 17 | - Bulk delete records from a custom object in an org with alias my-scratch and wait 5 minutes for the command to complete: 18 | 19 | <%= config.bin %> <%= command.id %> --sobject MyObject__c --file files/delete.csv --wait 5 --target-org my-scratch 20 | 21 | # flags.sobject.summary 22 | 23 | API name of the Salesforce object, either standard or custom, that you want to delete records from. 24 | 25 | # flags.file.summary 26 | 27 | CSV file that contains the IDs of the records to delete. 28 | 29 | # flags.wait.summary 30 | 31 | Number of minutes to wait for the command to complete before displaying the results. 32 | -------------------------------------------------------------------------------- /messages/bulk.delete.resume.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Resume a bulk delete job that you previously started. Uses Bulk API 2.0. 4 | 5 | # description 6 | 7 | The command uses the job ID returned by the "<%= config.bin %> data delete bulk" command or the most recently-run bulk delete job. 8 | 9 | # examples 10 | 11 | - Resume a bulk delete job from your default org using an ID: 12 | 13 | <%= config.bin %> <%= command.id %> --job-id 750xx000000005sAAA 14 | 15 | - Resume the most recently run bulk delete job for an org with alias my-scratch: 16 | 17 | <%= config.bin %> <%= command.id %> --use-most-recent --target-org my-scratch 18 | -------------------------------------------------------------------------------- /messages/bulk.status.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | View the status of a bulk data load job or batch. Uses Bulk API 1.0. 4 | 5 | # description 6 | 7 | Run this command using the job ID or batch ID returned from the "<%= config.bin %> force data bulk delete" or "<%= config.bin %> force data bulk upsert" commands. 8 | 9 | # examples 10 | 11 | - View the status of a bulk load job in your default org: 12 | 13 | <%= config.bin %> <%= command.id %> --job-id 750xx000000005sAAA 14 | 15 | - View the status of a bulk load job and a specific batches in an org with alias my-scratch: 16 | 17 | <%= config.bin %> <%= command.id %> --job-id 750xx000000005sAAA --batch-id 751xx000000005nAAA --target-org my-scratch 18 | 19 | # flags.job-id.summary 20 | 21 | ID of the job whose status you want to view. 22 | 23 | # flags.batch-id.summary 24 | 25 | ID of the batch whose status you want to view; you must also specify the job ID. 26 | 27 | # NoBatchFound 28 | 29 | Unable to find batch %s for job %s. 30 | -------------------------------------------------------------------------------- /messages/bulk.upsert.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Bulk upsert records to an org from a CSV file. Uses Bulk API 1.0. 4 | 5 | # description 6 | 7 | An upsert refers to inserting a record into a Salesforce object if the record doesn't already exist, or updating it if it does exist. 8 | 9 | When you execute this command, it starts a job and one or more batches, displays their IDs, and then immediately returns control of the terminal to you by default. If you prefer to wait, set the --wait flag to the number of minutes; if it times out, the command outputs the IDs. Use the job and batch IDs to check the status of the job with the "<%= config.bin %> force data bulk status" command. A single job can contain many batches, depending on the length of the CSV file. 10 | 11 | See "Prepare CSV Files" in the Bulk API Developer Guide for details on formatting your CSV file. (https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/datafiles_csv_preparing.htm) 12 | 13 | By default, the job runs the batches in parallel, which we recommend. You can run jobs serially by specifying the --serial flag. But don't process data in serial mode unless you know this would otherwise result in lock timeouts and you can't reorganize your batches to avoid the locks. 14 | 15 | # examples 16 | 17 | - Bulk upsert records to the Contact object in your default org: 18 | 19 | <%= config.bin %> --sobject Contact --file files/contacts.csv --external-id Id 20 | 21 | - Bulk upsert records to a custom object in an org with alias my-scratch and wait 5 minutes for the command to complete: 22 | 23 | <%= config.bin %> <%= command.id %> --sobject MyObject__c --file files/file.csv --external-id MyField__c --wait 5 --target-org my-scratch 24 | 25 | # flags.sobject.summary 26 | 27 | API name of the Salesforce object, either standard or custom, that you want to upsert records to. 28 | 29 | # flags.file.summary 30 | 31 | CSV file that contains the records to upsert. 32 | 33 | # flags.external-id.summary 34 | 35 | Name of the external ID field, or the Id field. 36 | 37 | # flags.wait.summary 38 | 39 | Number of minutes to wait for the command to complete before displaying the results. 40 | 41 | # flags.serial.summary 42 | 43 | Run batches in serial mode. 44 | -------------------------------------------------------------------------------- /messages/bulk.upsert.resume.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Resume a bulk upsert job that you previously started. Uses Bulk API 2.0. 4 | 5 | # description 6 | 7 | The command uses the job ID returned from the "<%= config.bin %> data upsert bulk" command or the most recently-run bulk upsert job. 8 | 9 | # examples 10 | 11 | - Resume a bulk upsert job from your default org using an ID: 12 | 13 | <%= config.bin %> <%= command.id %> --job-id 750xx000000005sAAA 14 | 15 | - Resume the most recently run bulk upsert job for an org with alias my-scratch: 16 | 17 | <%= config.bin %> <%= command.id %> --use-most-recent --target-org my-scratch 18 | -------------------------------------------------------------------------------- /messages/bulkIngest.md: -------------------------------------------------------------------------------- 1 | # export.resume 2 | 3 | Run "sf %s --job-id %s" to resume the operation. 4 | 5 | # error.timeout 6 | 7 | The operation timed out after %s minutes. 8 | 9 | Run "sf %s --job-id %s" to resume it. 10 | 11 | # error.failedRecordDetails 12 | 13 | Job finished being processed but failed to process %s records. 14 | 15 | # error.failedRecordDetails.actions 16 | 17 | - Get the job results by running: "sf data bulk results -o %s --job-id %s". 18 | - View the job in the org: "sf org open -o %s --path '/lightning/setup/AsyncApiJobStatus/page?address=%2F%s'". 19 | 20 | # error.jobFailed 21 | 22 | Job failed to be processed due to: 23 | 24 | %s 25 | 26 | # error.jobFailed.actions 27 | 28 | - Get the job results by running: "sf data bulk results -o %s --job-id %s". 29 | - View the job in the org: "sf org open -o %s --path '/lightning/setup/AsyncApiJobStatus/page?address=%2F%s'". 30 | 31 | # error.jobAborted 32 | 33 | Job has been aborted. 34 | 35 | # error.jobAborted.actions 36 | 37 | - Get the job results by running: "sf data bulk results -o %s --job-id %s". 38 | - View the job in the org: "sf org open -o %s --path '/lightning/setup/AsyncApiJobStatus/page?address=%2F%s'". 39 | 40 | # error.hardDeletePermission 41 | 42 | You must have the "Bulk API Hard Delete" system permission to use the --hard-delete flag. This permission is disabled by default and can be enabled only by a system administrator. 43 | 44 | # error.noProcessedRecords 45 | 46 | Job finished successfully but it didn't process any record. 47 | 48 | # error.noProcessedRecords.actions 49 | 50 | - Check that the provided CSV file is valid. 51 | - View the job in the org: "sf org open -o %s --path '/lightning/setup/AsyncApiJobStatus/page?address=%2F%s'". 52 | 53 | # flags.column-delimiter.summary 54 | 55 | Column delimiter used in the CSV file. 56 | 57 | # flags.line-ending.summary 58 | 59 | Line ending used in the CSV file. Default value on Windows is `CRLF`; on macOS and Linux it's `LF`. 60 | 61 | # flags.sobject.summary 62 | 63 | API name of the Salesforce object, either standard or custom, that you want to update or delete records from. 64 | 65 | # flags.csvfile.summary 66 | 67 | CSV file that contains the IDs of the records to update or delete. 68 | 69 | # flags.wait.summary 70 | 71 | Number of minutes to wait for the command to complete before displaying the results. 72 | 73 | # flags.async.summary 74 | 75 | Run the command asynchronously. 76 | 77 | # flags.jobid 78 | 79 | ID of the job you want to resume. 80 | 81 | # flags.useMostRecent.summary 82 | 83 | Use the ID of the most recently-run bulk job. 84 | 85 | # flags.targetOrg.summary 86 | 87 | Username or alias of the target org. Not required if the "target-org" configuration variable is already set. 88 | 89 | # flags.wait.summary 90 | 91 | Number of minutes to wait for the command to complete before displaying the results. 92 | -------------------------------------------------------------------------------- /messages/bulkv2.delete.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Bulk delete records from an org using a CSV file. Uses Bulk API 2.0. 4 | 5 | # description 6 | 7 | The CSV file must have only one column ("Id") and then the list of record IDs you want to delete, one ID per line. 8 | 9 | When you execute this command, it starts a job, displays the ID, and then immediately returns control of the terminal to you by default. If you prefer to wait, set the --wait flag to the number of minutes; if it times out, the command outputs the IDs. Use the job ID to check the status of the job with the "<%= config.bin %> data delete resume" command. 10 | 11 | # examples 12 | 13 | - Bulk delete Account records from your default org using the list of IDs in the "files/delete.csv" file: 14 | 15 | <%= config.bin %> <%= command.id %> --sobject Account --file files/delete.csv 16 | 17 | - Bulk delete records from a custom object in an org with alias my-scratch and wait 5 minutes for the command to complete: 18 | 19 | <%= config.bin %> <%= command.id %> --sobject MyObject__c --file files/delete.csv --wait 5 --target-org my-scratch 20 | 21 | # flags.hard-delete.summary 22 | 23 | Mark the records as immediately eligible for deletion by your org. If you don't specify this flag, the deleted records go into the Recycle Bin. 24 | 25 | # flags.hard-delete.description 26 | 27 | You must have the "Bulk API Hard Delete" system permission to use this flag. The permission is disabled by default and can be enabled only by a system administrator. 28 | -------------------------------------------------------------------------------- /messages/bulkv2.upsert.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Bulk upsert records to an org from a CSV file. Uses Bulk API 2.0. 4 | 5 | # description 6 | 7 | An upsert refers to inserting a record into a Salesforce object if the record doesn't already exist, or updating it if it does exist. 8 | 9 | When you execute this command, it starts a job, displays the ID, and then immediately returns control of the terminal to you by default. If you prefer to wait, set the --wait flag to the number of minutes; if it times out, the command outputs the IDs. Use the job and batch IDs to check the status of the job with the "<%= config.bin %> data upsert resume" command. 10 | 11 | See "Prepare CSV Files" in the Bulk API Developer Guide for details on formatting your CSV file. (https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/datafiles_prepare_csv.htm) 12 | 13 | # examples 14 | 15 | - Bulk upsert records to the Contact object in your default org: 16 | 17 | <%= config.bin %> <%= command.id %> --sobject Contact --file files/contacts.csv --external-id Id 18 | 19 | - Bulk upsert records to a custom object in an org with alias my-scratch and wait 5 minutes for the command to complete: 20 | 21 | <%= config.bin %> <%= command.id %> --sobject MyObject__c --file files/file.csv --external-id MyField__c --wait 5 --target-org my-scratch 22 | 23 | # flags.external-id.summary 24 | 25 | Name of the external ID field, or the Id field. 26 | -------------------------------------------------------------------------------- /messages/data.bulk.results.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Get the results of a bulk ingest job that you previously ran. 4 | 5 | # description 6 | 7 | Use this command to get the complete results after running one of the CLI commands that uses Bulk API 2.0 to ingest (import, update, upsert, or delete) large datasets to your org, such as "data import bulk". The previously-run bulk command must have completed; if it's still processing, run the corresponding resume command first, such as "data import resume." Make note of the job ID of the previous bulk command because you use it to run this command. 8 | 9 | You can also use this command to get results from running a bulk ingest job with a different tool, such as Data Loader, as long as you have the job ID. For information on Data Loader, see https://developer.salesforce.com/docs/atlas.en-us.dataLoader.meta/dataLoader/data_loader_intro.htm. 10 | 11 | This command first displays the status of the previous bulk job, the operation that was executed in the org (such as insert or hard delete), and the updated Salesforce object. The command then displays how many records were processed in total, and how many were successful or failed. Finally, the output displays the names of the generated CSV-formatted files that contain the specific results for each ingested record. Depending on the success or failure of the bulk command, the results files can include the IDs of inserted records or the specific errors. When possible, if the ingest job failed or was aborted, you also get a CSV file with the unprocessed results. 12 | 13 | # flags.job-id.summary 14 | 15 | Job ID of the bulk job. 16 | 17 | # examples 18 | 19 | - Get results from a bulk ingest job; use the org with alias "my-scratch": 20 | 21 | <%= config.bin %> <%= command.id %> --job-id 7507i000fake341G --target-org my-scratch 22 | 23 | # error.jobInProgress 24 | 25 | Job hasn't finished being processed yet. 26 | 27 | # error.invalidId 28 | 29 | Can't find a bulk job with ID %s. 30 | 31 | # error.invalidId.actions 32 | 33 | - Ensure the ID is from a previously run bulk ingest job, and not a query job. 34 | 35 | - Run this command and verify that the job ID for the bulk command exists in your org: 36 | 37 | sf org open --target-org %s --path "/lightning/setup/AsyncApiJobStatus/page?address=%2F750" 38 | 39 | # error.noRecords 40 | 41 | Unable to get results because the job processed 0 records. 42 | -------------------------------------------------------------------------------- /messages/data.create.file.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Upload a local file to an org. 4 | 5 | # description 6 | 7 | This command always creates a new file in the org; you can't update an existing file. After a successful upload, the command displays the ID of the new ContentDocument record which represents the uploaded file. 8 | 9 | By default, the uploaded file isn't attached to a record; in the Salesforce UI the file shows up in the Files tab. You can optionally attach the file to an existing record, such as an account, as long as you know its record ID. 10 | 11 | You can also give the file a new name after it's been uploaded; by default its name in the org is the same as the local file name. 12 | 13 | # flags.title.summary 14 | 15 | New title given to the file (ContentDocument) after it's uploaded. 16 | 17 | # examples 18 | 19 | - Upload the local file "resources/astro.png" to your default org: 20 | 21 | <%= config.bin %> <%= command.id %> --file resources/astro.png 22 | 23 | - Give the file a different filename after it's uploaded to the org with alias "my-scratch": 24 | 25 | <%= config.bin %> <%= command.id %> --file resources/astro.png --title AstroOnABoat.png --target-org my-scratch 26 | 27 | - Attach the file to a record in the org: 28 | 29 | <%= config.bin %> <%= command.id %> --file path/to/astro.png --parent-id a03fakeLoJWPIA3 30 | 31 | # flags.file.summary 32 | 33 | Path of file to upload. 34 | 35 | # flags.parent-id.summary 36 | 37 | ID of the record to attach the file to. 38 | 39 | # createSuccess 40 | 41 | Created file with ContentDocumentId %s. 42 | 43 | # attachSuccess 44 | 45 | File attached to record with ID %s. 46 | 47 | # attachFailure 48 | 49 | The file was successfully uploaded, but we weren't able to attach it to the record. 50 | 51 | # insufficientAccessActions 52 | 53 | - Check that the record ID is correct and that you have access to it. 54 | -------------------------------------------------------------------------------- /messages/data.export.bulk.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Bulk export records from an org into a file using a SOQL query. Uses Bulk API 2.0. 4 | 5 | # description 6 | 7 | You can use this command to export millions of records from an org, either to migrate data or to back it up. 8 | 9 | Use a SOQL query to specify the fields of a standard or custom object that you want to export. Specify the SOQL query either at the command line with the --query flag or read it from a file with the --query-file flag; you can't specify both flags. The --output-file flag is required, which means you can only write the records to a file, in either CSV or JSON format. 10 | 11 | Bulk exports can take a while, depending on how many records are returned by the SOQL query. If the command times out, or you specified the --async flag, the command displays the job ID. To see the status and get the results of the job, run "sf data export resume" and pass the job ID to the --job-id flag. 12 | 13 | IMPORTANT: This command uses Bulk API 2.0, which limits the type of SOQL queries you can run. For example, you can't use aggregate functions such as count(). For the complete list of limitations, see the "SOQL Considerations" section in the "Bulk API 2.0 and Bulk API Developer Guide" (https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/queries.htm). 14 | 15 | # examples 16 | 17 | - Export the Id, Name, and Account.Name fields of the Contact object into a CSV-formatted file; if the export doesn't complete in 10 minutes, the command ends and displays a job ID. Use the org with alias "my-scratch": 18 | 19 | <%= config.bin %> <%= command.id %> --query "SELECT Id, Name, Account.Name FROM Contact" --output-file export-accounts.csv --wait 10 --target-org my-scratch 20 | 21 | - Similar to previous example, but use the default org, export the records into a JSON-formatted file, and include records that have been soft deleted: 22 | 23 | <%= config.bin %> <%= command.id %> --query "SELECT Id, Name, Account.Name FROM Contact" --output-file export-accounts.json --result-format json --wait 10 --all-rows 24 | 25 | - Export asynchronously; the command immediately returns a job ID that you then pass to the "sf data export resume" command: 26 | 27 | <%= config.bin %> <%= command.id %> --query "SELECT Id, Name, Account.Name FROM Contact" --output-file export-accounts.json --result-format json --async 28 | 29 | # flags.wait.summary 30 | 31 | Time to wait for the command to finish, in minutes. 32 | 33 | # flags.async.summary 34 | 35 | Don't wait for the job to complete. 36 | 37 | # flags.query.summary 38 | 39 | SOQL query to execute. 40 | 41 | # flags.all-rows.summary 42 | 43 | Include records that have been soft-deleted due to a merge or delete. By default, deleted records are not returned. 44 | 45 | # flags.output-file.summary 46 | 47 | File where records are written. 48 | 49 | # flags.result-format.summary 50 | 51 | Format to write the results. 52 | 53 | # flags.column-delimiter.summary 54 | 55 | Column delimiter to be used when writing CSV output. Default is COMMA. 56 | 57 | # flags.line-ending.summary 58 | 59 | Line ending to be used when writing CSV output. Default value on Windows is is `CRLF`; on macOS and Linux it's `LR`. 60 | 61 | # flags.query-file.summary 62 | 63 | File that contains the SOQL query. 64 | 65 | # export.timeout 66 | 67 | Run "sf data export resume --job-id %s" to get the latest status and results. 68 | -------------------------------------------------------------------------------- /messages/data.export.resume.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Resume a bulk export job that you previously started. Uses Bulk API 2.0. 4 | 5 | # description 6 | 7 | When the original "data export bulk" command either times out or is run with the --async flag, it displays a job ID. To see the status and get the results of the bulk export, run this command by either passing it the job ID or using the --use-most-recent flag to specify the most recent bulk export job. 8 | 9 | # flags.job-id.summary 10 | 11 | Job ID of the bulk export. 12 | 13 | # flags.use-most-recent.summary 14 | 15 | Use the job ID of the bulk export job that was most recently run. 16 | 17 | # examples 18 | 19 | - Resume a bulk export job run on your default org by specifying a job ID: 20 | 21 | sf <%= command.id %> --job-id 750xx000000005sAAA 22 | 23 | - Resume the most recently-run bulk export job for an org with alias my-scratch: 24 | 25 | sf data export resume --use-most-recent --target-org my-scratch 26 | -------------------------------------------------------------------------------- /messages/data.import.bulk.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Bulk import records into a Salesforce object from a CSV file. Uses Bulk API 2.0. 4 | 5 | # description 6 | 7 | You can use this command to import millions of records into the object from a file in comma-separated values (CSV) format. 8 | 9 | All the records in the CSV file must be for the same Salesforce object. Specify the object with the `--sobject` flag. 10 | 11 | Bulk imports can take a while, depending on how many records are in the CSV file. If the command times out, or you specified the --async flag, the command displays the job ID. To see the status and get the results of the job, run "sf data import resume" and pass the job ID to the --job-id flag. 12 | 13 | For information and examples about how to prepare your CSV files, see "Prepare Data to Ingest" in the "Bulk API 2.0 and Bulk API Developer Guide" (https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/datafiles_prepare_data.htm). 14 | 15 | # examples 16 | 17 | - Import Account records from a CSV-formatted file into an org with alias "my-scratch"; if the import doesn't complete in 10 minutes, the command ends and displays a job ID: 18 | 19 | <%= config.bin %> <%= command.id %> --file accounts.csv --sobject Account --wait 10 --target-org my-scratch 20 | 21 | - Import asynchronously and use the default org; the command immediately returns a job ID that you then pass to the "sf data import resume" command: 22 | 23 | <%= config.bin %> <%= command.id %> --file accounts.csv --sobject Account --async 24 | 25 | # flags.async.summary 26 | 27 | Don't wait for the command to complete. 28 | 29 | # flags.file.summary 30 | 31 | CSV file that contains the Salesforce object records you want to import. 32 | 33 | # flags.sobject.summary 34 | 35 | API name of the Salesforce object, either standard or custom, into which you're importing records. 36 | 37 | # flags.wait.summary 38 | 39 | Time to wait for the command to finish, in minutes. 40 | 41 | # flags.line-ending.summary 42 | 43 | Line ending used in the CSV file. Default value on Windows is `CRLF`; on macOS and Linux it's `LF`. 44 | -------------------------------------------------------------------------------- /messages/data.import.resume.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Resume a bulk import job that you previously started. Uses Bulk API 2.0. 4 | 5 | # description 6 | 7 | When the original "sf data import bulk" command either times out or is run with the --async flag, it displays a job ID. To see the status and get the results of the bulk import, run this command by either passing it the job ID or using the --use-most-recent flag to specify the most recent bulk import job. 8 | 9 | # examples 10 | 11 | - Resume a bulk import job to your default org using an ID: 12 | 13 | <%= config.bin %> <%= command.id %> --job-id 750xx000000005sAAA 14 | 15 | - Resume the most recently run bulk import job for an org with alias my-scratch: 16 | 17 | <%= config.bin %> <%= command.id %> --use-most-recent --target-org my-scratch 18 | 19 | # flags.use-most-recent.summary 20 | 21 | Use the job ID of the bulk import job that was most recently run. 22 | 23 | # flags.job-id.summary 24 | 25 | Job ID of the bulk import. 26 | 27 | # flags.wait.summary 28 | 29 | Time to wait for the command to finish, in minutes. 30 | -------------------------------------------------------------------------------- /messages/data.resume.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | View the status of a bulk data load job or batch. 4 | 5 | # description 6 | 7 | Run this command using the job ID or batch ID returned from the "<%= config.bin %> data delete bulk" or "<%= config.bin %> data upsert bulk" commands. 8 | 9 | # examples 10 | 11 | - View the status of a bulk load job: 12 | 13 | <%= config.bin %> <%= command.id %> --job-id 750xx000000005sAAA 14 | 15 | - View the status of a bulk load job and a specific batches: 16 | 17 | <%= config.bin %> <%= command.id %> --job-id 750xx000000005sAAA --batch-id 751xx000000005nAAA 18 | 19 | # flags.job-id.summary 20 | 21 | ID of the job whose status you want to view. 22 | 23 | # flags.batch-id.summary 24 | 25 | ID of the batch whose status you want to view; you must also specify the job ID. 26 | 27 | # NoBatchFound 28 | 29 | Unable to find batch %s for job %s. 30 | -------------------------------------------------------------------------------- /messages/data.search.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Execute a SOSL text-based search query. 4 | 5 | # description 6 | 7 | Specify the SOSL query at the command line with the --query flag or read the query from a file with the --file flag. 8 | 9 | By default, the results are written to the terminal in human-readable format. If you specify `--result-format csv`, the output is written to one or more CSV (comma-separated values) files. The file names correspond to the Salesforce objects in the results, such as Account.csv. Both `--result-format human` and `--result-format json` display only to the terminal. 10 | 11 | # examples 12 | 13 | - Specify a SOSL query at the command line; the command uses your default org: 14 | 15 | <%= config.bin %> <%= command.id %> --query "FIND {Anna Jones} IN Name Fields RETURNING Contact (Name, Phone)" 16 | 17 | - Read the SOSL query from a file called "query.txt"; the command uses the org with alias "my-scratch": 18 | 19 | <%= config.bin %> <%= command.id %> --file query.txt --target-org my-scratch 20 | 21 | - Similar to the previous example, but write the results to one or more CSV files, depending on the Salesforce objects in the results: 22 | 23 | <%= config.bin %> <%= command.id %> --file query.txt --target-org my-scratch --result-format csv 24 | 25 | # flags.query.summary 26 | 27 | SOSL query to execute. 28 | 29 | # flags.result-format.summary 30 | 31 | Format to display the results, or to write to disk if you specify "csv". 32 | 33 | # flags.file.summary 34 | 35 | File that contains the SOSL query. 36 | 37 | # displayQueryRecordsRetrieved 38 | 39 | Total number of records retrieved: %s. 40 | 41 | # queryRunningMessage 42 | 43 | Querying Data 44 | -------------------------------------------------------------------------------- /messages/data.update.bulk.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Bulk update records to an org from a CSV file. Uses Bulk API 2.0. 4 | 5 | # description 6 | 7 | You can use this command to update millions of Salesforce object records based on a file in comma-separated values (CSV) format. 8 | 9 | All the records in the CSV file must be for the same Salesforce object. Specify the object with the `--sobject` flag. The first column of every line in the CSV file must be an ID of the record you want to update. The CSV file can contain only existing records; if a record in the file doesn't currently exist in the Salesforce object, the command fails. Consider using "sf data upsert bulk" if you also want to insert new records. 10 | 11 | Bulk updates can take a while, depending on how many records are in the CSV file. If the command times out, or you specified the --async flag, the command displays the job ID. To see the status and get the results of the job, run "sf data update resume" and pass the job ID to the --job-id flag. 12 | 13 | For information and examples about how to prepare your CSV files, see "Prepare Data to Ingest" in the "Bulk API 2.0 and Bulk API Developer Guide" (https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/datafiles_prepare_data.htm). 14 | 15 | # examples 16 | 17 | - Update Account records from a CSV-formatted file into an org with alias "my-scratch"; if the update doesn't complete in 10 minutes, the command ends and displays a job ID: 18 | 19 | <%= config.bin %> <%= command.id %> --file accounts.csv --sobject Account --wait 10 --target-org my-scratch 20 | 21 | - Update asynchronously and use the default org; the command immediately returns a job ID that you then pass to the "sf data update resume" command: 22 | 23 | <%= config.bin %> <%= command.id %> --file accounts.csv --sobject Account --async 24 | 25 | # flags.async.summary 26 | 27 | Don't wait for the command to complete. 28 | 29 | # flags.wait.summary 30 | 31 | Time to wait for the command to finish, in minutes. 32 | 33 | # flags.file.summary 34 | 35 | CSV file that contains the Salesforce object records you want to update. 36 | 37 | # flags.sobject.summary 38 | 39 | API name of the Salesforce object, either standard or custom, which you are updating. 40 | -------------------------------------------------------------------------------- /messages/data.update.resume.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Resume a bulk update job that you previously started. Uses Bulk API 2.0. 4 | 5 | # description 6 | 7 | When the original "sf data update bulk" command either times out or is run with the --async flag, it displays a job ID. To see the status and get the results of the bulk update, run this command by either passing it the job ID or using the --use-most-recent flag to specify the most recent bulk update job. 8 | 9 | # examples 10 | 11 | - Resume a bulk update job of your default org using a job ID: 12 | 13 | <%= config.bin %> <%= command.id %> --job-id 750xx000000005sAAA 14 | 15 | - Resume the most recently run bulk update job for an org with alias "my-scratch": 16 | 17 | <%= config.bin %> <%= command.id %> --use-most-recent --target-org my-scratch 18 | 19 | # flags.use-most-recent.summary 20 | 21 | Use the job ID of the bulk update job that was most recently run. 22 | 23 | # flags.job-id.summary 24 | 25 | Job ID of the bulk update. 26 | 27 | # flags.wait.summary 28 | 29 | Time to wait for the command to finish, in minutes. 30 | -------------------------------------------------------------------------------- /messages/exportApi.md: -------------------------------------------------------------------------------- 1 | # queryNotProvided 2 | 3 | Provide a SOQL query or a file that contains a SOQL query. 4 | 5 | # soqlInvalid 6 | 7 | Invalid SOQL query: %s 8 | 9 | # soqlMalformed 10 | 11 | The provided SOQL query is malformed: %s 12 | 13 | # soqlMalformedAction 14 | 15 | Check the SOQL query syntax and try again. 16 | 17 | # dataExportRecordCount 18 | 19 | Processed %s records from query: %s 20 | 21 | # dataExportRecordCountWarning 22 | 23 | Query returned more than 200 records. Run the command using the --plan flag instead. 24 | Record Count: %s 25 | Query: %s 26 | 27 | # noRecordsReturned 28 | 29 | The query for %s returned 0 records. 30 | -------------------------------------------------------------------------------- /messages/importApi.md: -------------------------------------------------------------------------------- 1 | # dataFileEmpty 2 | 3 | Data file is empty: %s. 4 | 5 | # dataImportFailed 6 | 7 | Import failed from file: %s. Results: %s. 8 | 9 | # FlsError 10 | 11 | We couldn't process your request because you don't have access to %s on %s. To learn more about field-level security, visit Tips and Hints for Page Layouts and Field-Level Security in our Developer Documentation. 12 | 13 | # error.InvalidDataImport 14 | 15 | Data plan file %s did not validate against the schema. Errors: %s. 16 | 17 | # error.InvalidDataImport.actions 18 | 19 | - Did you run the "sf data export tree" command with the --plan flag? 20 | 21 | - Make sure you're importing a plan definition file. 22 | 23 | - Get help with the import plan schema by running "sf data import beta tree --help". 24 | 25 | # error.NonStringFiles 26 | 27 | The `files` property of the plan objects must contain only strings 28 | 29 | # error.UnresolvableRefs 30 | 31 | There are references in a data file %s that can't be resolved: 32 | 33 | %s 34 | 35 | # error.RefsInFiles 36 | 37 | The file %s includes references (ex: '@AccountRef1'). Those are only supported with --plan, not --files.` 38 | 39 | # error.noRecordTypeName 40 | 41 | This file contains an unresolvable RecordType ID. Try exporting the data by specifying RecordType.Name in the SOQL query, and then run the data import again. 42 | -------------------------------------------------------------------------------- /messages/messages.md: -------------------------------------------------------------------------------- 1 | # perfLogLevelOption 2 | 3 | Get API performance data. 4 | 5 | # perfLogLevelOptionLong 6 | 7 | Gets data on API performance metrics from the server. The data is stored in $HOME/.sfdx/apiPerformanceLog.json. 8 | 9 | # DataRecordGetNoRecord 10 | 11 | No matching record found. 12 | 13 | # DataRecordGetMultipleRecords 14 | 15 | %s is not a unique qualifier for %s; %s records were retrieved. 16 | Retrieve only one record by making your --where clause more specific. 17 | 18 | # TextUtilMalformedKeyValuePair 19 | 20 | Malformed key=value pair for value: %s. 21 | 22 | # flags.resultFormat.summary 23 | 24 | Format to display the results; the --json flag overrides this flag. 25 | 26 | # error.bulkRequestIdNotFound 27 | 28 | Could not find a cache entry for job ID %s. 29 | 30 | # error.missingCacheEntryError 31 | 32 | Could not load a most recent cache entry for a bulk request. Please rerun your command with a bulk request id. 33 | 34 | # error.skipCacheValidateNoOrg 35 | 36 | A default target org for the job %s is required to be set because the job isn't in the local cache. 37 | 38 | # usernameRequired 39 | 40 | A valid username is required when creating a cache entry. 41 | 42 | # invalidSobject 43 | 44 | The supplied SObject type "%s" is invalid. Error message: %s. 45 | -------------------------------------------------------------------------------- /messages/record.create.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Create and insert a record into a Salesforce or Tooling API object. 4 | 5 | # description 6 | 7 | You must specify a value for all required fields of the object. 8 | 9 | When specifying fields, use the format =. Enclose all field-value pairs in one set of double quotation marks, delimited by spaces. Enclose values that contain spaces in single quotes. 10 | 11 | This command inserts a record into Salesforce objects by default. Use the --use-tooling-api flag to insert into a Tooling API object. 12 | 13 | # flags.sobject.summary 14 | 15 | API name of the Salesforce or Tooling API object that you're inserting a record into. 16 | 17 | # flags.values.summary 18 | 19 | Values for the flags in the form =, separate multiple pairs with spaces. 20 | 21 | # flags.use-tooling-api.summary 22 | 23 | Use Tooling API so you can insert a record in a Tooling API object. 24 | 25 | # examples 26 | 27 | - Insert a record into the Account object of your default org; only the required Name field has a value: 28 | 29 | <%= config.bin %> <%= command.id %> --sobject Account --values "Name=Acme" 30 | 31 | - Insert an Account record with values for two fields, one value contains a space; the command uses the org with alias "my-scratch": 32 | 33 | <%= config.bin %> <%= command.id %> --sobject Account --values "Name='Universal Containers' Website=www.example.com" --target-org my-scratch 34 | 35 | - Insert a record into the Tooling API object TraceFlag: 36 | 37 | <%= config.bin %> <%= command.id %> --use-tooling-api --sobject TraceFlag --values "DebugLevelId=7dl170000008U36AAE StartDate=2022-12-15T00:26:04.000+0000 ExpirationDate=2022-12-15T00:56:04.000+0000 LogType=CLASS_TRACING TracedEntityId=01p17000000R6bLAAS" 38 | 39 | # createSuccess 40 | 41 | Successfully created record: %s. 42 | 43 | # createFailure 44 | 45 | Failed to create record. %s 46 | -------------------------------------------------------------------------------- /messages/record.delete.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Deletes a single record from a Salesforce or Tooling API object. 4 | 5 | # description 6 | 7 | Specify the record you want to delete with either its ID or with a list of field-value pairs that identify the record. If your list of fields identifies more than one record, the delete fails; the error displays how many records were found. 8 | 9 | When specifying field-value pairs, use the format =. Enclose all field-value pairs in one set of double quotation marks, delimited by spaces. Enclose values that contain spaces in single quotes. 10 | 11 | This command deletes a record from Salesforce objects by default. Use the --use-tooling-api flag to delete from a Tooling API object. 12 | 13 | # flags.sobject.summary 14 | 15 | API name of the Salesforce or Tooling API object that you're deleting a record from. 16 | 17 | # flags.record-id.summary 18 | 19 | ID of the record you’re deleting. 20 | 21 | # flags.where.summary 22 | 23 | List of = pairs that identify the record you want to delete. 24 | 25 | # flags.use-tooling-api.summary 26 | 27 | Use Tooling API so you can delete a record from a Tooling API object. 28 | 29 | # examples 30 | 31 | - Delete a record from Account with the specified (truncated) ID: 32 | 33 | <%= config.bin %> <%= command.id %> --sobject Account --record-id 00180XX 34 | 35 | - Delete a record from Account whose name equals "Acme": 36 | 37 | <%= config.bin %> <%= command.id %> --sobject Account --where "Name=Acme" 38 | 39 | - Delete a record from Account identified with two field values, one that contains a space; the command uses the org with alias "my-scratch": 40 | 41 | <%= config.bin %> <%= command.id %> --sobject Account --where "Name='Universal Containers' Phone='(123) 456-7890'" --target-org myscratch 42 | 43 | - Delete a record from the Tooling API object TraceFlag with the specified (truncated) ID: 44 | 45 | <%= config.bin %> <%= command.id %> --use-tooling-api --sobject TraceFlag --record-id 7tf8c 46 | 47 | # deleteSuccess 48 | 49 | Successfully deleted record: %s. 50 | 51 | # deleteFailure 52 | 53 | Failed to delete record. %s 54 | -------------------------------------------------------------------------------- /messages/record.get.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Retrieve and display a single record of a Salesforce or Tooling API object. 4 | 5 | # description 6 | 7 | Specify the record you want to retrieve with either its ID or with a list of field-value pairs that identify the record. If your list of fields identifies more than one record, the command fails; the error displays how many records were found. 8 | 9 | When specifying field-value pairs, use the format =. Enclose all field-value pairs in one set of double quotation marks, delimited by spaces. Enclose values that contain spaces in single quotes. 10 | 11 | The command displays all the record's fields and their values, one field per terminal line. Fields with no values are displayed as "null". 12 | 13 | This command retrieves a record from Salesforce objects by default. Use the --use-tooling-api flag to retrieve from a Tooling API object. 14 | 15 | # flags.sobject.summary 16 | 17 | API name of the Salesforce or Tooling API object that you're retrieving a record from. 18 | 19 | # flags.record-id.summary 20 | 21 | ID of the record you’re retrieving. 22 | 23 | # flags.where.summary 24 | 25 | List of = pairs that identify the record you want to display. 26 | 27 | # flags.use-tooling-api.summary 28 | 29 | Use Tooling API so you can retrieve a record from a Tooling API object. 30 | 31 | # examples 32 | 33 | - Retrieve and display a record from Account with the specified (truncated) ID: 34 | 35 | <%= config.bin %> <%= command.id %> --sobject Account --record-id 00180XX 36 | 37 | - Retrieve a record from Account whose name equals "Acme": 38 | 39 | <%= config.bin %> <%= command.id %> --sobject Account --where "Name=Acme" 40 | 41 | - Retrieve a record from Account identified with two field values, one that contains a space; the command uses the org with alias "my-scratch": 42 | 43 | <%= config.bin %> <%= command.id %> --sobject Account --where "Name='Universal Containers' Phone='(123) 456-7890'" --target-org myscratch 44 | 45 | - Retrieve a record from the Tooling API object TraceFlag with the specified (truncated) ID: 46 | 47 | <%= config.bin %> <%= command.id %> --use-tooling-api --sobject TraceFlag --record-id 7tf8c 48 | -------------------------------------------------------------------------------- /messages/record.update.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Updates a single record of a Salesforce or Tooling API object. 4 | 5 | # description 6 | 7 | Specify the record you want to update with either its ID or with a list of field-value pairs that identify the record. If your list of fields identifies more than one record, the update fails; the error displays how many records were found. 8 | 9 | When using field-value pairs for both identifying the record and specifiyng the new field values, use the format =. Enclose all field-value pairs in one set of double quotation marks, delimited by spaces. Enclose values that contain spaces in single quotes. 10 | 11 | This command updates a record in Salesforce objects by default. Use the --use-tooling-api flag to update a Tooling API object. 12 | 13 | # flags.sobject.summary 14 | 15 | API name of the Salesforce or Tooling API object that contains the record you're updating. 16 | 17 | # flags.record-id.summary 18 | 19 | ID of the record you’re updating. 20 | 21 | # flags.where.summary 22 | 23 | List of = pairs that identify the record you want to update. 24 | 25 | # flags.use-tooling-api.summary 26 | 27 | Use Tooling API so you can update a record in a Tooling API object. 28 | 29 | # flags.values.summary 30 | 31 | Fields that you're updating, in the format of = pairs. 32 | 33 | # examples 34 | 35 | - Update the Name field of an Account record with the specified (truncated) ID: 36 | 37 | <%= config.bin %> <%= command.id %> --sobject Account --record-id 001D0 --values "Name=NewAcme" 38 | 39 | - Update the Name field of an Account record whose current name is 'Old Acme': 40 | 41 | <%= config.bin %> <%= command.id %> --sobject Account --where "Name='Old Acme'" --values "Name='New Acme'" 42 | 43 | - Update the Name and Website fields of an Account record with the specified (truncated) ID: 44 | 45 | <%= config.bin %> <%= command.id %> --sobject Account --record-id 001D0 --values "Name='Acme III' Website=www.example.com" 46 | 47 | - Update the ExpirationDate field of a record of the Tooling API object TraceFlag using the specified (truncated) ID: 48 | 49 | <%= config.bin %> <%= command.id %> -t --sobject TraceFlag --record-id 7tf170000009cUBAAY --values "ExpirationDate=2017-12-01T00:58:04.000+0000" 50 | 51 | # updateSuccess 52 | 53 | Successfully updated record: %s. 54 | 55 | # updateFailure 56 | 57 | Failed to update record. %s 58 | 59 | # updateFailureWithFields 60 | 61 | Failed to update record with code %s. Message: %s. Fields: %s. 62 | -------------------------------------------------------------------------------- /messages/soql.query.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Execute a SOQL query. 4 | 5 | # description 6 | 7 | Specify the SOQL query at the command line with the --query flag or read the query from a file with the --file flag. 8 | 9 | If your query returns more than 10,000 records, prefer to use the `sf data export bulk` command instead. It runs the query using Bulk API 2.0, which has higher limits than the default API used by the command. 10 | 11 | # examples 12 | 13 | - Specify a SOQL query at the command line; the command uses your default org: 14 | 15 | <%= config.bin %> <%= command.id %> --query "SELECT Id, Name, Account.Name FROM Contact" 16 | 17 | - Read the SOQL query from a file called "query.txt" and write the CSV-formatted output to a file; the command uses the org with alias "my-scratch": 18 | 19 | <%= config.bin %> <%= command.id %> --file query.txt --output-file output.csv --result-format csv --target-org my-scratch 20 | 21 | - Use Tooling API to run a query on the ApexTrigger Tooling API object: 22 | 23 | <%= config.bin %> <%= command.id %> --query "SELECT Name FROM ApexTrigger" --use-tooling-api 24 | 25 | # flags.query.summary 26 | 27 | SOQL query to execute. 28 | 29 | # flags.use-tooling-api.summary 30 | 31 | Use Tooling API so you can run queries on Tooling API objects. 32 | 33 | # flags.file.summary 34 | 35 | File that contains the SOQL query. 36 | 37 | # flags.all-rows.summary 38 | 39 | Include deleted records. By default, deleted records are not returned. 40 | 41 | # flags.output-file.summary 42 | 43 | File where records are written; only CSV and JSON output formats are supported. 44 | 45 | # displayQueryRecordsRetrieved 46 | 47 | Total number of records retrieved: %s. 48 | 49 | # queryRunningMessage 50 | 51 | Querying Data 52 | -------------------------------------------------------------------------------- /messages/tree.export.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Export data from an org into one or more JSON files. 4 | 5 | # description 6 | 7 | Specify a SOQL query, either directly at the command line or read from a file, to retrieve the data you want to export. The exported data is written to JSON files in sObject tree format, which is a collection of nested, parent-child records with a single root record. Use these JSON files to import data into an org with the "<%= config.bin %> data import tree" command. 8 | 9 | If your SOQL query references multiple objects, the command generates a single JSON file by default. You can specify the --plan flag to generate separate JSON files for each object and a plan definition file that aggregates them. You then specify just this plan definition file when you import the data into an org. 10 | 11 | The SOQL query can return a maximum of 2,000 records. For more information, see the REST API Developer Guide. (https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_sobject_tree.htm). 12 | 13 | # flags.query.summary 14 | 15 | SOQL query, or filepath of a file that contains the query, to retrieve records. 16 | 17 | # flags.plan.summary 18 | 19 | Generate multiple sObject tree files and a plan definition file for aggregated import. 20 | 21 | # flags.prefix.summary 22 | 23 | Prefix of generated files. 24 | 25 | # flags.output-dir.summary 26 | 27 | Directory in which to generate the JSON files; default is current directory. 28 | 29 | # examples 30 | 31 | - Export records retrieved with the specified SOQL query into a single JSON file in the current directory; the command uses your default org: 32 | 33 | <%= config.bin %> <%= command.id %> --query "SELECT Id, Name, (SELECT Name, Address__c FROM Properties__r) FROM Broker__c" 34 | 35 | - Export data using a SOQL query in the "query.txt" file and generate JSON files for each object and a plan that aggregates them: 36 | 37 | <%= config.bin %> <%= command.id %> --query query.txt --plan 38 | 39 | - Prepend "export-demo" before each generated file and generate the files in the "export-out" directory; run the command on the org with alias "my-scratch": 40 | 41 | <%= config.bin %> <%= command.id %> --query query.txt --plan --prefix export-demo --output-dir export-out --target-org my-scratch 42 | 43 | # PrefixSlashError 44 | 45 | `--prefix` cannot contain a forward slash or backslash. 46 | -------------------------------------------------------------------------------- /messages/tree.import.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Import data from one or more JSON files into an org. 4 | 5 | # description 6 | 7 | The JSON files that contain the data are in sObject tree format, which is a collection of nested, parent-child records with a single root record. Use the "<%= config.bin %> data export tree" command to generate these JSON files. 8 | 9 | If you used the --plan flag when exporting the data to generate a plan definition file, use the --plan flag to reference the file when you import. If you're not using a plan, use the --files flag to list the files. If you specify multiple JSON files that depend on each other in a parent-child relationship, be sure you list them in the correct order. 10 | 11 | # flags.files.summary 12 | 13 | Comma-separated and in-order JSON files that contain the records, in sObject tree format, that you want to insert. 14 | 15 | # flag.files.description 16 | 17 | Each file can contain up to 200 total records. For more information, see the REST API Developer Guide. (https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_sobject_tree.htm) 18 | 19 | # flags.plan.summary 20 | 21 | Plan definition file to insert multiple data files. 22 | 23 | # flags.plan.description 24 | 25 | Unlike when you use the `--files` flag, the files listed in the plan definition file **can** contain more then 200 records. When the CLI executes the import, it automatically batches the records to comply with the 200 record limit set by the API. 26 | 27 | The order in which you list the files in the plan definition file matters. Specifically, records with lookups to records in another file should be listed AFTER that file. For example, let's say you're loading Account and Contact records, and the contacts have references to those accounts. Be sure you list the Accounts file before the Contacts file. 28 | 29 | The plan definition file has the following schema: 30 | 31 | - items(object) - SObject Type: Definition of records to be insert per SObject Type 32 | - sobject(string) - Name of SObject: Child file references must have SObject roots of this type 33 | - files(array) - Files: An array of files paths to load 34 | 35 | # examples 36 | 37 | - Import the records contained in two JSON files into the org with alias "my-scratch": 38 | 39 | <%= config.bin %> <%= command.id %> --files Contact.json,Account.json --target-org my-scratch 40 | 41 | - Import records using a plan definition file into your default org: 42 | 43 | <%= config.bin %> <%= command.id %> --plan Account-Contact-plan.json 44 | -------------------------------------------------------------------------------- /schema/dataImportPlanSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$comment": "Copyright (c) 2016, salesforce.com, inc. All rights reserved. Licensed under the BSD 3-Clause license. For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "type": "array", 5 | "title": "Data Import Plan", 6 | "description": "Schema for data import plan JSON.", 7 | "items": { 8 | "type": "object", 9 | "title": "SObject Type", 10 | "description": "Definition of records to be insert per SObject Type", 11 | "properties": { 12 | "sobject": { 13 | "type": "string", 14 | "title": "Name of SObject", 15 | "description": "Child file references must have SObject roots of this type" 16 | }, 17 | "saveRefs": { 18 | "type": "boolean", 19 | "title": "Save References", 20 | "description": "Post-save, save references (Name/ID) to be used for reference replacement in subsequent saves. Applies to all data files for this SObject type.", 21 | "default": false 22 | }, 23 | "resolveRefs": { 24 | "type": "boolean", 25 | "title": "Resolve References", 26 | "description": "Pre-save, replace @ with ID from previous save. Applies to all data files for this SObject type.", 27 | "default": false 28 | }, 29 | "files": { 30 | "type": "array", 31 | "title": "Files", 32 | "description": "An array of files paths to load", 33 | "items": { 34 | "title": "Filepath.", 35 | "description": "Filepath string or object to point to a JSON or XML file having data defined in SObject Tree format.", 36 | "oneOf": [ 37 | { 38 | "type": "string" 39 | }, 40 | { 41 | "type": "object", 42 | "properties": { 43 | "file": { 44 | "type": "string", 45 | "title": "Filepath schema", 46 | "description": "Filepath to JSON or XML file having data defined in SObject Tree format" 47 | }, 48 | "contentType": { 49 | "title": "Filepath schema.", 50 | "description": "If data file extension is not .json or .xml, provide content type.", 51 | "enum": ["application/json", "application/xml"] 52 | }, 53 | "saveRefs": { 54 | "type": "boolean", 55 | "title": "Save References", 56 | "description": "Post-save, save references (Name/ID) to be used for reference replacement in subsequent saves. Overrides SObject-level 'saveRefs' setting." 57 | }, 58 | "resolveRefs": { 59 | "type": "boolean", 60 | "title": "Resolve References", 61 | "description": "Pre-save, replace @ with ID from previous save. Overrides SObject-level 'replaceRefs' setting." 62 | } 63 | }, 64 | "required": ["file"] 65 | } 66 | ] 67 | } 68 | } 69 | }, 70 | "required": ["sobject", "files"] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/api/data/tree/functions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import type { SObjectTreeInput } from '../../../types.js'; 9 | 10 | /** This is the format for references created by the export command */ 11 | const genericRefRegex = new RegExp('^@\\w+Ref\\d+$'); 12 | 13 | export const isUnresolvedRef = (v: unknown): boolean => typeof v === 'string' && genericRefRegex.test(v); 14 | 15 | /** at least record in the array has at least one property value that matches the regex */ 16 | export const hasUnresolvedRefs = (records: SObjectTreeInput[]): boolean => 17 | records.some((r) => Object.values(r).some(isUnresolvedRef)); 18 | -------------------------------------------------------------------------------- /src/api/data/tree/importTypes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import type { Dictionary } from '@salesforce/ts-types'; 8 | import type { DataPlanPart } from '../../../types.js'; 9 | 10 | /* 11 | * Copyright (c) 2023, salesforce.com, inc. 12 | * All rights reserved. 13 | * Licensed under the BSD 3-Clause license. 14 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 15 | */ 16 | export type TreeResponse = TreeResponseSuccess | TreeResponseError; 17 | 18 | type TreeResponseSuccess = { 19 | hasErrors: false; 20 | results: Array<{ 21 | referenceId: string; 22 | id: string; 23 | }>; 24 | }; 25 | 26 | type TreeResponseError = { 27 | hasErrors: true; 28 | results: Array<{ 29 | referenceId: string; 30 | errors: Array<{ 31 | statusCode: string; 32 | message: string; 33 | fields: string[]; 34 | }>; 35 | }>; 36 | }; 37 | 38 | export type ResponseRefs = { 39 | referenceId: string; 40 | id: string; 41 | }; 42 | export type ImportResults = { 43 | responseRefs?: ResponseRefs[]; 44 | sobjectTypes?: Dictionary; 45 | errors?: string[]; 46 | }; 47 | 48 | export type ImportResult = { 49 | refId: string; 50 | type: string; 51 | id: string; 52 | }; /** like the original DataPlanPart but without the non-string options inside files */ 53 | 54 | export type DataPlanPartFilesOnly = { 55 | sobject: string; 56 | files: string[]; 57 | saveRefs: boolean; 58 | resolveRefs: boolean; 59 | } & Partial; 60 | -------------------------------------------------------------------------------- /src/api/file/fileToContentVersion.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { readFile } from 'node:fs/promises'; 9 | import { basename } from 'node:path'; 10 | import { Connection } from '@salesforce/core'; 11 | import type { Record, SaveResult } from '@jsforce/jsforce-node'; 12 | import FormData from 'form-data'; 13 | 14 | export type ContentVersion = { 15 | Title: string; 16 | FileExtension: string; 17 | VersionData: string; 18 | /** this could be undefined outside of our narrow use case (created files) */ 19 | ContentDocumentId: string; 20 | } & Record; 21 | 22 | type ContentVersionCreateRequest = { 23 | PathOnClient: string; 24 | Title?: string; 25 | }; 26 | 27 | export async function file2CV(conn: Connection, filepath: string, title?: string): Promise { 28 | const req: ContentVersionCreateRequest = { 29 | PathOnClient: filepath, 30 | Title: title, 31 | }; 32 | 33 | const form = new FormData(); 34 | form.append('VersionData', await readFile(filepath), { filename: title ?? basename(filepath) }); 35 | form.append('entity_content', JSON.stringify(req), { contentType: 'application/json' }); 36 | 37 | // POST the multipart form to Salesforce's API, can't use the normal "create" action because it doesn't support multipart 38 | const CV = await conn.request({ 39 | url: '/sobjects/ContentVersion', 40 | headers: { ...form.getHeaders() }, 41 | body: form.getBuffer(), 42 | method: 'POST', 43 | }); 44 | 45 | if (!CV.success) { 46 | throw new Error(`Failed to create ContentVersion: ${CV.errors.map((e) => JSON.stringify(e, null, 2)).join('\n')}`); 47 | } 48 | 49 | return conn.singleRecordQuery( 50 | `Select Id, ContentDocumentId, Title, FileExtension from ContentVersion where Id='${CV.id}'` 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/data/create/file.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; 9 | import { Messages, SfError } from '@salesforce/core'; 10 | import { ContentVersion, file2CV } from '../../../api/file/fileToContentVersion.js'; 11 | 12 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 13 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'data.create.file'); 14 | 15 | type CDLCreateRequest = { 16 | ContentDocumentId: string; 17 | LinkedEntityId: string; 18 | ShareType: string; 19 | }; 20 | 21 | export default class DataCreateFile extends SfCommand { 22 | public static readonly summary = messages.getMessage('summary'); 23 | public static readonly description = messages.getMessage('description'); 24 | public static readonly examples = messages.getMessages('examples'); 25 | 26 | public static readonly flags = { 27 | 'target-org': Flags.requiredOrg(), 28 | 'api-version': Flags.orgApiVersion(), 29 | title: Flags.string({ 30 | summary: messages.getMessage('flags.title.summary'), 31 | char: 't', 32 | required: false, 33 | }), 34 | file: Flags.file({ 35 | summary: messages.getMessage('flags.file.summary'), 36 | char: 'f', 37 | required: true, 38 | exists: true, 39 | }), 40 | // it really could be most any valid ID 41 | // eslint-disable-next-line sf-plugin/id-flag-suggestions 42 | 'parent-id': Flags.salesforceId({ 43 | summary: messages.getMessage('flags.parent-id.summary'), 44 | char: 'i', 45 | length: 'both', 46 | }), 47 | }; 48 | 49 | public async run(): Promise { 50 | const { flags } = await this.parse(DataCreateFile); 51 | const conn = flags['target-org'].getConnection(flags['api-version']); 52 | const cv = await file2CV(conn, flags.file, flags.title); 53 | this.logSuccess(messages.getMessage('createSuccess', [cv.ContentDocumentId])); 54 | 55 | if (!flags['parent-id']) { 56 | return cv; 57 | } 58 | 59 | const CDLReq = { 60 | ContentDocumentId: cv.ContentDocumentId, 61 | LinkedEntityId: flags['parent-id'], 62 | ShareType: 'V', 63 | } satisfies CDLCreateRequest; 64 | try { 65 | const CDLCreateResult = await conn.sobject('ContentDocumentLink').create(CDLReq); 66 | 67 | if (CDLCreateResult.success) { 68 | this.logSuccess(messages.getMessage('attachSuccess', [flags['parent-id']])); 69 | return cv; 70 | } else { 71 | throw SfError.create({ 72 | message: messages.getMessage('attachFailure'), 73 | data: CDLCreateResult.errors, 74 | }); 75 | } 76 | } catch (e) { 77 | const error = SfError.wrap(e); 78 | if (error.name === 'INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY') { 79 | error.actions = messages.getMessages('insufficientAccessActions'); 80 | } 81 | throw error; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/commands/data/create/record.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { Messages } from '@salesforce/core'; 9 | import type { SaveResult } from '@jsforce/jsforce-node'; 10 | import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; 11 | import { orgFlags, perflogFlag } from '../../../flags.js'; 12 | import { stringToDictionary, collectErrorMessages } from '../../../dataUtils.js'; 13 | 14 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 15 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'record.create'); 16 | 17 | export default class Create extends SfCommand { 18 | public static readonly summary = messages.getMessage('summary'); 19 | public static readonly description = messages.getMessage('description'); 20 | public static readonly examples = messages.getMessages('examples'); 21 | public static readonly aliases = ['force:data:record:create']; 22 | public static readonly deprecateAliases = true; 23 | public static readonly flags = { 24 | ...orgFlags, 25 | sobject: Flags.string({ 26 | char: 's', 27 | required: true, 28 | summary: messages.getMessage('flags.sobject.summary'), 29 | aliases: ['sobjecttype'], 30 | deprecateAliases: true, 31 | }), 32 | values: Flags.string({ 33 | char: 'v', 34 | required: true, 35 | summary: messages.getMessage('flags.values.summary'), 36 | }), 37 | 'use-tooling-api': Flags.boolean({ 38 | char: 't', 39 | summary: messages.getMessage('flags.use-tooling-api.summary'), 40 | aliases: ['usetoolingapi'], 41 | deprecateAliases: true, 42 | }), 43 | perflog: perflogFlag, 44 | }; 45 | 46 | public async run(): Promise { 47 | const { flags } = await this.parse(Create); 48 | this.spinner.start(`Creating record for ${flags.sobject}`); 49 | 50 | const sobject = ( 51 | flags['use-tooling-api'] 52 | ? flags['target-org'].getConnection(flags['api-version']).tooling 53 | : flags['target-org'].getConnection(flags['api-version']) 54 | ).sobject(flags.sobject); 55 | const values = stringToDictionary(flags.values); 56 | const result = await sobject.insert(values); 57 | if (result.success) { 58 | this.log(messages.getMessage('createSuccess', [result.id || 'unknown id'])); 59 | this.spinner.stop(); 60 | } else { 61 | const errors = collectErrorMessages(result); 62 | this.spinner.stop('failed'); 63 | this.error(messages.getMessage('createFailure', [errors])); 64 | } 65 | return result; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/commands/data/delete/bulk.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { Messages } from '@salesforce/core'; 9 | import { Flags, SfCommand } from '@salesforce/sf-plugins-core'; 10 | import { baseUpsertDeleteFlags, lineEndingFlag, bulkIngest } from '../../../bulkIngest.js'; 11 | import { BulkDeleteRequestCache } from '../../../bulkDataRequestCache.js'; 12 | import { BulkResultV2 } from '../../../types.js'; 13 | import { transformResults } from '../../../bulkUtils.js'; 14 | 15 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 16 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'bulkv2.delete'); 17 | 18 | export default class Delete extends SfCommand { 19 | public static readonly examples = messages.getMessages('examples'); 20 | public static readonly summary = messages.getMessage('summary'); 21 | public static readonly description = messages.getMessage('description'); 22 | 23 | public static readonly flags = { 24 | ...baseUpsertDeleteFlags, 25 | 'line-ending': lineEndingFlag, 26 | 'hard-delete': Flags.boolean({ 27 | summary: messages.getMessage('flags.hard-delete.summary'), 28 | description: messages.getMessage('flags.hard-delete.description'), 29 | default: false, 30 | }), 31 | }; 32 | 33 | public async run(): Promise { 34 | const { flags } = await this.parse(Delete); 35 | 36 | const res = await bulkIngest({ 37 | resumeCmdId: 'data delete resume', 38 | stageTitle: 'Deleting data', 39 | object: flags.sobject, 40 | operation: flags['hard-delete'] ? 'hardDelete' : 'delete', 41 | lineEnding: flags['line-ending'], 42 | columnDelimiter: undefined, 43 | conn: flags['target-org'].getConnection(flags['api-version']), 44 | cache: await BulkDeleteRequestCache.create(), 45 | async: flags.async, 46 | wait: flags.wait, 47 | file: flags.file, 48 | jsonEnabled: this.jsonEnabled(), 49 | logFn: (arg: string) => { 50 | this.log(arg); 51 | }, 52 | warnFn: (arg: SfCommand.Warning) => { 53 | this.warn(arg); 54 | }, 55 | }); 56 | 57 | const job = flags['target-org'].getConnection(flags['api-version']).bulk2.job('ingest', { 58 | id: res.jobId, 59 | }); 60 | 61 | if (res.failedRecords && res.failedRecords > 0) { 62 | process.exitCode = 1; 63 | } 64 | 65 | const jobInfo = await job.check(); 66 | 67 | return { 68 | jobInfo, 69 | records: 70 | jobInfo.state === 'JobComplete' 71 | ? transformResults(await job.getAllResults()) 72 | : { 73 | successfulResults: [], 74 | failedResults: [], 75 | unprocessedRecords: [], 76 | }, 77 | }; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/commands/data/delete/resume.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { Messages } from '@salesforce/core'; 9 | import { SfCommand } from '@salesforce/sf-plugins-core'; 10 | import type { BulkResultV2 } from '../../../types.js'; 11 | import { BulkDeleteRequestCache } from '../../../bulkDataRequestCache.js'; 12 | import { transformResults } from '../../../bulkUtils.js'; 13 | import { bulkIngestResume, baseUpsertDeleteResumeFlags } from '../../../bulkIngest.js'; 14 | 15 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 16 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'bulk.delete.resume'); 17 | 18 | export default class DeleteResume extends SfCommand { 19 | public static readonly summary = messages.getMessage('summary'); 20 | public static readonly description = messages.getMessage('description'); 21 | public static readonly examples = messages.getMessages('examples'); 22 | public static readonly deprecateAliases = true; 23 | 24 | public static readonly flags = baseUpsertDeleteResumeFlags; 25 | 26 | public async run(): Promise { 27 | const [{ flags }, cache] = await Promise.all([this.parse(DeleteResume), BulkDeleteRequestCache.create()]); 28 | 29 | const res = await bulkIngestResume({ 30 | cmdId: 'data delete resume', 31 | stageTitle: 'Deleting data', 32 | cache, 33 | jobIdOrMostRecent: flags['job-id'] ?? flags['use-most-recent'], 34 | jsonEnabled: this.jsonEnabled(), 35 | wait: flags.wait, 36 | warnFn: (arg: SfCommand.Warning) => { 37 | this.warn(arg); 38 | }, 39 | }); 40 | 41 | const { 42 | options: { connection: conn }, 43 | } = await cache.resolveResumeOptionsFromCache(flags['job-id'] ?? flags['use-most-recent']); 44 | 45 | const job = conn.bulk2.job('ingest', { 46 | id: res.jobId, 47 | }); 48 | 49 | return { 50 | jobInfo: await job.check(), 51 | records: transformResults(await job.getAllResults()), 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/commands/data/export/tree.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { Messages } from '@salesforce/core'; 9 | import { SfCommand, Flags, Ux } from '@salesforce/sf-plugins-core'; 10 | import { orgFlags, prefixValidation } from '../../../flags.js'; 11 | import { ExportConfig, runExport } from '../../../export.js'; 12 | import type { DataPlanPart, SObjectTreeFileContents } from '../../../types.js'; 13 | 14 | export type ExportTreeResult = DataPlanPart[] | SObjectTreeFileContents; 15 | 16 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 17 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'tree.export'); 18 | 19 | export default class Export extends SfCommand { 20 | public static readonly summary = messages.getMessage('summary'); 21 | public static readonly description = messages.getMessage('description'); 22 | public static readonly examples = messages.getMessages('examples'); 23 | public static readonly aliases = ['force:data:tree:export']; 24 | public static readonly deprecateAliases = true; 25 | 26 | public static readonly flags = { 27 | ...orgFlags, 28 | query: Flags.string({ 29 | multiple: true, 30 | char: 'q', 31 | summary: messages.getMessage('flags.query.summary'), 32 | required: true, 33 | }), 34 | plan: Flags.boolean({ 35 | char: 'p', 36 | summary: messages.getMessage('flags.plan.summary'), 37 | }), 38 | prefix: Flags.string({ 39 | char: 'x', 40 | summary: messages.getMessage('flags.prefix.summary'), 41 | parse: prefixValidation, 42 | }), 43 | 'output-dir': Flags.directory({ 44 | char: 'd', 45 | summary: messages.getMessage('flags.output-dir.summary'), 46 | aliases: ['outputdir'], 47 | deprecateAliases: true, 48 | }), 49 | }; 50 | 51 | public async run(): Promise { 52 | const { flags } = await this.parse(Export); 53 | const exportConfig: ExportConfig = { 54 | outputDir: flags['output-dir'], 55 | plan: flags.plan, 56 | prefix: flags.prefix, 57 | queries: flags.query, 58 | conn: flags['target-org'].getConnection(flags['api-version']), 59 | ux: new Ux({ jsonEnabled: this.jsonEnabled() }), 60 | }; 61 | return runExport(exportConfig); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/commands/data/get/record.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { Messages, SfError } from '@salesforce/core'; 9 | import type { Record } from '@jsforce/jsforce-node'; 10 | import { toAnyJson } from '@salesforce/ts-types'; 11 | import { SfCommand, Flags, Ux } from '@salesforce/sf-plugins-core'; 12 | import { orgFlags, perflogFlag } from '../../../flags.js'; 13 | import { query, logNestedObject } from '../../../dataUtils.js'; 14 | 15 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 16 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'record.get'); 17 | 18 | export default class Get extends SfCommand { 19 | public static readonly summary = messages.getMessage('summary'); 20 | public static readonly description = messages.getMessage('description'); 21 | public static readonly examples = messages.getMessages('examples'); 22 | public static readonly aliases = ['force:data:record:get']; 23 | public static readonly deprecateAliases = true; 24 | 25 | public static readonly flags = { 26 | ...orgFlags, 27 | sobject: Flags.string({ 28 | char: 's', 29 | required: true, 30 | summary: messages.getMessage('flags.sobject.summary'), 31 | aliases: ['sobjecttype'], 32 | deprecateAliases: true, 33 | }), 34 | // eslint-disable-next-line sf-plugin/id-flag-suggestions 35 | 'record-id': Flags.salesforceId({ 36 | length: 'both', 37 | char: 'i', 38 | summary: messages.getMessage('flags.record-id.summary'), 39 | exactlyOne: ['where', 'record-id'], 40 | aliases: ['sobjectid'], 41 | deprecateAliases: true, 42 | }), 43 | where: Flags.string({ 44 | char: 'w', 45 | summary: messages.getMessage('flags.where.summary'), 46 | exactlyOne: ['where', 'record-id'], 47 | }), 48 | 'use-tooling-api': Flags.boolean({ 49 | char: 't', 50 | summary: messages.getMessage('flags.use-tooling-api.summary'), 51 | aliases: ['usetoolingapi'], 52 | deprecateAliases: true, 53 | }), 54 | perflog: perflogFlag, 55 | }; 56 | 57 | public async run(): Promise { 58 | const { flags } = await this.parse(Get); 59 | this.spinner.start('Getting Record'); 60 | const conn = flags['use-tooling-api'] 61 | ? flags['target-org'].getConnection(flags['api-version']).tooling 62 | : flags['target-org'].getConnection(flags['api-version']); 63 | try { 64 | const sObjectId = flags['record-id'] ?? ((await query(conn, flags.sobject, flags.where as string)).Id as string); 65 | const result = await conn.sobject(flags.sobject).retrieve(sObjectId); 66 | if (!this.jsonEnabled()) { 67 | logNestedObject(new Ux({ jsonEnabled: this.jsonEnabled() }), result as never); 68 | } 69 | this.spinner.stop(); 70 | return toAnyJson(result) as Record; 71 | } catch (err) { 72 | this.spinner.stop('failed'); 73 | throw new SfError((err as Error).message, (err as Error).name); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/commands/data/import/bulk.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; 9 | import { Messages } from '@salesforce/core'; 10 | import { bulkIngest, columnDelimiterFlag } from '../../../bulkIngest.js'; 11 | import { BulkImportRequestCache } from '../../../bulkDataRequestCache.js'; 12 | 13 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 14 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'data.import.bulk'); 15 | 16 | export type DataImportBulkResult = { 17 | jobId: string; 18 | processedRecords?: number; 19 | successfulRecords?: number; 20 | failedRecords?: number; 21 | }; 22 | 23 | export default class DataImportBulk extends SfCommand { 24 | public static readonly summary = messages.getMessage('summary'); 25 | public static readonly description = messages.getMessage('description'); 26 | public static readonly examples = messages.getMessages('examples'); 27 | 28 | public static readonly flags = { 29 | async: Flags.boolean({ 30 | summary: messages.getMessage('flags.async.summary'), 31 | char: 'a', 32 | exclusive: ['wait'], 33 | deprecated: true, 34 | }), 35 | file: Flags.file({ 36 | summary: messages.getMessage('flags.file.summary'), 37 | char: 'f', 38 | required: true, 39 | exists: true, 40 | }), 41 | sobject: Flags.string({ 42 | summary: messages.getMessage('flags.sobject.summary'), 43 | char: 's', 44 | required: true, 45 | }), 46 | 'api-version': Flags.orgApiVersion(), 47 | wait: Flags.duration({ 48 | summary: messages.getMessage('flags.wait.summary'), 49 | char: 'w', 50 | unit: 'minutes', 51 | exclusive: ['async'], 52 | }), 53 | 'target-org': Flags.requiredOrg(), 54 | 'line-ending': Flags.option({ 55 | summary: messages.getMessage('flags.line-ending.summary'), 56 | dependsOn: ['file'], 57 | options: ['CRLF', 'LF'] as const, 58 | })(), 59 | 'column-delimiter': columnDelimiterFlag, 60 | }; 61 | 62 | public async run(): Promise { 63 | const { flags } = await this.parse(DataImportBulk); 64 | 65 | return bulkIngest({ 66 | resumeCmdId: 'data import resume', 67 | stageTitle: 'Importing data', 68 | object: flags.sobject, 69 | operation: 'insert', 70 | lineEnding: flags['line-ending'], 71 | columnDelimiter: flags['column-delimiter'], 72 | conn: flags['target-org'].getConnection(flags['api-version']), 73 | cache: await BulkImportRequestCache.create(), 74 | async: flags.async, 75 | wait: flags.wait, 76 | file: flags.file, 77 | jsonEnabled: this.jsonEnabled(), 78 | logFn: (arg: string) => { 79 | this.log(arg); 80 | }, 81 | warnFn: (arg: SfCommand.Warning) => { 82 | this.warn(arg); 83 | }, 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/commands/data/import/resume.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; 9 | import { Messages } from '@salesforce/core'; 10 | import { BulkImportRequestCache } from '../../../bulkDataRequestCache.js'; 11 | import { bulkIngestResume } from '../../../bulkIngest.js'; 12 | 13 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 14 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'data.import.resume'); 15 | 16 | export type DataImportResumeResult = { 17 | jobId: string; 18 | processedRecords?: number; 19 | successfulRecords?: number; 20 | failedRecords?: number; 21 | }; 22 | 23 | export default class DataImportResume extends SfCommand { 24 | public static readonly summary = messages.getMessage('summary'); 25 | public static readonly description = messages.getMessage('description'); 26 | public static readonly examples = messages.getMessages('examples'); 27 | 28 | public static readonly flags = { 29 | 'use-most-recent': Flags.boolean({ 30 | summary: messages.getMessage('flags.use-most-recent.summary'), 31 | exactlyOne: ['job-id'], 32 | }), 33 | 'job-id': Flags.salesforceId({ 34 | summary: messages.getMessage('flags.job-id.summary'), 35 | char: 'i', 36 | length: 18, 37 | startsWith: '750', 38 | exactlyOne: ['use-most-recent'], 39 | }), 40 | wait: Flags.duration({ 41 | char: 'w', 42 | unit: 'minutes', 43 | summary: messages.getMessage('flags.wait.summary'), 44 | defaultValue: 5, 45 | }), 46 | }; 47 | 48 | public async run(): Promise { 49 | const { flags } = await this.parse(DataImportResume); 50 | 51 | return bulkIngestResume({ 52 | cmdId: 'data import resume', 53 | stageTitle: 'Updating data', 54 | cache: await BulkImportRequestCache.create(), 55 | jobIdOrMostRecent: flags['job-id'] ?? flags['use-most-recent'], 56 | jsonEnabled: this.jsonEnabled(), 57 | wait: flags.wait, 58 | warnFn: (arg: SfCommand.Warning) => { 59 | this.warn(arg); 60 | }, 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/commands/data/resume.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { Messages, SfError } from '@salesforce/core'; 9 | import { SfCommand, Flags, Ux } from '@salesforce/sf-plugins-core'; 10 | import { BatchInfo } from '@jsforce/jsforce-node/lib/api/bulk.js'; 11 | import { orgFlags } from '../../flags.js'; 12 | import { Batcher } from '../../batcher.js'; 13 | import type { StatusResult } from '../../types.js'; 14 | 15 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 16 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'data.resume'); 17 | 18 | export default class Status extends SfCommand { 19 | public static readonly summary = messages.getMessage('summary'); 20 | public static readonly description = messages.getMessage('description'); 21 | public static readonly examples = messages.getMessages('examples'); 22 | 23 | public static readonly flags = { 24 | ...orgFlags, 25 | 'batch-id': Flags.salesforceId({ 26 | length: 18, 27 | char: 'b', 28 | startsWith: '751', 29 | summary: messages.getMessage('flags.batch-id.summary'), 30 | aliases: ['batchid'], 31 | deprecateAliases: true, 32 | }), 33 | 'job-id': Flags.salesforceId({ 34 | length: 18, 35 | char: 'i', 36 | startsWith: '750', 37 | summary: messages.getMessage('flags.job-id.summary'), 38 | required: true, 39 | aliases: ['jobid'], 40 | deprecateAliases: true, 41 | }), 42 | }; 43 | 44 | public async run(): Promise { 45 | const { flags } = await this.parse(Status); 46 | this.spinner.start('Getting Status'); 47 | const conn = flags['target-org'].getConnection(flags['api-version']); 48 | const batcher = new Batcher(conn, new Ux({ jsonEnabled: this.jsonEnabled() })); 49 | if (flags['job-id'] && flags['batch-id']) { 50 | // view batch status 51 | const job = conn.bulk.job(flags['job-id']); 52 | 53 | const batches: BatchInfo[] = await job.list(); 54 | const matchBatch = batches 55 | .filter((batch) => batch.id === flags['batch-id']) 56 | .map((batch) => batcher.bulkStatus(batch)); 57 | 58 | if (!matchBatch.length) { 59 | throw new SfError(messages.getMessage('NoBatchFound', [flags['batch-id'], flags['job-id']]), 'NoBatchFound'); 60 | } 61 | 62 | this.spinner.stop(); 63 | return batches; 64 | } 65 | // view job status 66 | const jobStatus = await batcher.fetchAndDisplayJobStatus(flags['job-id']); 67 | this.spinner.stop(); 68 | return jobStatus; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/commands/data/search.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import fs from 'node:fs'; 9 | import { Messages } from '@salesforce/core'; 10 | import type { SearchResult } from '@jsforce/jsforce-node'; 11 | import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; 12 | import { displaySearchResults } from '../../searchUtils.js'; 13 | import { FormatTypes, formatTypes } from '../../reporters/query/reporters.js'; 14 | 15 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 16 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'data.search'); 17 | 18 | export class DataSearchCommand extends SfCommand { 19 | public static readonly summary = messages.getMessage('summary'); 20 | public static readonly description = messages.getMessage('description'); 21 | public static readonly examples = messages.getMessages('examples'); 22 | 23 | public static readonly flags = { 24 | 'target-org': Flags.requiredOrg(), 25 | 'api-version': Flags.orgApiVersion(), 26 | query: Flags.string({ 27 | char: 'q', 28 | summary: messages.getMessage('flags.query.summary'), 29 | exactlyOne: ['query', 'file'], 30 | }), 31 | file: Flags.file({ 32 | char: 'f', 33 | exists: true, 34 | summary: messages.getMessage('flags.file.summary'), 35 | exactlyOne: ['query', 'file'], 36 | }), 37 | 'result-format': Flags.custom({ 38 | char: 'r', 39 | summary: messages.getMessage('flags.result-format.summary'), 40 | options: formatTypes, 41 | default: 'human', 42 | exclusive: ['json'], 43 | })(), 44 | }; 45 | 46 | public async run(): Promise { 47 | const flags = (await this.parse(DataSearchCommand)).flags; 48 | 49 | try { 50 | // --file will be present if flags.query isn't. Oclif exactlyOne isn't quite that clever 51 | const queryString = flags.query ?? fs.readFileSync(flags.file as string, 'utf8'); 52 | const conn = flags['target-org'].getConnection(flags['api-version']); 53 | if (flags['result-format'] !== 'json') this.spinner.start(messages.getMessage('queryRunningMessage')); 54 | const queryResult = await conn.search(queryString); 55 | if (!this.jsonEnabled()) { 56 | displaySearchResults(queryResult, flags['result-format']); 57 | } 58 | return queryResult; 59 | } finally { 60 | if (flags['result-format'] !== 'json') this.spinner.stop(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/commands/data/update/bulk.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; 9 | import { Messages } from '@salesforce/core'; 10 | import { bulkIngest, columnDelimiterFlag, lineEndingFlag } from '../../../bulkIngest.js'; 11 | import { BulkUpdateRequestCache } from '../../../bulkDataRequestCache.js'; 12 | 13 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 14 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'data.update.bulk'); 15 | 16 | export type DataUpdateBulkResult = { 17 | jobId: string; 18 | processedRecords?: number; 19 | successfulRecords?: number; 20 | failedRecords?: number; 21 | }; 22 | 23 | export default class DataUpdateBulk extends SfCommand { 24 | public static readonly summary = messages.getMessage('summary'); 25 | public static readonly description = messages.getMessage('description'); 26 | public static readonly examples = messages.getMessages('examples'); 27 | 28 | public static readonly flags = { 29 | async: Flags.boolean({ 30 | summary: messages.getMessage('flags.async.summary'), 31 | char: 'a', 32 | deprecated: true, 33 | }), 34 | wait: Flags.duration({ 35 | summary: messages.getMessage('flags.wait.summary'), 36 | char: 'w', 37 | unit: 'minutes', 38 | }), 39 | file: Flags.file({ 40 | summary: messages.getMessage('flags.file.summary'), 41 | char: 'f', 42 | required: true, 43 | exists: true, 44 | }), 45 | sobject: Flags.string({ 46 | summary: messages.getMessage('flags.sobject.summary'), 47 | char: 's', 48 | required: true, 49 | }), 50 | 'api-version': Flags.orgApiVersion(), 51 | 'target-org': Flags.requiredOrg(), 52 | 'line-ending': lineEndingFlag, 53 | 'column-delimiter': columnDelimiterFlag, 54 | }; 55 | 56 | public async run(): Promise { 57 | const { flags } = await this.parse(DataUpdateBulk); 58 | 59 | return bulkIngest({ 60 | resumeCmdId: 'data update resume', 61 | stageTitle: 'Updating data', 62 | object: flags.sobject, 63 | operation: 'update', 64 | lineEnding: flags['line-ending'], 65 | columnDelimiter: flags['column-delimiter'], 66 | conn: flags['target-org'].getConnection(flags['api-version']), 67 | cache: await BulkUpdateRequestCache.create(), 68 | async: flags.async, 69 | wait: flags.wait, 70 | file: flags.file, 71 | jsonEnabled: this.jsonEnabled(), 72 | logFn: (arg: string) => { 73 | this.log(arg); 74 | }, 75 | warnFn: (arg: SfCommand.Warning) => { 76 | this.warn(arg); 77 | }, 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/commands/data/update/resume.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; 9 | import { Messages } from '@salesforce/core'; 10 | import { BulkUpdateRequestCache } from '../../../bulkDataRequestCache.js'; 11 | import { bulkIngestResume } from '../../../bulkIngest.js'; 12 | 13 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 14 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'data.update.resume'); 15 | 16 | export type DataUpdateResumeResult = { 17 | jobId: string; 18 | processedRecords?: number; 19 | successfulRecords?: number; 20 | failedRecords?: number; 21 | }; 22 | 23 | export default class DataUpdateResume extends SfCommand { 24 | public static readonly summary = messages.getMessage('summary'); 25 | public static readonly description = messages.getMessage('description'); 26 | public static readonly examples = messages.getMessages('examples'); 27 | 28 | public static readonly flags = { 29 | 'use-most-recent': Flags.boolean({ 30 | summary: messages.getMessage('flags.use-most-recent.summary'), 31 | exactlyOne: ['job-id'], 32 | }), 33 | 'job-id': Flags.salesforceId({ 34 | summary: messages.getMessage('flags.job-id.summary'), 35 | char: 'i', 36 | length: 18, 37 | startsWith: '750', 38 | exactlyOne: ['use-most-recent'], 39 | }), 40 | wait: Flags.duration({ 41 | char: 'w', 42 | unit: 'minutes', 43 | summary: messages.getMessage('flags.wait.summary'), 44 | defaultValue: 5, 45 | }), 46 | }; 47 | 48 | public async run(): Promise { 49 | const { flags } = await this.parse(DataUpdateResume); 50 | 51 | return bulkIngestResume({ 52 | cmdId: 'data update resume', 53 | stageTitle: 'Updating data', 54 | cache: await BulkUpdateRequestCache.create(), 55 | jobIdOrMostRecent: flags['job-id'] ?? flags['use-most-recent'], 56 | jsonEnabled: this.jsonEnabled(), 57 | wait: flags.wait, 58 | warnFn: (arg: SfCommand.Warning) => { 59 | this.warn(arg); 60 | }, 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/commands/data/upsert/bulk.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { Messages } from '@salesforce/core'; 9 | import { Flags, SfCommand } from '@salesforce/sf-plugins-core'; 10 | import { baseUpsertDeleteFlags, bulkIngest, columnDelimiterFlag, lineEndingFlag } from '../../../bulkIngest.js'; 11 | import type { BulkResultV2 } from '../../../types.js'; 12 | import { BulkUpsertRequestCache } from '../../../bulkDataRequestCache.js'; 13 | import { transformResults } from '../../../bulkUtils.js'; 14 | 15 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 16 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'bulkv2.upsert'); 17 | 18 | export default class Upsert extends SfCommand { 19 | public static readonly summary = messages.getMessage('summary'); 20 | public static readonly description = messages.getMessage('description'); 21 | public static readonly examples = messages.getMessages('examples'); 22 | 23 | public static readonly flags = { 24 | ...baseUpsertDeleteFlags, 25 | 'line-ending': lineEndingFlag, 26 | 'column-delimiter': columnDelimiterFlag, 27 | 'external-id': Flags.string({ 28 | char: 'i', 29 | summary: messages.getMessage('flags.external-id.summary'), 30 | required: true, 31 | aliases: ['externalid'], 32 | deprecateAliases: true, 33 | }), 34 | }; 35 | 36 | public async run(): Promise { 37 | const { flags } = await this.parse(Upsert); 38 | 39 | const res = await bulkIngest({ 40 | resumeCmdId: 'data upsert resume', 41 | stageTitle: 'Upserting data', 42 | object: flags.sobject, 43 | operation: 'upsert', 44 | lineEnding: flags['line-ending'], 45 | columnDelimiter: flags['column-delimiter'], 46 | externalId: flags['external-id'], 47 | conn: flags['target-org'].getConnection(flags['api-version']), 48 | cache: await BulkUpsertRequestCache.create(), 49 | async: flags.async, 50 | wait: flags.wait, 51 | file: flags.file, 52 | jsonEnabled: this.jsonEnabled(), 53 | logFn: (arg: string) => { 54 | this.log(arg); 55 | }, 56 | warnFn: (arg: SfCommand.Warning) => { 57 | this.warn(arg); 58 | }, 59 | }); 60 | 61 | const job = flags['target-org'].getConnection(flags['api-version']).bulk2.job('ingest', { 62 | id: res.jobId, 63 | }); 64 | 65 | if (res.failedRecords && res.failedRecords > 0) { 66 | process.exitCode = 1; 67 | } 68 | 69 | const jobInfo = await job.check(); 70 | 71 | return { 72 | jobInfo, 73 | records: 74 | jobInfo.state === 'JobComplete' 75 | ? transformResults(await job.getAllResults()) 76 | : { 77 | successfulResults: [], 78 | failedResults: [], 79 | unprocessedRecords: [], 80 | }, 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/commands/data/upsert/resume.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { Messages } from '@salesforce/core'; 9 | import { SfCommand } from '@salesforce/sf-plugins-core'; 10 | import type { BulkResultV2 } from '../../../types.js'; 11 | import { BulkUpsertRequestCache } from '../../../bulkDataRequestCache.js'; 12 | import { transformResults } from '../../../bulkUtils.js'; 13 | import { baseUpsertDeleteResumeFlags, bulkIngestResume } from '../../../bulkIngest.js'; 14 | 15 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 16 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'bulk.upsert.resume'); 17 | 18 | export default class UpsertResume extends SfCommand { 19 | public static readonly summary = messages.getMessage('summary'); 20 | public static readonly description = messages.getMessage('description'); 21 | public static readonly examples = messages.getMessages('examples'); 22 | 23 | public static readonly flags = baseUpsertDeleteResumeFlags; 24 | 25 | public async run(): Promise { 26 | const [{ flags }, cache] = await Promise.all([this.parse(UpsertResume), BulkUpsertRequestCache.create()]); 27 | 28 | const res = await bulkIngestResume({ 29 | cmdId: 'data upsert resume', 30 | stageTitle: 'Upserting data', 31 | cache, 32 | jobIdOrMostRecent: flags['job-id'] ?? flags['use-most-recent'], 33 | jsonEnabled: this.jsonEnabled(), 34 | wait: flags.wait, 35 | warnFn: (arg: SfCommand.Warning) => { 36 | this.warn(arg); 37 | }, 38 | }); 39 | 40 | const { 41 | options: { connection: conn }, 42 | } = await cache.resolveResumeOptionsFromCache(flags['job-id'] ?? flags['use-most-recent']); 43 | 44 | const job = conn.bulk2.job('ingest', { 45 | id: res.jobId, 46 | }); 47 | 48 | return { 49 | jobInfo: await job.check(), 50 | records: transformResults(await job.getAllResults()), 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/commands/force/data/bulk/delete.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import fs from 'node:fs'; 8 | import { ReadStream } from 'node:fs'; 9 | 10 | import { Connection, Messages } from '@salesforce/core'; 11 | import { Flags, SfCommand, Ux } from '@salesforce/sf-plugins-core'; 12 | import { orgFlags } from '../../../../flags.js'; 13 | import { Batcher, BatcherReturnType } from '../../../../batcher.js'; 14 | import { validateSobjectType } from '../../../../bulkUtils.js'; 15 | 16 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 17 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'bulk.delete'); 18 | 19 | export default class Delete extends SfCommand { 20 | public static readonly examples = messages.getMessages('examples'); 21 | public static readonly summary = messages.getMessage('summary'); 22 | public static readonly description = messages.getMessage('description'); 23 | 24 | public static readonly flags = { 25 | ...orgFlags, 26 | file: Flags.file({ 27 | char: 'f', 28 | summary: messages.getMessage('flags.file.summary'), 29 | required: true, 30 | exists: true, 31 | aliases: ['csvfile'], 32 | deprecateAliases: true, 33 | }), 34 | sobject: Flags.string({ 35 | char: 's', 36 | summary: messages.getMessage('flags.sobject.summary'), 37 | required: true, 38 | aliases: ['sobjecttype'], 39 | deprecateAliases: true, 40 | }), 41 | wait: Flags.duration({ 42 | char: 'w', 43 | unit: 'minutes', 44 | summary: messages.getMessage('flags.wait.summary'), 45 | min: 0, 46 | defaultValue: 0, 47 | }), 48 | }; 49 | 50 | public async run(): Promise { 51 | const { flags } = await this.parse(Delete); 52 | 53 | const conn: Connection = flags['target-org'].getConnection(flags['api-version']); 54 | this.spinner.start('Bulk Delete'); 55 | 56 | const sobject = await validateSobjectType(flags.sobject, conn); 57 | 58 | const csvRecords: ReadStream = fs.createReadStream(flags.file, { encoding: 'utf-8' }); 59 | const job = conn.bulk.createJob<'delete'>(sobject, 'delete'); 60 | const batcher: Batcher = new Batcher(conn, new Ux({ jsonEnabled: this.jsonEnabled() })); 61 | 62 | // eslint-disable-next-line @typescript-eslint/no-misused-promises,no-async-promise-executor 63 | return new Promise(async (resolve, reject) => { 64 | job.on('error', (err): void => { 65 | throw err; 66 | }); 67 | 68 | try { 69 | resolve(await batcher.createAndExecuteBatches(job, csvRecords, sobject, flags.wait?.minutes)); 70 | this.spinner.stop(); 71 | } catch (e) { 72 | this.spinner.stop('error'); 73 | reject(e); 74 | } 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/commands/force/data/bulk/status.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { BatchInfo } from '@jsforce/jsforce-node/lib/api/bulk.js'; 9 | import { Messages, SfError } from '@salesforce/core'; 10 | import { Flags, SfCommand, Ux } from '@salesforce/sf-plugins-core'; 11 | import { orgFlags } from '../../../../flags.js'; 12 | import { Batcher } from '../../../../batcher.js'; 13 | import type { StatusResult } from '../../../../types.js'; 14 | 15 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 16 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'bulk.status'); 17 | 18 | export default class Status extends SfCommand { 19 | public static readonly summary = messages.getMessage('summary'); 20 | public static readonly description = messages.getMessage('description'); 21 | public static readonly examples = messages.getMessages('examples'); 22 | 23 | public static readonly flags = { 24 | ...orgFlags, 25 | 'batch-id': Flags.salesforceId({ 26 | length: 18, 27 | char: 'b', 28 | startsWith: '751', 29 | summary: messages.getMessage('flags.batch-id.summary'), 30 | aliases: ['batchid'], 31 | deprecateAliases: true, 32 | }), 33 | 'job-id': Flags.salesforceId({ 34 | length: 18, 35 | char: 'i', 36 | startsWith: '750', 37 | summary: messages.getMessage('flags.job-id.summary'), 38 | required: true, 39 | aliases: ['jobid'], 40 | deprecateAliases: true, 41 | }), 42 | }; 43 | 44 | public async run(): Promise { 45 | const { flags } = await this.parse(Status); 46 | this.spinner.start('Getting Status'); 47 | const conn = flags['target-org'].getConnection(flags['api-version']); 48 | const batcher = new Batcher(conn, new Ux({ jsonEnabled: this.jsonEnabled() })); 49 | if (flags['job-id'] && flags['batch-id']) { 50 | // view batch status 51 | const job = conn.bulk.job(flags['job-id']); 52 | let found = false; 53 | 54 | const batches: BatchInfo[] = await job.list(); 55 | batches.forEach((batch: BatchInfo) => { 56 | if (batch.id === flags['batch-id']) { 57 | batcher.bulkStatus(batch); 58 | found = true; 59 | } 60 | }); 61 | if (!found) { 62 | throw new SfError(messages.getMessage('NoBatchFound', [flags['batch-id'], flags['job-id']]), 'NoBatchFound'); 63 | } 64 | 65 | this.spinner.stop(); 66 | return batches; 67 | } else { 68 | // view job status 69 | const jobStatus = await batcher.fetchAndDisplayJobStatus(flags['job-id']); 70 | this.spinner.stop(); 71 | return jobStatus; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/flags.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { Messages } from '@salesforce/core'; 9 | import { 10 | Flags, 11 | loglevel, 12 | orgApiVersionFlagWithDeprecations, 13 | requiredOrgFlagWithDeprecations, 14 | } from '@salesforce/sf-plugins-core'; 15 | import { FormatTypes, formatTypes } from './reporters/query/reporters.js'; 16 | 17 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 18 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'messages'); 19 | 20 | export const perflogFlag = Flags.boolean({ 21 | summary: messages.getMessage('perfLogLevelOption'), 22 | description: messages.getMessage('perfLogLevelOptionLong'), 23 | hidden: true, 24 | deprecated: { 25 | version: '57', 26 | }, 27 | }); 28 | 29 | /** 30 | * Use only for commands that maintain sfdx compatibility. 31 | * 32 | * @deprecated 33 | */ 34 | export const orgFlags = { 35 | 'target-org': requiredOrgFlagWithDeprecations, 36 | 'api-version': orgApiVersionFlagWithDeprecations, 37 | loglevel, 38 | }; 39 | 40 | export const resultFormatFlag = Flags.custom({ 41 | char: 'r', 42 | summary: messages.getMessage('flags.resultFormat.summary'), 43 | options: formatTypes, 44 | default: 'human', 45 | aliases: ['resultformat'], 46 | deprecateAliases: true, 47 | }); 48 | 49 | export const prefixValidation = (i: string): Promise => { 50 | if (i.includes('/') || i.includes('\\')) { 51 | const treeExportMsgs = Messages.loadMessages('@salesforce/plugin-data', 'tree.export'); 52 | throw new Error(treeExportMsgs.getMessage('PrefixSlashError')); 53 | } 54 | return Promise.resolve(i); 55 | }; 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | export default {}; 9 | -------------------------------------------------------------------------------- /src/queryUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { SoqlQueryResult } from './types.js'; 8 | import { FormatTypes, JsonReporter } from './reporters/query/reporters.js'; 9 | import { CsvReporter } from './reporters/query/csvReporter.js'; 10 | import { HumanReporter } from './reporters/query/humanReporter.js'; 11 | 12 | export const displayResults = (queryResult: SoqlQueryResult, resultFormat: FormatTypes, outputFile?: string): void => { 13 | let reporter: HumanReporter | JsonReporter | CsvReporter; 14 | switch (resultFormat) { 15 | case 'human': 16 | reporter = new HumanReporter(queryResult, queryResult.columns); 17 | break; 18 | case 'json': 19 | reporter = new JsonReporter(queryResult, queryResult.columns, outputFile); 20 | break; 21 | case 'csv': 22 | reporter = new CsvReporter(queryResult, queryResult.columns, outputFile); 23 | break; 24 | } 25 | // delegate to selected reporter 26 | reporter.display(); 27 | }; 28 | -------------------------------------------------------------------------------- /src/reporters/search/csvSearchReporter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import os from 'node:os'; 8 | import fs from 'node:fs'; 9 | import { SearchResult } from '@jsforce/jsforce-node'; 10 | import { SearchReporter } from './reporter.js'; 11 | 12 | export class CsvSearchReporter extends SearchReporter { 13 | public constructor(props: SearchResult) { 14 | super(props); 15 | } 16 | 17 | public display(): void { 18 | if (this.typeRecordsMap.size === 0) { 19 | this.ux.log('No Records Found'); 20 | } 21 | this.typeRecordsMap.forEach((records, type) => { 22 | this.ux.log(`Written to ${type}.csv`); 23 | const cols = Object.keys(records[0]).join(','); 24 | const body = records.map((r) => Object.values(r).join(',')).join(os.EOL); 25 | 26 | fs.writeFileSync(`${type}.csv`, [cols, body].join(os.EOL)); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/reporters/search/humanSearchReporter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { SearchResult } from '@jsforce/jsforce-node'; 8 | import { SearchReporter } from './reporter.js'; 9 | 10 | export class HumanSearchReporter extends SearchReporter { 11 | public constructor(props: SearchResult) { 12 | super(props); 13 | } 14 | 15 | public display(): void { 16 | if (this.typeRecordsMap.size === 0) { 17 | this.ux.log('No Records Found'); 18 | } 19 | this.typeRecordsMap.forEach((records, type) => { 20 | this.ux.table({ 21 | data: records, 22 | overflow: 'wrap', 23 | title: type, 24 | }); 25 | this.ux.log(); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/reporters/search/reporter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { Ux } from '@salesforce/sf-plugins-core'; 8 | import { Record, SearchResult } from '@jsforce/jsforce-node'; 9 | import { ensureString } from '@salesforce/ts-types'; 10 | import { omit } from '@salesforce/kit'; 11 | 12 | export abstract class SearchReporter { 13 | public typeRecordsMap: Map = new Map(); 14 | public ux = new Ux(); 15 | protected constructor(public result: SearchResult) { 16 | this.result.searchRecords.map((r) => { 17 | const type = ensureString(r.attributes?.type); 18 | return this.typeRecordsMap.has(type) 19 | ? // the extra info in 'attributes' causes issues when creating generic csv/table columns 20 | this.typeRecordsMap.get(type)!.push(omit(r, 'attributes')) 21 | : this.typeRecordsMap.set(type, [omit(r, 'attributes')]); 22 | }); 23 | } 24 | public abstract display(): void; 25 | } 26 | 27 | export class JsonSearchReporter extends SearchReporter { 28 | public constructor(props: SearchResult) { 29 | super(props); 30 | } 31 | 32 | public display(): void { 33 | this.ux.styledJSON(this.result); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/searchUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import type { SearchResult } from '@jsforce/jsforce-node'; 8 | import { HumanSearchReporter } from './reporters/search/humanSearchReporter.js'; 9 | import { JsonSearchReporter } from './reporters/search/reporter.js'; 10 | import { CsvSearchReporter } from './reporters/search/csvSearchReporter.js'; 11 | 12 | export const displaySearchResults = (queryResult: SearchResult, resultFormat: 'human' | 'json' | 'csv'): void => { 13 | let reporter: HumanSearchReporter | JsonSearchReporter | CsvSearchReporter; 14 | 15 | switch (resultFormat) { 16 | case 'human': 17 | reporter = new HumanSearchReporter(queryResult); 18 | break; 19 | case 'csv': 20 | reporter = new CsvSearchReporter(queryResult); 21 | break; 22 | case 'json': 23 | reporter = new JsonSearchReporter(queryResult); 24 | break; 25 | } 26 | // delegate to selected reporter 27 | reporter.display(); 28 | }; 29 | -------------------------------------------------------------------------------- /test/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | module.exports = { 9 | extends: '../.eslintrc.cjs', 10 | // Allow describe and it 11 | env: { mocha: true }, 12 | rules: { 13 | // Allow assert style expressions. i.e. expect(true).to.be.true 14 | 'no-unused-expressions': 'off', 15 | 16 | // It is common for tests to stub out method. 17 | '@typescript-eslint/ban-ts-ignore': 'off', 18 | 19 | // Return types are defined by the source code. Allows for quick overwrites. 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | // Mocked out the methods that shouldn't do anything in the tests. 22 | '@typescript-eslint/no-empty-function': 'off', 23 | // Easily return a promise in a mocked method. 24 | '@typescript-eslint/require-await': 'off', 25 | // relax unused vars for tests 26 | '@typescript-eslint/no-unused-vars': 'off', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /test/api/data/tree/importFiles.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import fs from 'node:fs'; 8 | import { Messages } from '@salesforce/core'; 9 | 10 | import { expect, assert } from 'chai'; 11 | import { SObjectTreeFileContents } from '../../../../src/types.js'; 12 | import { FileInfo, createSObjectTypeMap, validateNoRefs } from '../../../../src/api/data/tree/importFiles.js'; 13 | 14 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 15 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'importApi'); 16 | 17 | describe('importFiles', () => { 18 | describe('validateNoRefs', () => { 19 | const good: FileInfo = { 20 | rawContents: '', 21 | records: [ 22 | { 23 | attributes: { 24 | type: 'Account', 25 | referenceId: 'ref1', 26 | }, 27 | }, 28 | ], 29 | filePath: 'testPath', 30 | sobject: 'Account', 31 | }; 32 | it('return a good FileInfo', () => { 33 | expect(validateNoRefs(good)).to.deep.equal(good); 34 | }); 35 | it('throws for a bad ref', () => { 36 | const bad = { 37 | ...good, 38 | // eslint-disable-next-line camelcase 39 | records: [...good.records, { attributes: { type: 'Account', referenceId: 'ref2' }, Field__c: '@ContactRef46' }], 40 | }; 41 | const expectedError = messages.getMessage('error.RefsInFiles', [bad.filePath]); 42 | try { 43 | validateNoRefs(bad); 44 | throw new Error('Expected an error'); 45 | } catch (e) { 46 | assert(e instanceof Error); 47 | expect(e.message).to.equal(expectedError); 48 | } 49 | }); 50 | }); 51 | describe('createSobjectTypeMap', () => { 52 | it('works with a 2-level tree file with nested records', () => { 53 | const accountsContactsTreeJSON = JSON.parse( 54 | fs.readFileSync('test/api/data/tree/test-files/accounts-contacts-tree.json', 'utf-8') 55 | ) as SObjectTreeFileContents; 56 | 57 | expect(createSObjectTypeMap(accountsContactsTreeJSON.records)).to.deep.equal( 58 | new Map([ 59 | ['SampleAccountRef', 'Account'], 60 | ['PresidentSmithRef', 'Contact'], 61 | ['VPEvansRef', 'Contact'], 62 | ['SampleAcct2Ref', 'Account'], 63 | ]) 64 | ); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/api/data/tree/test-files/accounts-contacts-plan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sobject": "Account", 4 | "saveRefs": false, 5 | "files": [ 6 | { 7 | "file": "accounts-only.json", 8 | "saveRefs": true 9 | } 10 | ] 11 | }, 12 | { 13 | "sobject": "Account", 14 | "resolveRefs": false, 15 | "files": [ 16 | { 17 | "file": "contacts-only-1.json", 18 | "resolveRefs": true 19 | }, 20 | "contacts-only-2.json" 21 | ] 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /test/api/data/tree/test-files/accounts-contacts-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { "type": "Account", "referenceId": "SampleAccountRef" }, 5 | "name": "SampleAccount", 6 | "phone": "1234567890", 7 | "website": "www.salesforce.com", 8 | "numberOfEmployees": "100", 9 | "industry": "Banking", 10 | "Contacts": { 11 | "records": [ 12 | { 13 | "attributes": { "type": "Contact", "referenceId": "PresidentSmithRef" }, 14 | "lastname": "Smith", 15 | "title": "President" 16 | }, 17 | { 18 | "attributes": { "type": "Contact", "referenceId": "VPEvansRef" }, 19 | "lastname": "Evans", 20 | "title": "Vice President" 21 | } 22 | ] 23 | } 24 | }, 25 | { 26 | "attributes": { "type": "Account", "referenceId": "SampleAcct2Ref" }, 27 | "name": "SampleAccount2", 28 | "phone": "1234567890", 29 | "website": "www.salesforce2.com", 30 | "numberOfEmployees": "100", 31 | "industry": "Banking" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test/api/data/tree/test-files/accounts-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { "type": "Account", "referenceId": "SampleAccountRef" }, 5 | "name": "SampleAccount", 6 | "phone": "1234567890", 7 | "website": "www.salesforce.com", 8 | "numberOfEmployees": "100", 9 | "industry": "Banking", 10 | "Contacts": { 11 | "records": [ 12 | { 13 | "attributes": { "type": "Contact", "referenceId": "PresidentSmithRef" }, 14 | "lastname": "Smith", 15 | "title": "President" 16 | }, 17 | { 18 | "attributes": { "type": "Contact", "referenceId": "VPEvansRef" }, 19 | "lastname": "Evans", 20 | "title": "Vice President" 21 | } 22 | ] 23 | } 24 | }, 25 | { 26 | "attributes": { "type": "Account", "referenceId": "SampleAcct2Ref" }, 27 | "name": "SampleAccount2", 28 | "phone": "1234567890", 29 | "website": "www.salesforce2.com", 30 | "numberOfEmployees": "100", 31 | "industry": "Banking" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test/api/data/tree/test-files/contacts-only-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { "type": "Contact", "referenceId": "FrontDeskRef" }, 5 | "lastname": "Washington", 6 | "title": "President", 7 | "AccountId": "@SampleAccountRef" 8 | }, 9 | { 10 | "attributes": { "type": "Contact", "referenceId": "ManagerRef" }, 11 | "lastname": "Woods", 12 | "title": "Vice President", 13 | "AccountId": "@SampleAcct2Ref" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/api/data/tree/test-files/contacts-only-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { "type": "Contact", "referenceId": "JanitorRef" }, 5 | "lastname": "Williams", 6 | "title": "President" 7 | }, 8 | { 9 | "attributes": { "type": "Contact", "referenceId": "DeveloperRef" }, 10 | "lastname": "Davenport", 11 | "title": "Vice President" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/api/data/tree/test-files/contacts-only-2.sdx: -------------------------------------------------------------------------------- 1 | { 2 | "records" : [{ 3 | "attributes" : {"type" : "Contact", "referenceId" : "JanitorRef"}, 4 | "lastname" : "Williams", 5 | "title" : "President" 6 | },{ 7 | "attributes" : {"type" : "Contact", "referenceId" : "DeveloperRef"}, 8 | "lastname" : "Davenport", 9 | "title" : "Vice President" 10 | }] 11 | } 12 | -------------------------------------------------------------------------------- /test/api/data/tree/test-files/travel-expense.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Travel_Expense__c", 6 | "referenceId": "Travel_Expense__cRef1" 7 | }, 8 | "RecordType": { 9 | "attributes": { 10 | "type": "RecordType", 11 | "url": "/services/data/v62.0/sobjects/RecordType/012D6000004PyzjIAC" 12 | }, 13 | "Name": "food" 14 | }, 15 | "Name": "lunch" 16 | }, 17 | { 18 | "attributes": { 19 | "type": "Travel_Expense__c", 20 | "referenceId": "Travel_Expense__cRef2" 21 | }, 22 | "RecordType": { 23 | "attributes": { 24 | "type": "RecordType", 25 | "url": "/services/data/v62.0/sobjects/RecordType/012D6000004PyztIAC" 26 | }, 27 | "Name": "flights" 28 | }, 29 | "Name": "BOI-STX" 30 | }, 31 | { 32 | "attributes": { 33 | "type": "Travel_Expense__c", 34 | "referenceId": "Travel_Expense__cRef3" 35 | }, 36 | "RecordType": { 37 | "attributes": { 38 | "type": "RecordType", 39 | "url": "/services/data/v62.0/sobjects/RecordType/012D6000004PyzjIAC" 40 | }, 41 | "Name": "food" 42 | }, 43 | "Name": "dinner" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /test/bulkUtils.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { expect } from 'chai'; 9 | 10 | import { detectDelimiter } from '../src/bulkUtils.js'; 11 | 12 | describe('bulkUtils', () => { 13 | describe('csv', () => { 14 | it('detects column separator', async () => { 15 | expect(await detectDelimiter('./test/test-files/csv/backquote.csv')).to.equal('BACKQUOTE'); 16 | expect(await detectDelimiter('./test/test-files/csv/caret.csv')).to.equal('CARET'); 17 | expect(await detectDelimiter('./test/test-files/csv/comma.csv')).to.equal('COMMA'); 18 | expect(await detectDelimiter('./test/test-files/csv/single-column.csv')).to.equal('COMMA'); 19 | expect(await detectDelimiter('./test/test-files/csv/comma_wrapped_values.csv')).to.equal('COMMA'); 20 | expect(await detectDelimiter('./test/test-files/csv/pipe.csv')).to.equal('PIPE'); 21 | expect(await detectDelimiter('./test/test-files/csv/semicolon.csv')).to.equal('SEMICOLON'); 22 | expect(await detectDelimiter('./test/test-files/csv/tab.csv')).to.equal('TAB'); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/commands/data/create/file.nut.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import fs from 'node:fs'; 8 | import path from 'node:path'; 9 | import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; 10 | import { expect } from 'chai'; 11 | import type { SaveResult } from '@jsforce/jsforce-node'; 12 | import { SoqlQueryResult } from '../../../../src/types.js'; 13 | import { ContentVersion } from '../../../../src/api/file/fileToContentVersion.js'; 14 | 15 | describe('data create file NUTs', () => { 16 | const filename = 'hi.txt'; 17 | let session: TestSession; 18 | let acctId: string | undefined; 19 | before(async () => { 20 | session = await TestSession.create({ 21 | project: { name: 'dataCreateFile' }, 22 | scratchOrgs: [{ setDefault: true, edition: 'developer' }], 23 | devhubAuthStrategy: 'AUTO', 24 | }); 25 | // create one record in the org that we'll use to attach stuff to 26 | acctId = execCmd('data:create:record -s Account -v "Name=TestAccount" --json', { 27 | ensureExitCode: 0, 28 | cli: 'sf', 29 | }).jsonOutput?.result.id; 30 | expect(acctId).to.be.a('string'); 31 | // make a file we can upload 32 | await fs.promises.writeFile(path.join(session.project.dir, filename), 'hi'); 33 | }); 34 | 35 | after(async () => { 36 | await session?.clean(); 37 | }); 38 | 39 | it('basic file upload', () => { 40 | const command = `data:create:file --file ${filename} --json`; 41 | const output = execCmd(command, { ensureExitCode: 0 }).jsonOutput?.result; 42 | expect(output?.ContentDocumentId) 43 | .to.be.a('string') 44 | .match(/069\w{15}/); 45 | expect(output?.Id) 46 | .to.be.a('string') 47 | .match(/068\w{15}/); 48 | expect(output?.FileExtension).to.equal('txt'); 49 | }); 50 | 51 | it('file upload + attach with filename', () => { 52 | const newName = 'newName.txt'; 53 | const command = `data:create:file --file ${filename} --parent-id ${acctId} --title ${newName} --json`; 54 | const output = execCmd(command, { ensureExitCode: 0 }).jsonOutput?.result; 55 | expect(output?.ContentDocumentId) 56 | .to.be.a('string') 57 | .match(/069\w{15}/); 58 | expect(output?.Id) 59 | .to.be.a('string') 60 | .match(/068\w{15}/); 61 | expect(output?.Title).to.equal(newName); 62 | 63 | // make sure the file is attached to the record 64 | const query = `SELECT Id FROM ContentDocumentLink WHERE LinkedEntityId='${acctId}' AND ContentDocumentId='${output?.ContentDocumentId}'`; 65 | const result = execCmd(`data:query -q "${query}" --json`, { ensureExitCode: 0 }) 66 | .jsonOutput?.result; 67 | expect(result?.totalSize).to.equal(1); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/commands/data/create/record.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { resolve } from 'node:path'; 8 | import { dirname } from 'node:path'; 9 | import { fileURLToPath } from 'node:url'; 10 | import { Config } from '@oclif/core/config'; 11 | import { expect } from 'chai'; 12 | import { TestContext, MockTestOrgData } from '@salesforce/core/testSetup'; 13 | import { AnyJson, ensureJsonMap, ensureString } from '@salesforce/ts-types'; 14 | import type { SaveResult } from '@jsforce/jsforce-node'; 15 | import Create from '../../../../src/commands/data/create/record.js'; 16 | const sObjectId = '0011100001zhhyUAAQ'; 17 | 18 | describe('data:create:record', () => { 19 | const $$ = new TestContext(); 20 | const testOrg = new MockTestOrgData(); 21 | const config = new Config({ 22 | root: resolve(dirname(fileURLToPath(import.meta.url)), '../../../package.json'), 23 | }); 24 | 25 | beforeEach(async () => { 26 | await $$.stubAuths(testOrg); 27 | await config.load(); 28 | $$.fakeConnectionRequest = (request: AnyJson): Promise => { 29 | const requestWithUrl = ensureJsonMap(request); 30 | if (request && ensureString(requestWithUrl.url).includes('Account')) { 31 | return Promise.resolve({ 32 | id: sObjectId, 33 | success: true, 34 | errors: [], 35 | }); 36 | } 37 | throw new Error('Unexpected request: ' + JSON.stringify(request)); 38 | }; 39 | }); 40 | 41 | it('should create a new sobject', async () => { 42 | const cmd = new Create( 43 | ['--target-org', 'test@org.com', '--sobject', 'Account', '-v', '"Name=Acme"', '--json'], 44 | config 45 | ); 46 | 47 | const result = await cmd.run(); 48 | expect(result.id).to.equal(sObjectId); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/commands/data/dataCommand.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { expect } from 'chai'; 9 | 10 | import { stringToDictionary } from '../../../src/dataUtils.js'; 11 | 12 | describe('dataCommand', () => { 13 | it('should transform a "key=value" string into an equivalent object', () => { 14 | const dict = stringToDictionary('key=value'); 15 | 16 | expect(dict).to.deep.equal({ 17 | key: 'value', 18 | }); 19 | }); 20 | 21 | it('should transform a "key=leftvalue rightvalue" string into an equivalent object', () => { 22 | const dict = stringToDictionary('key="leftvalue rightvalue"'); 23 | 24 | expect(dict).to.deep.equal({ 25 | key: 'leftvalue rightvalue', 26 | }); 27 | }); 28 | 29 | it('should transform a "key1=value key2=value" string into an equivalent object', () => { 30 | const dict = stringToDictionary('key1=value key2=value'); 31 | 32 | expect(dict).to.deep.equal({ 33 | key1: 'value', 34 | key2: 'value', 35 | }); 36 | }); 37 | 38 | it('should transform a "key1=value key2=leftvalue rightvalue" string into an equivalent object', () => { 39 | const dict = stringToDictionary('key1=value key2="leftvalue rightvalue"'); 40 | 41 | expect(dict).to.deep.equal({ 42 | key1: 'value', 43 | key2: 'leftvalue rightvalue', 44 | }); 45 | }); 46 | 47 | it('should allow single quotes in key=value pairs', () => { 48 | let dict = stringToDictionary('key="val\'ue"'); 49 | 50 | expect(dict).to.deep.equal({ 51 | key: "val'ue", 52 | }); 53 | 54 | dict = stringToDictionary("key=val'ue"); 55 | 56 | expect(dict).to.deep.equal({ 57 | key: "val'ue", 58 | }); 59 | }); 60 | 61 | it('should allow double quotes in key=value pairs', () => { 62 | let dict = stringToDictionary("key='val\"ue'"); 63 | 64 | expect(dict).to.deep.equal({ 65 | key: 'val"ue', 66 | }); 67 | 68 | dict = stringToDictionary('key=val"ue'); 69 | 70 | expect(dict).to.deep.equal({ 71 | key: 'val"ue', 72 | }); 73 | }); 74 | 75 | it('should allow non alphanumeric characters in key=value pairs', () => { 76 | const dict = stringToDictionary('key=!@#$%^&*()-_=+[{]}\\|;:,<.>/?`~'); 77 | 78 | expect(dict).to.deep.equal({ 79 | key: '!@#$%^&*()-_=+[{]}\\|;:,<.>/?`~', 80 | }); 81 | }); 82 | 83 | it('should allow weird or foreign unicode characters in key=value pairs', () => { 84 | const dict = stringToDictionary('key=♣♦♥♠&£ë╤è☺¼Φ╚↕↓㍿々'); 85 | 86 | expect(dict).to.deep.equal({ 87 | key: '♣♦♥♠&£ë╤è☺¼Φ╚↕↓㍿々', 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/commands/data/get/record.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { resolve } from 'node:path'; 8 | import { strict as assert } from 'node:assert'; 9 | import { dirname } from 'node:path'; 10 | import { fileURLToPath } from 'node:url'; 11 | import { Messages } from '@salesforce/core'; 12 | import { ensureJsonMap, ensureString, AnyJson } from '@salesforce/ts-types'; 13 | import { expect } from 'chai'; 14 | import { TestContext, MockTestOrgData, shouldThrow } from '@salesforce/core/testSetup'; 15 | 16 | import { Config } from '@oclif/core/config'; 17 | import Get from '../../../../src/commands/data/get/record.js'; 18 | 19 | const sObjectId = '0011100001zhhyUAAQ'; 20 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 21 | const messages = Messages.loadMessages('@salesforce/plugin-data', 'messages'); 22 | 23 | describe('data:get:record', () => { 24 | const $$ = new TestContext(); 25 | const testOrg = new MockTestOrgData(); 26 | const config = new Config({ 27 | root: resolve(dirname(fileURLToPath(import.meta.url)), '../../../package.json'), 28 | }); 29 | 30 | beforeEach(async () => { 31 | await $$.stubAuths(testOrg); 32 | await config.load(); 33 | $$.fakeConnectionRequest = (request: AnyJson): Promise => { 34 | const requestMap = ensureJsonMap(request); 35 | if (ensureString(requestMap.url).includes('Account')) { 36 | return Promise.resolve({ 37 | attributes: { 38 | type: 'Account', 39 | url: `/services/data/v50.0/sobjects/Account/${sObjectId}`, 40 | }, 41 | Id: sObjectId, 42 | IsDeleted: false, 43 | Name: 'Acme', 44 | PhotoUrl: `/services/images/photo/0011100001zhhyUAAQ${sObjectId}`, 45 | OwnerId: '00511000009LARJAA4', 46 | CreatedDate: '2020-11-17T21:47:42.000+0000', 47 | CreatedById: '00511000009LARJAA4', 48 | LastModifiedDate: '2020-11-17T21:47:42.000+0000', 49 | LastModifiedById: '00511000009LARJAA4', 50 | SystemModstamp: '2020-11-17T21:47:42.000+0000', 51 | LastViewedDate: '2020-11-17T21:47:42.000+0000', 52 | LastReferencedDate: '2020-11-17T21:47:42.000+0000', 53 | }); 54 | } 55 | return Promise.resolve({}); 56 | }; 57 | }); 58 | 59 | afterEach(async () => { 60 | $$.restore(); 61 | }); 62 | 63 | it('returns record for provided record-id', async () => { 64 | const cmd = new Get( 65 | ['--target-org', 'test@org.com', '--sobject', 'Account', '--record-id', sObjectId, '--json'], 66 | config 67 | ); 68 | 69 | const result = await cmd.run(); 70 | 71 | expect(result.Id).to.equal('0011100001zhhyUAAQ'); 72 | }); 73 | 74 | it('should throw an error if values provided to where flag are invalid', async () => { 75 | const cmd = new Get( 76 | ['--target-org', 'test@org.com', '--sobject', 'Account', '--where', '"Name"', '--json'], 77 | config 78 | ); 79 | try { 80 | await shouldThrow(cmd.run()); 81 | } catch (e) { 82 | assert(e instanceof Error); 83 | expect(e.message).to.equal(messages.getMessage('TextUtilMalformedKeyValuePair', ['Name'])); 84 | } 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/commands/data/tree/dataTreeCommonChild.nut.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import path from 'node:path'; 8 | import { expect } from 'chai'; 9 | import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; 10 | import { ImportResult } from '../../../../src/api/data/tree/importTypes.js'; 11 | import { QueryResult } from '../query/query.nut.js'; 12 | 13 | describe('data:tree commands with a polymorphic whatId (on tasks) shared between multiple parents', () => { 14 | let testSession: TestSession; 15 | const importAlias = 'commonChild'; 16 | const prefix = 'CC'; 17 | 18 | before(async () => { 19 | testSession = await TestSession.create({ 20 | scratchOrgs: [ 21 | { 22 | config: 'config/project-scratch-def.json', 23 | setDefault: true, 24 | }, 25 | { 26 | config: 'config/project-scratch-def.json', 27 | setDefault: false, 28 | alias: importAlias, 29 | }, 30 | ], 31 | project: { sourceDir: path.join('test', 'test-files', 'data-project') }, 32 | devhubAuthStrategy: 'AUTO', 33 | }); 34 | }); 35 | 36 | after(async () => { 37 | await testSession?.clean(); 38 | }); 39 | 40 | it('import -> export -> import round trip should succeed', () => { 41 | const query = 42 | "SELECT Id, Name, (SELECT Id, Name, StageName, CloseDate, (SELECT Id, Subject FROM Tasks) FROM Opportunities), (SELECT Id, Subject, Status, (SELECT Id, Subject FROM Tasks) FROM Cases) FROM Account where name != 'Sample Account for Entitlements'"; 43 | 44 | // Import data to the default org. 45 | execCmd( 46 | `data:import:tree --plan ${path.join( 47 | '.', 48 | 'data', 49 | 'commonChild', 50 | 'Account-Opportunity-Task-Case-plan.json' 51 | )} --json`, 52 | { 53 | ensureExitCode: 0, 54 | } 55 | ); 56 | 57 | execCmd( 58 | `data:export:tree --query "${query}" --prefix ${prefix} --output-dir ${path.join( 59 | '.', 60 | 'export_data' 61 | )} --plan --json`, 62 | { ensureExitCode: 0 } 63 | ); 64 | 65 | // Import data to the 2nd org org. 66 | execCmd( 67 | `data:import:tree --target-org ${importAlias} --plan ${path.join( 68 | '.', 69 | 'export_data', 70 | `${prefix}-Account-Opportunity-Task-Case-plan.json` 71 | )} --json`, 72 | { 73 | ensureExitCode: 0, 74 | } 75 | ); 76 | 77 | // query the new org for import verification 78 | const queryResults = execCmd(`data:query --target-org ${importAlias} --query "${query}" --json`, { 79 | ensureExitCode: 0, 80 | }).jsonOutput; 81 | 82 | expect(queryResults?.result.totalSize).to.equal( 83 | 2, 84 | `Expected 2 Account objects returned by the query to org: ${importAlias}` 85 | ); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/commands/data/tree/dataTreeMissingRef.nut.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import path from 'node:path'; 8 | import { expect } from 'chai'; 9 | import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; 10 | import { ImportResult } from '../../../../src/api/data/tree/importTypes.js'; 11 | 12 | describe('data:tree beta commands with a missing reference', () => { 13 | let testSession: TestSession; 14 | 15 | before(async () => { 16 | testSession = await TestSession.create({ 17 | scratchOrgs: [ 18 | { 19 | config: 'config/project-scratch-def.json', 20 | setDefault: true, 21 | }, 22 | ], 23 | project: { sourceDir: path.join('test', 'test-files', 'data-project') }, 24 | devhubAuthStrategy: 'AUTO', 25 | }); 26 | }); 27 | 28 | after(async () => { 29 | await testSession?.clean(); 30 | }); 31 | 32 | it('import breaks recursion and fails with good error when a ref is missing', () => { 33 | const failResult = execCmd( 34 | `data:import:tree --plan ${path.join('.', 'data', 'missingRef', 'Account-Opportunity-plan.json')} --json`, 35 | { 36 | ensureExitCode: 'nonZero', 37 | } 38 | ); 39 | expect(failResult.jsonOutput?.name).to.equal('UnresolvableRefsError'); 40 | expect(failResult.jsonOutput?.message).to.include('@AccountRef2000'); // includes the missing ref 41 | expect(failResult.jsonOutput?.message).to.includes('Opportunity.json'); // includes the filename where the ref is 42 | expect(failResult.jsonOutput?.data).to.have.length(2); // data contains results that have already succeeded in import 43 | }); 44 | 45 | it('import breaks recursion and fails with good error when a ref is missing', () => { 46 | const failResult = execCmd( 47 | `data:import:tree --plan ${path.join('.', 'data', 'missingSelfRef', 'Account-plan.json')} --json`, 48 | { 49 | ensureExitCode: 'nonZero', 50 | } 51 | ); 52 | expect(failResult.jsonOutput?.name).to.equal('UnresolvableRefsError'); 53 | expect(failResult.jsonOutput?.message).to.include('@AccountRef2000'); // includes the missing ref 54 | expect(failResult.jsonOutput?.message).to.includes('Account.json'); // includes the filename where the ref is 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/commands/data/tree/dataTreeMoreThan200.nut.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import path from 'node:path'; 8 | import { expect } from 'chai'; 9 | import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; 10 | import { ImportResult } from '../../../../src/api/data/tree/importTypes.js'; 11 | import { QueryResult } from '../query/query.nut.js'; 12 | 13 | describe('data:tree commands with more than 200 records are batches in safe groups', () => { 14 | let testSession: TestSession; 15 | const importAlias = 'importOrgMoreThan200'; 16 | const prefix = '200'; 17 | 18 | before(async () => { 19 | testSession = await TestSession.create({ 20 | scratchOrgs: [ 21 | { 22 | config: 'config/project-scratch-def.json', 23 | setDefault: true, 24 | }, 25 | { 26 | config: 'config/project-scratch-def.json', 27 | setDefault: false, 28 | alias: importAlias, 29 | }, 30 | ], 31 | project: { sourceDir: path.join('test', 'test-files', 'data-project') }, 32 | devhubAuthStrategy: 'AUTO', 33 | }); 34 | }); 35 | 36 | after(async () => { 37 | await testSession?.clean(); 38 | }); 39 | 40 | it('import -> export -> import round trip should succeed', () => { 41 | const query = "SELECT Id, Name, ParentId FROM Account where name != 'Sample Account for Entitlements'"; 42 | 43 | // Import data to the default org. 44 | const importResult = execCmd( 45 | `data:import:tree --plan ${path.join('.', 'data', 'moreThan200', 'Account-plan.json')} --json`, 46 | { 47 | ensureExitCode: 0, 48 | } 49 | ); 50 | expect(importResult.jsonOutput?.result.length).to.equal(10_000, 'Expected 10000 records to be imported'); 51 | 52 | execCmd( 53 | `data:export:tree --query "${query}" --prefix ${prefix} --output-dir ${path.join( 54 | '.', 55 | 'export_data' 56 | )} --plan --json`, 57 | { ensureExitCode: 0 } 58 | ); 59 | 60 | // Import data to the 2nd org org. 61 | execCmd( 62 | `data:import:tree --target-org ${importAlias} --plan ${path.join( 63 | '.', 64 | 'export_data', 65 | `${prefix}-Account-plan.json` 66 | )} --json`, 67 | { 68 | ensureExitCode: 0, 69 | } 70 | ); 71 | 72 | // query the new org for import verification 73 | const queryResults = execCmd(`data:query --target-org ${importAlias} --query "${query}" --json`, { 74 | ensureExitCode: 0, 75 | }).jsonOutput; 76 | 77 | expect(queryResults?.result.totalSize).to.equal( 78 | 10_000, 79 | `Expected 10000 Account objects returned by the query to org: ${importAlias}` 80 | ); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/commands/data/tree/dataTreeSelfReferencing.nut.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import path from 'node:path'; 8 | import { expect } from 'chai'; 9 | import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; 10 | import { QueryResult } from '../query/query.nut.js'; 11 | 12 | describe('data:tree commands with records that refer to other records of the same type in the same file', () => { 13 | let testSession: TestSession; 14 | 15 | before(async () => { 16 | testSession = await TestSession.create({ 17 | scratchOrgs: [ 18 | { 19 | config: 'config/project-scratch-def.json', 20 | setDefault: true, 21 | }, 22 | { 23 | config: 'config/project-scratch-def.json', 24 | setDefault: false, 25 | alias: 'importOrg', 26 | }, 27 | ], 28 | project: { sourceDir: path.join('test', 'test-files', 'data-project') }, 29 | devhubAuthStrategy: 'AUTO', 30 | }); 31 | }); 32 | 33 | after(async () => { 34 | await testSession?.clean(); 35 | }); 36 | 37 | it('import -> export -> import round trip should succeed', () => { 38 | // exclude an account that occurs in many scratch orgs 39 | const query = "SELECT Id, Name, ParentId FROM Account where name != 'Sample Account for Entitlements'"; 40 | 41 | // Import data to the default org. 42 | execCmd(`data:import:tree --plan ${path.join('.', 'data', 'self-referencing', 'Account-plan.json')} --json`, { 43 | ensureExitCode: 0, 44 | }); 45 | 46 | execCmd( 47 | `data:export:tree --query "${query}" --prefix INT --output-dir ${path.join('.', 'export_data')} --plan --json`, 48 | { ensureExitCode: 0 } 49 | ); 50 | 51 | // Import data to the 2nd org org. 52 | execCmd( 53 | `data:import:tree --target-org importOrg --plan ${path.join('.', 'export_data', 'INT-Account-plan.json')} --json`, 54 | { 55 | ensureExitCode: 0, 56 | } 57 | ); 58 | 59 | // query the new org for import verification 60 | const queryResults = execCmd(`data:query --target-org importOrg --query "${query}" --json`, { 61 | ensureExitCode: 0, 62 | }).jsonOutput; 63 | 64 | expect(queryResults?.result.totalSize).to.equal( 65 | 12, 66 | 'Expected 12 Account objects returned by the query to org: importOrg' 67 | ); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/commands/data/update/bulk.nut.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import path from 'node:path'; 8 | import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; 9 | import { expect } from 'chai'; 10 | import { Org } from '@salesforce/core'; 11 | import { ensureString } from '@salesforce/ts-types'; 12 | import { generateUpdatedCsv, generateAccountsCsv } from '../../../testUtil.js'; 13 | import { DataUpdateBulkResult } from '../../../../src/commands/data/update/bulk.js'; 14 | import { DataImportBulkResult } from '../../../../src/commands/data/import/bulk.js'; 15 | 16 | describe('data update bulk NUTs', () => { 17 | let session: TestSession; 18 | 19 | before(async () => { 20 | session = await TestSession.create({ 21 | scratchOrgs: [ 22 | { 23 | config: 'config/project-scratch-def.json', 24 | setDefault: true, 25 | }, 26 | ], 27 | project: { sourceDir: path.join('test', 'test-files', 'data-project') }, 28 | devhubAuthStrategy: 'AUTO', 29 | }); 30 | }); 31 | 32 | after(async () => { 33 | await session?.clean(); 34 | }); 35 | 36 | it('should bulk update account records', async () => { 37 | const csvFile = await generateAccountsCsv(session.dir); 38 | 39 | const result = execCmd( 40 | `data import bulk --file ${csvFile} --sobject Account --wait 10 --json`, 41 | { ensureExitCode: 0 } 42 | ).jsonOutput?.result; 43 | 44 | const conn = ( 45 | await Org.create({ 46 | aliasOrUsername: session.orgs.get('default')?.username, 47 | }) 48 | ).getConnection(); 49 | 50 | const importJob = conn.bulk2.job('ingest', { 51 | id: ensureString(result?.jobId), 52 | }); 53 | 54 | const successfulIds = (await importJob.getSuccessfulResults()).map((r) => r.sf__Id); 55 | 56 | const updatedCsv = await generateUpdatedCsv( 57 | csvFile, 58 | successfulIds, 59 | path.join(session.dir, 'data-project', 'updated.csv') 60 | ); 61 | 62 | const dataUpdateResult = execCmd( 63 | `data update bulk --file ${updatedCsv} --sobject account --wait 10 --json`, 64 | { ensureExitCode: 0 } 65 | ).jsonOutput?.result; 66 | 67 | expect(dataUpdateResult?.processedRecords).to.equal(10_000); 68 | expect(dataUpdateResult?.successfulRecords).to.equal(10_000); 69 | expect(dataUpdateResult?.failedRecords).to.equal(0); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/commands/data/update/resume.nut.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import path from 'node:path'; 8 | import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; 9 | import { expect } from 'chai'; 10 | import { Org } from '@salesforce/core'; 11 | import { ensureString } from '@salesforce/ts-types'; 12 | import { generateUpdatedCsv, generateAccountsCsv, validateCacheFile } from '../../../testUtil.js'; 13 | import { DataUpdateBulkResult } from '../../../../src/commands/data/update/bulk.js'; 14 | import { DataImportBulkResult } from '../../../../src/commands/data/import/bulk.js'; 15 | 16 | describe('data update resume NUTs', () => { 17 | let session: TestSession; 18 | 19 | before(async () => { 20 | session = await TestSession.create({ 21 | scratchOrgs: [ 22 | { 23 | config: 'config/project-scratch-def.json', 24 | setDefault: true, 25 | }, 26 | ], 27 | project: { sourceDir: path.join('test', 'test-files', 'data-project') }, 28 | devhubAuthStrategy: 'AUTO', 29 | }); 30 | }); 31 | 32 | after(async () => { 33 | await session?.clean(); 34 | }); 35 | 36 | it('should resume bulk udpate via--use-most-recent', async () => { 37 | const csvFile = await generateAccountsCsv(session.dir); 38 | 39 | const result = execCmd( 40 | `data import bulk --file ${csvFile} --sobject Account --wait 10 --json`, 41 | { ensureExitCode: 0 } 42 | ).jsonOutput?.result; 43 | 44 | const conn = ( 45 | await Org.create({ 46 | aliasOrUsername: session.orgs.get('default')?.username, 47 | }) 48 | ).getConnection(); 49 | 50 | const importJob = conn.bulk2.job('ingest', { 51 | id: ensureString(result?.jobId), 52 | }); 53 | 54 | const successfulIds = (await importJob.getSuccessfulResults()).map((r) => r.sf__Id); 55 | 56 | const updatedCsv = await generateUpdatedCsv( 57 | csvFile, 58 | successfulIds, 59 | path.join(session.dir, 'data-project', 'updated.csv') 60 | ); 61 | 62 | const dataUpdateAsyncRes = execCmd( 63 | `data update bulk --file ${updatedCsv} --sobject account --async --json`, 64 | { ensureExitCode: 0 } 65 | ).jsonOutput?.result; 66 | 67 | expect(dataUpdateAsyncRes?.jobId).to.be.length(18); 68 | 69 | await validateCacheFile( 70 | path.join(session.homeDir, '.sf', 'bulk-data-update-cache.json'), 71 | ensureString(dataUpdateAsyncRes?.jobId) 72 | ); 73 | 74 | const dataUpdateResumeRes = execCmd( 75 | `data update resume -i ${ensureString(dataUpdateAsyncRes?.jobId)} --wait 10 --json`, 76 | { ensureExitCode: 0 } 77 | ).jsonOutput?.result; 78 | 79 | expect(dataUpdateResumeRes?.processedRecords).to.equal(10_000); 80 | expect(dataUpdateResumeRes?.successfulRecords).to.equal(10_000); 81 | expect(dataUpdateResumeRes?.failedRecords).to.equal(0); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/commands/force/data/bulk/upsert.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { dirname, resolve } from 'node:path'; 9 | import { fileURLToPath } from 'node:url'; 10 | import fs from 'node:fs'; 11 | import { SfError } from '@salesforce/core/sfError'; 12 | import { TestContext, MockTestOrgData, shouldThrow } from '@salesforce/core/testSetup'; 13 | import { Config } from '@oclif/core/config'; 14 | import { expect } from 'chai'; 15 | import Upsert from '../../../../../src/commands/force/data/bulk/upsert.js'; 16 | describe('force:data:bulk:upsert', () => { 17 | const $$ = new TestContext(); 18 | const testOrg = new MockTestOrgData(); 19 | let config: Config; 20 | 21 | before(async () => { 22 | config = new Config({ root: resolve(dirname(fileURLToPath(import.meta.url)), '../../../..') }); 23 | await config.load(); 24 | }); 25 | 26 | beforeEach(async () => { 27 | await $$.stubAuths(testOrg); 28 | $$.SANDBOX.stub(fs, 'existsSync').returns(true); 29 | $$.SANDBOX.stub(fs, 'createReadStream').throws(new SfError('Error')); 30 | // @ts-expect-error only stubbing a very small part 31 | $$.SANDBOX.stub(fs.promises, 'stat').resolves({ isFile: () => true }); 32 | }); 33 | 34 | afterEach(async () => { 35 | $$.SANDBOX.restore(); 36 | }); 37 | 38 | it('should fail correctly with error message', async () => { 39 | const cmd = new Upsert( 40 | [ 41 | '--target-org', 42 | 'test@org.com', 43 | '--sobject', 44 | 'custom__c', 45 | '--file', 46 | 'fileToUpsert.csv', 47 | '--externalid', 48 | 'field__c', 49 | '--json', 50 | ], 51 | config 52 | ); 53 | try { 54 | await shouldThrow(cmd.run()); 55 | } catch (err) { 56 | if (!(err instanceof SfError)) { 57 | expect.fail('Expected SfError to be thrown'); 58 | } 59 | expect(err.exitCode).to.equal(1); 60 | // expect(err.commandName).to.equal('Upsert'); 61 | expect(err.message).to.equal('Error'); 62 | } 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/queryFields.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { expect } from 'chai'; 9 | 10 | import sinon from 'sinon'; 11 | import { retrieveColumns } from '../src/commands/data/query.js'; 12 | import { queryFieldsExemplars } from './test-files/queryFields.exemplars.js'; 13 | import { createFakeConnection } from './testUtil.js'; 14 | 15 | describe('queryFields tests', () => { 16 | const fakeConnection = createFakeConnection(); 17 | const sandbox = sinon.createSandbox(); 18 | afterEach(() => { 19 | sandbox.restore(); 20 | }); 21 | 22 | it('should handle a query with just fields', async () => { 23 | sandbox 24 | .stub(fakeConnection, 'request') 25 | .resolves({ columnMetadata: queryFieldsExemplars.simpleQuery.columnMetadata }); 26 | const results = await retrieveColumns(fakeConnection, 'SELECT id, name FROM Contact'); 27 | expect(results).to.be.deep.equal(queryFieldsExemplars.simpleQuery.columns); 28 | }); 29 | it('should handle a query with a subquery with both having just fields', async () => { 30 | sandbox.stub(fakeConnection, 'request').resolves({ columnMetadata: queryFieldsExemplars.subquery.columnMetadata }); 31 | const results = await retrieveColumns( 32 | fakeConnection, 33 | 'SELECT Name, ( SELECT LastName FROM Contacts ) FROM Account' 34 | ); 35 | expect(results).to.be.deep.equal(queryFieldsExemplars.subquery.columns); 36 | }); 37 | it('should handle a query with aggregate fields', async () => { 38 | sandbox 39 | .stub(fakeConnection, 'request') 40 | .resolves({ columnMetadata: queryFieldsExemplars.aggregateQuery.columnMetadata }); 41 | const results = await retrieveColumns( 42 | fakeConnection, 43 | 'SELECT CampaignId, AVG(Amount) FROM Opportunity GROUP BY CampaignId' 44 | ); 45 | expect(results).to.be.deep.equal(queryFieldsExemplars.aggregateQuery.columns); 46 | }); 47 | it('should handle a query with column join', async () => { 48 | sandbox 49 | .stub(fakeConnection, 'request') 50 | .resolves({ columnMetadata: queryFieldsExemplars.queryWithJoin.columnMetadata }); 51 | const results = await retrieveColumns(fakeConnection, 'SELECT Name, Owner.Name FROM Account'); 52 | expect(results).to.be.deep.equal(queryFieldsExemplars.queryWithJoin.columns); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/reporters/csvSearchReporter.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import fs from 'node:fs'; 8 | import { TestContext } from '@salesforce/core/testSetup'; 9 | import { expect } from 'chai'; 10 | import { stubUx } from '@salesforce/sf-plugins-core'; 11 | import { CsvSearchReporter } from '../../src/reporters/search/csvSearchReporter.js'; 12 | 13 | describe('CSV search reporter', () => { 14 | const $$ = new TestContext(); 15 | let commandStubs: ReturnType; 16 | 17 | beforeEach(() => { 18 | commandStubs = stubUx($$.SANDBOX); 19 | }); 20 | 21 | afterEach(() => { 22 | $$.SANDBOX.restore(); 23 | }); 24 | 25 | it('will write two csv files', () => { 26 | const writeFileStub = $$.SANDBOX.stub(fs, 'writeFileSync'); 27 | const reporter = new CsvSearchReporter({ 28 | searchRecords: [ 29 | { 30 | attributes: { 31 | type: 'Account', 32 | url: '/services/data/v61.0/sobjects/Account/0017X00001Av4xPQAR', 33 | }, 34 | Name: 'Jones', 35 | Industry: 'Apparel', 36 | }, 37 | { 38 | attributes: { 39 | type: 'Contact', 40 | url: '/services/data/v61.0/sobjects/Contact/0037X000012i5QzQAI', 41 | }, 42 | FirstName: 'Bry', 43 | LastName: 'Jones', 44 | Department: null, 45 | }, 46 | { 47 | attributes: { 48 | type: 'Contact', 49 | url: '/services/data/v61.0/sobjects/Contact/0037X000012i5QuQAI', 50 | }, 51 | FirstName: 'Bob', 52 | LastName: 'Jones', 53 | Department: null, 54 | }, 55 | ], 56 | }); 57 | 58 | reporter.display(); 59 | 60 | expect(writeFileStub.callCount).to.equal(2); 61 | expect(writeFileStub.firstCall.args[0]).to.equal('Account.csv'); 62 | expect(writeFileStub.firstCall.args[1]).to.include('Name,Industry'); 63 | expect(writeFileStub.firstCall.args[1]).to.include('Jones,Apparel'); 64 | expect(writeFileStub.secondCall.args[0]).to.include('Contact.csv'); 65 | expect(commandStubs.log.callCount).to.equal(2); 66 | }); 67 | it('will not write', () => { 68 | const reporter = new CsvSearchReporter({ 69 | searchRecords: [], 70 | }); 71 | 72 | reporter.display(); 73 | 74 | // two objects, two tables 75 | expect(commandStubs.log.firstCall.firstArg).to.equal('No Records Found'); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/reporters/humanSearchReporter.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { TestContext } from '@salesforce/core/testSetup'; 8 | import { expect } from 'chai'; 9 | import { stubUx } from '@salesforce/sf-plugins-core'; 10 | import { HumanSearchReporter } from '../../src/reporters/search/humanSearchReporter.js'; 11 | 12 | describe('human search reporter', () => { 13 | const $$ = new TestContext(); 14 | let commandStubs: ReturnType; 15 | 16 | beforeEach(() => { 17 | commandStubs = stubUx($$.SANDBOX); 18 | }); 19 | 20 | afterEach(() => { 21 | $$.SANDBOX.restore(); 22 | }); 23 | 24 | it('will write two tables', () => { 25 | const reporter = new HumanSearchReporter({ 26 | searchRecords: [ 27 | { 28 | attributes: { 29 | type: 'Account', 30 | url: '/services/data/v61.0/sobjects/Account/0017X00001Av4xPQAR', 31 | }, 32 | Name: 'Jones', 33 | Industry: 'Apparel', 34 | }, 35 | { 36 | attributes: { 37 | type: 'Contact', 38 | url: '/services/data/v61.0/sobjects/Contact/0037X000012i5QzQAI', 39 | }, 40 | FirstName: 'Bry', 41 | LastName: 'Jones', 42 | Department: null, 43 | }, 44 | { 45 | attributes: { 46 | type: 'Contact', 47 | url: '/services/data/v61.0/sobjects/Contact/0037X000012i5QuQAI', 48 | }, 49 | FirstName: 'Bob', 50 | LastName: 'Jones', 51 | Department: null, 52 | }, 53 | ], 54 | }); 55 | 56 | reporter.display(); 57 | 58 | // two objects, two tables 59 | expect(commandStubs.table.callCount).to.equal(2); 60 | expect(commandStubs.table.firstCall.args[0].data).to.deep.equal([{ Name: 'Jones', Industry: 'Apparel' }]); 61 | expect(commandStubs.table.firstCall.args[0].title).to.equal('Account'); 62 | 63 | expect(commandStubs.table.secondCall.args[0].data).to.deep.equal([ 64 | { FirstName: 'Bry', LastName: 'Jones', Department: null }, 65 | { FirstName: 'Bob', LastName: 'Jones', Department: null }, 66 | ]); 67 | expect(commandStubs.table.secondCall.args[0].title).to.equal('Contact'); 68 | }); 69 | 70 | it('will not print a table for no results', () => { 71 | const reporter = new HumanSearchReporter({ 72 | searchRecords: [], 73 | }); 74 | 75 | reporter.display(); 76 | expect(commandStubs.log.firstCall.firstArg).to.equal('No Records Found'); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/reporters/reporters.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { expect } from 'chai'; 9 | import { renameAggregates } from '../../src/reporters/query/reporters.js'; 10 | import { Field, FieldType } from '../../src/types.js'; 11 | 12 | describe('rename aggregates', () => { 13 | it('has no aggregates', () => { 14 | const aggregates: Field[] = []; 15 | const record = { a: 1, b: 2, c: 3 }; 16 | expect(renameAggregates(aggregates)(record)).to.be.deep.equal(record); 17 | }); 18 | it('has only an non-aliased aggregate', () => { 19 | const aggregates: Field[] = [{ fieldType: FieldType.functionField, name: 'avg(AnnualRevenue)' }]; 20 | const record = { 21 | attributes: { 22 | type: 'AggregateResult', 23 | }, 24 | Name: 'Pyramid Construction Inc.', 25 | expr0: 950_000_000, 26 | }; 27 | expect(renameAggregates(aggregates)(record)).to.be.deep.equal({ 28 | attributes: { 29 | type: 'AggregateResult', 30 | }, 31 | Name: 'Pyramid Construction Inc.', 32 | 'avg(AnnualRevenue)': 950_000_000, 33 | }); 34 | }); 35 | it('has only a aliased aggregate', () => { 36 | const aggregates: Field[] = [{ fieldType: FieldType.functionField, name: 'avg(AnnualRevenue)', alias: 'Avg Rev' }]; 37 | const record = { 38 | attributes: { 39 | type: 'AggregateResult', 40 | }, 41 | Name: 'Pyramid Construction Inc.', 42 | expr0: 950_000_000, 43 | }; 44 | expect(renameAggregates(aggregates)(record)).to.be.deep.equal({ 45 | attributes: { 46 | type: 'AggregateResult', 47 | }, 48 | Name: 'Pyramid Construction Inc.', 49 | 'Avg Rev': 950_000_000, 50 | }); 51 | }); 52 | it('has an aggregate of each type', () => { 53 | const aggregates: Field[] = [ 54 | { fieldType: FieldType.functionField, name: 'sum(AnnualRevenue)', alias: 'Total Rev' }, 55 | { fieldType: FieldType.functionField, name: 'avg(AnnualRevenue)' }, 56 | ]; 57 | const record = { 58 | attributes: { 59 | type: 'AggregateResult', 60 | }, 61 | Name: 'Pyramid Construction Inc.', 62 | expr0: 950_000_000, 63 | expr1: 950_000, 64 | }; 65 | expect(renameAggregates(aggregates)(record)).to.be.deep.equal({ 66 | attributes: { 67 | type: 'AggregateResult', 68 | }, 69 | Name: 'Pyramid Construction Inc.', 70 | 'Total Rev': 950_000_000, 71 | 'avg(AnnualRevenue)': 950_000, 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/test-files/csv/backquote.csv: -------------------------------------------------------------------------------- 1 | NAME`TYPE`PHONE`WEBSITE`ANNUALREVENUE 2 | account Upsert #0`Account`415-555-0000`http://www.accountUpsert0.com`0 3 | account Upsert #1`Account`415-555-0000`http://www.accountUpsert1.com`1000 4 | account Upsert #2`Account`415-555-0000`http://www.accountUpsert2.com`2000 5 | account Upsert #3`Account`415-555-0000`http://www.accountUpsert3.com`3000 6 | account Upsert #4`Account`415-555-0000`http://www.accountUpsert4.com`4000 7 | account Upsert #5`Account`415-555-0000`http://www.accountUpsert5.com`5000 8 | account Upsert #6`Account`415-555-0000`http://www.accountUpsert6.com`6000 9 | account Upsert #7`Account`415-555-0000`http://www.accountUpsert7.com`7000 10 | account Upsert #8`Account`415-555-0000`http://www.accountUpsert8.com`8000 11 | account Upsert #9`Account`415-555-0000`http://www.accountUpsert9.com`9000 12 | -------------------------------------------------------------------------------- /test/test-files/csv/caret.csv: -------------------------------------------------------------------------------- 1 | NAME^TYPE^PHONE^WEBSITE^ANNUALREVENUE 2 | account Upsert #0^Account^415-555-0000^http://www.accountUpsert0.com^0 3 | account Upsert #1^Account^415-555-0000^http://www.accountUpsert1.com^1000 4 | account Upsert #2^Account^415-555-0000^http://www.accountUpsert2.com^2000 5 | account Upsert #3^Account^415-555-0000^http://www.accountUpsert3.com^3000 6 | account Upsert #4^Account^415-555-0000^http://www.accountUpsert4.com^4000 7 | account Upsert #5^Account^415-555-0000^http://www.accountUpsert5.com^5000 8 | account Upsert #6^Account^415-555-0000^http://www.accountUpsert6.com^6000 9 | account Upsert #7^Account^415-555-0000^http://www.accountUpsert7.com^7000 10 | account Upsert #8^Account^415-555-0000^http://www.accountUpsert8.com^8000 11 | account Upsert #9^Account^415-555-0000^http://www.accountUpsert9.com^9000 12 | -------------------------------------------------------------------------------- /test/test-files/csv/comma.csv: -------------------------------------------------------------------------------- 1 | NAME,TYPE,PHONE,WEBSITE,ANNUALREVENUE 2 | account Upsert #0,Account,415-555-0000,http://www.accountUpsert0.com,0 3 | account Upsert #1,Account,415-555-0000,http://www.accountUpsert1.com,1000 4 | account Upsert #2,Account,415-555-0000,http://www.accountUpsert2.com,2000 5 | account Upsert #3,Account,415-555-0000,http://www.accountUpsert3.com,3000 6 | account Upsert #4,Account,415-555-0000,http://www.accountUpsert4.com,4000 7 | account Upsert #5,Account,415-555-0000,http://www.accountUpsert5.com,5000 8 | account Upsert #6,Account,415-555-0000,http://www.accountUpsert6.com,6000 9 | account Upsert #7,Account,415-555-0000,http://www.accountUpsert7.com,7000 10 | account Upsert #8,Account,415-555-0000,http://www.accountUpsert8.com,8000 11 | account Upsert #9,Account,415-555-0000,http://www.accountUpsert9.com,9000 12 | -------------------------------------------------------------------------------- /test/test-files/csv/comma_wrapped_values.csv: -------------------------------------------------------------------------------- 1 | "NAME","TYPE","PHONE","WEBSITE","ANNUALREVENUE" 2 | "account Upsert #0","Account","415-555-0000","http://www.accountUpsert0.com","0" 3 | "account Upsert #1","Account","415-555-0000","http://www.accountUpsert1.com","1000" 4 | "account Upsert #2","Account","415-555-0000","http://www.accountUpsert2.com","2000" 5 | "account Upsert #3","Account","415-555-0000","http://www.accountUpsert3.com","3000" 6 | "account Upsert #4","Account","415-555-0000","http://www.accountUpsert4.com","4000" 7 | "account Upsert #5","Account","415-555-0000","http://www.accountUpsert5.com","5000" 8 | "account Upsert #6","Account","415-555-0000","http://www.accountUpsert6.com","6000" 9 | "account Upsert #7","Account","415-555-0000","http://www.accountUpsert7.com","7000" 10 | "account Upsert #8","Account","415-555-0000","http://www.accountUpsert8.com","8000" 11 | "account Upsert #9","Account","415-555-0000","http://www.accountUpsert9.com","9000" 12 | -------------------------------------------------------------------------------- /test/test-files/csv/pipe.csv: -------------------------------------------------------------------------------- 1 | NAME|TYPE|PHONE|WEBSITE|ANNUALREVENUE 2 | account Upsert #0|Account|415-555-0000|http://www.accountUpsert0.com|0 3 | account Upsert #1|Account|415-555-0000|http://www.accountUpsert1.com|1000 4 | account Upsert #2|Account|415-555-0000|http://www.accountUpsert2.com|2000 5 | account Upsert #3|Account|415-555-0000|http://www.accountUpsert3.com|3000 6 | account Upsert #4|Account|415-555-0000|http://www.accountUpsert4.com|4000 7 | account Upsert #5|Account|415-555-0000|http://www.accountUpsert5.com|5000 8 | account Upsert #6|Account|415-555-0000|http://www.accountUpsert6.com|6000 9 | account Upsert #7|Account|415-555-0000|http://www.accountUpsert7.com|7000 10 | account Upsert #8|Account|415-555-0000|http://www.accountUpsert8.com|8000 11 | account Upsert #9|Account|415-555-0000|http://www.accountUpsert9.com|9000 12 | -------------------------------------------------------------------------------- /test/test-files/csv/semicolon.csv: -------------------------------------------------------------------------------- 1 | NAME;TYPE;PHONE;WEBSITE;ANNUALREVENUE 2 | account Upsert #0;Account;415-555-0000;http://www.accountUpsert0.com;0 3 | account Upsert #1;Account;415-555-0000;http://www.accountUpsert1.com;1000 4 | account Upsert #2;Account;415-555-0000;http://www.accountUpsert2.com;2000 5 | account Upsert #3;Account;415-555-0000;http://www.accountUpsert3.com;3000 6 | account Upsert #4;Account;415-555-0000;http://www.accountUpsert4.com;4000 7 | account Upsert #5;Account;415-555-0000;http://www.accountUpsert5.com;5000 8 | account Upsert #6;Account;415-555-0000;http://www.accountUpsert6.com;6000 9 | account Upsert #7;Account;415-555-0000;http://www.accountUpsert7.com;7000 10 | account Upsert #8;Account;415-555-0000;http://www.accountUpsert8.com;8000 11 | account Upsert #9;Account;415-555-0000;http://www.accountUpsert9.com;9000 12 | -------------------------------------------------------------------------------- /test/test-files/csv/single-column.csv: -------------------------------------------------------------------------------- 1 | NAME 2 | account Upsert #0 3 | account Upsert #1 4 | account Upsert #2 5 | account Upsert #3 6 | account Upsert #4 7 | account Upsert #5 8 | account Upsert #6 9 | account Upsert #7 10 | account Upsert #8 11 | account Upsert #9 12 | -------------------------------------------------------------------------------- /test/test-files/csv/tab.csv: -------------------------------------------------------------------------------- 1 | NAME TYPE PHONE WEBSITE ANNUALREVENUE 2 | account Upsert #0 Account 415-555-0000 http://www.accountUpsert0.com 0 3 | account Upsert #1 Account 415-555-0000 http://www.accountUpsert1.com 1000 4 | account Upsert #2 Account 415-555-0000 http://www.accountUpsert2.com 2000 5 | account Upsert #3 Account 415-555-0000 http://www.accountUpsert3.com 3000 6 | account Upsert #4 Account 415-555-0000 http://www.accountUpsert4.com 4000 7 | account Upsert #5 Account 415-555-0000 http://www.accountUpsert5.com 5000 8 | account Upsert #6 Account 415-555-0000 http://www.accountUpsert6.com 6000 9 | account Upsert #7 Account 415-555-0000 http://www.accountUpsert7.com 7000 10 | account Upsert #8 Account 415-555-0000 http://www.accountUpsert8.com 8000 11 | account Upsert #9 Account 415-555-0000 http://www.accountUpsert9.com 9000 12 | -------------------------------------------------------------------------------- /test/test-files/data-project/config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "peter.hale company", 3 | "edition": "Developer", 4 | "features": ["EnableSetPasswordInApi", "ContactsToMultipleAccounts"], 5 | "settings": { 6 | "lightningExperienceSettings": { 7 | "enableS1DesktopEnabled": true 8 | }, 9 | "mobileSettings": { 10 | "enableS1EncryptedStoragePref2": false 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/accounts-contacts-plan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sobject": "Account", 4 | "saveRefs": false, 5 | "files": ["accounts-only.json"] 6 | }, 7 | { 8 | "sobject": "Contact", 9 | "resolveRefs": false, 10 | "files": ["contacts-only-1.json", "contacts-only-2.json"] 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/accounts-contacts-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { "type": "Account", "referenceId": "SampleAccountRef" }, 5 | "name": "SampleAccount", 6 | "phone": "1234567890", 7 | "website": "www.salesforce.com", 8 | "numberOfEmployees": "100", 9 | "industry": "Banking", 10 | "Contacts": { 11 | "records": [ 12 | { 13 | "attributes": { "type": "Contact", "referenceId": "PresidentSmithRef" }, 14 | "lastname": "Smith", 15 | "title": "President" 16 | }, 17 | { 18 | "attributes": { "type": "Contact", "referenceId": "VPEvansRef" }, 19 | "lastname": "Evans", 20 | "title": "Vice President" 21 | } 22 | ] 23 | } 24 | }, 25 | { 26 | "attributes": { "type": "Account", "referenceId": "SampleAcct2Ref" }, 27 | "name": "SampleAccount2", 28 | "phone": "1234567890", 29 | "website": "www.salesforce2.com", 30 | "numberOfEmployees": "100", 31 | "industry": "Banking" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/accounts-contacts-tree2.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { "type": "Account", "referenceId": "SampleAccountRef" }, 5 | "name": "SampleAccount3", 6 | "phone": "1234567890", 7 | "website": "www.salesforce.com", 8 | "numberOfEmployees": "100", 9 | "industry": "Banking", 10 | "Contacts": { 11 | "records": [ 12 | { 13 | "attributes": { "type": "Contact", "referenceId": "PresidentSmithContactRef" }, 14 | "lastname": "Smith", 15 | "title": "President" 16 | }, 17 | { 18 | "attributes": { "type": "Contact", "referenceId": "VPEvansContactRef" }, 19 | "lastname": "Evans", 20 | "title": "Vice President" 21 | } 22 | ] 23 | } 24 | }, 25 | { 26 | "attributes": { "type": "Account", "referenceId": "SampleAcct2Ref" }, 27 | "name": "SampleAccount4", 28 | "phone": "1234567890", 29 | "website": "www.salesforce2.com", 30 | "numberOfEmployees": "100", 31 | "industry": "Banking" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/accounts-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { "type": "Account", "referenceId": "SampleAccountRef" }, 5 | "name": "SampleAccount", 6 | "phone": "1234567890", 7 | "website": "www.salesforce.com", 8 | "numberOfEmployees": "100", 9 | "industry": "Banking", 10 | "Contacts": { 11 | "records": [ 12 | { 13 | "attributes": { "type": "Contact", "referenceId": "PresidentSmithRef" }, 14 | "lastname": "Smith", 15 | "title": "President" 16 | }, 17 | { 18 | "attributes": { "type": "Contact", "referenceId": "VPEvansRef" }, 19 | "lastname": "Evans", 20 | "title": "Vice President" 21 | } 22 | ] 23 | } 24 | }, 25 | { 26 | "attributes": { "type": "Account", "referenceId": "SampleAcct2Ref" }, 27 | "name": "SampleAccount2", 28 | "phone": "1234567890", 29 | "website": "www.salesforce2.com", 30 | "numberOfEmployees": "100", 31 | "industry": "Banking" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/bulkUpsert.csv: -------------------------------------------------------------------------------- 1 | NAME,TYPE,PHONE,WEBSITE,ANNUALREVENUE 2 | account Upsert #0,Account,415-555-0000,http://www.accountUpsert0.com,0 3 | account Upsert #1,Account,415-555-0000,http://www.accountUpsert1.com,1000 4 | account Upsert #2,Account,415-555-0000,http://www.accountUpsert2.com,2000 5 | account Upsert #3,Account,415-555-0000,http://www.accountUpsert3.com,3000 6 | account Upsert #4,Account,415-555-0000,http://www.accountUpsert4.com,4000 7 | account Upsert #5,Account,415-555-0000,http://www.accountUpsert5.com,5000 8 | account Upsert #6,Account,415-555-0000,http://www.accountUpsert6.com,6000 9 | account Upsert #7,Account,415-555-0000,http://www.accountUpsert7.com,7000 10 | account Upsert #8,Account,415-555-0000,http://www.accountUpsert8.com,8000 11 | account Upsert #9,Account,415-555-0000,http://www.accountUpsert9.com,9000 -------------------------------------------------------------------------------- /test/test-files/data-project/data/bulkUpsertBackquote.csv: -------------------------------------------------------------------------------- 1 | NAME`TYPE`PHONE`WEBSITE`ANNUALREVENUE 2 | account Upsert #0`Account`415-555-0000`http://www.accountUpsert0.com`0 3 | account Upsert #1`Account`415-555-0000`http://www.accountUpsert1.com`1000 4 | account Upsert #2`Account`415-555-0000`http://www.accountUpsert2.com`2000 5 | account Upsert #3`Account`415-555-0000`http://www.accountUpsert3.com`3000 6 | account Upsert #4`Account`415-555-0000`http://www.accountUpsert4.com`4000 7 | account Upsert #5`Account`415-555-0000`http://www.accountUpsert5.com`5000 8 | account Upsert #6`Account`415-555-0000`http://www.accountUpsert6.com`6000 9 | account Upsert #7`Account`415-555-0000`http://www.accountUpsert7.com`7000 10 | account Upsert #8`Account`415-555-0000`http://www.accountUpsert8.com`8000 11 | account Upsert #9`Account`415-555-0000`http://www.accountUpsert9.com`9000 12 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/commonChild/Account-Opportunity-Task-Case-plan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sobject": "Account", 4 | "saveRefs": true, 5 | "resolveRefs": false, 6 | "files": ["Account.json"] 7 | }, 8 | { 9 | "sobject": "Opportunity", 10 | "saveRefs": true, 11 | "resolveRefs": true, 12 | "files": ["Opportunity.json"] 13 | }, 14 | { 15 | "sobject": "Task", 16 | "saveRefs": false, 17 | "resolveRefs": true, 18 | "files": ["Task.json"] 19 | }, 20 | { 21 | "sobject": "Case", 22 | "saveRefs": true, 23 | "resolveRefs": true, 24 | "files": ["Case.json"] 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/commonChild/Account.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Account", 6 | "referenceId": "AccountRef1" 7 | }, 8 | "Name": "Test" 9 | }, 10 | { 11 | "attributes": { 12 | "type": "Account", 13 | "referenceId": "AccountRef2" 14 | }, 15 | "Name": "Other Test Account" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/commonChild/Case.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Case", 6 | "referenceId": "CaseRef1" 7 | }, 8 | "Status": "New", 9 | "AccountId": "@AccountRef1" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/commonChild/Opportunity.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Opportunity", 6 | "referenceId": "OpportunityRef1" 7 | }, 8 | "Name": "Test Oppty", 9 | "StageName": "Qualification", 10 | "CloseDate": "2024-02-29", 11 | "AccountId": "@AccountRef1" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/commonChild/Task.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Task", 6 | "referenceId": "TaskRef1" 7 | }, 8 | "Subject": "Do something", 9 | "WhatId": "@OpportunityRef1" 10 | }, 11 | { 12 | "attributes": { 13 | "type": "Task", 14 | "referenceId": "TaskRef2" 15 | }, 16 | "Subject": "Do a case thing", 17 | "WhatId": "@CaseRef1" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/contacts-only-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { "type": "Contact", "referenceId": "FrontDeskRef" }, 5 | "lastname": "Washington", 6 | "title": "President", 7 | "AccountId": "@SampleAccountRef" 8 | }, 9 | { 10 | "attributes": { "type": "Contact", "referenceId": "ManagerRef" }, 11 | "lastname": "Woods", 12 | "firstname": "Sample", 13 | "title": "Vice President", 14 | "AccountId": "@SampleAcct2Ref" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/contacts-only-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { "type": "Contact", "referenceId": "JanitorRef" }, 5 | "lastname": "Williams", 6 | "title": "President" 7 | }, 8 | { 9 | "attributes": { "type": "Contact", "referenceId": "DeveloperRef" }, 10 | "lastname": "Davenport", 11 | "title": "Vice President" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/contacts-only-2.sdx: -------------------------------------------------------------------------------- 1 | { 2 | "records" : [{ 3 | "attributes" : {"type" : "Contact", "referenceId" : "JanitorRef"}, 4 | "lastname" : "Williams", 5 | "title" : "President" 6 | },{ 7 | "attributes" : {"type" : "Contact", "referenceId" : "DeveloperRef"}, 8 | "lastname" : "Davenport", 9 | "title" : "Vice President" 10 | }] 11 | } 12 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/deep/accounts-contacts-plan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sobject": "Account", 4 | "files": ["accounts-only.json"] 5 | }, 6 | { 7 | "sobject": "Contact", 8 | "files": ["contacts-only-1.json", "contacts-only-2.json"] 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/deep/accounts-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { "type": "Account", "referenceId": "SampleAccountRef" }, 5 | "name": "SampleAccount", 6 | "phone": "1234567890", 7 | "website": "www.salesforce.com", 8 | "numberOfEmployees": "100", 9 | "industry": "Banking", 10 | "Contacts": { 11 | "records": [ 12 | { 13 | "attributes": { "type": "Contact", "referenceId": "PresidentSmithRef" }, 14 | "lastname": "Smith", 15 | "title": "President" 16 | }, 17 | { 18 | "attributes": { "type": "Contact", "referenceId": "VPEvansRef" }, 19 | "lastname": "Evans", 20 | "title": "Vice President" 21 | } 22 | ] 23 | } 24 | }, 25 | { 26 | "attributes": { "type": "Account", "referenceId": "SampleAcct2Ref" }, 27 | "name": "SampleAccount2", 28 | "phone": "1234567890", 29 | "website": "www.salesforce2.com", 30 | "numberOfEmployees": "100", 31 | "industry": "Banking" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/deep/contacts-only-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { "type": "Contact", "referenceId": "FrontDeskRef" }, 5 | "lastname": "Washington", 6 | "title": "President", 7 | "AccountId": "@SampleAccountRef" 8 | }, 9 | { 10 | "attributes": { "type": "Contact", "referenceId": "ManagerRef" }, 11 | "lastname": "Woods", 12 | "title": "Vice President", 13 | "AccountId": "@SampleAcct2Ref" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/deep/contacts-only-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { "type": "Contact", "referenceId": "JanitorRef" }, 5 | "lastname": "Williams", 6 | "title": "President" 7 | }, 8 | { 9 | "attributes": { "type": "Contact", "referenceId": "DeveloperRef" }, 10 | "lastname": "Davenport", 11 | "title": "Vice President" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/junction/Account-AccountContactRelation-Contact-plan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sobject": "Account", 4 | "saveRefs": true, 5 | "resolveRefs": false, 6 | "files": ["Account.json"] 7 | }, 8 | { 9 | "sobject": "AccountContactRelation", 10 | "saveRefs": false, 11 | "resolveRefs": true, 12 | "files": ["AccountContactRelation.json"] 13 | }, 14 | { 15 | "sobject": "Contact", 16 | "saveRefs": true, 17 | "resolveRefs": true, 18 | "files": ["Contact.json"] 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/junction/Account.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Account", 6 | "referenceId": "AccountRef2" 7 | }, 8 | "Name": "Acct 2" 9 | }, 10 | { 11 | "attributes": { 12 | "type": "Account", 13 | "referenceId": "AccountRef3" 14 | }, 15 | "Name": "Acct 2" 16 | }, 17 | { 18 | "attributes": { 19 | "type": "Account", 20 | "referenceId": "AccountRef4" 21 | }, 22 | "Name": "Acct 4" 23 | }, 24 | { 25 | "attributes": { 26 | "type": "Account", 27 | "referenceId": "AccountRef5" 28 | }, 29 | "Name": "We Know Everybody" 30 | }, 31 | { 32 | "attributes": { 33 | "type": "Account", 34 | "referenceId": "AccountRef6" 35 | }, 36 | "Name": "Acct 1" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/junction/AccountContactRelation.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "AccountContactRelation", 6 | "referenceId": "AccountContactRelationRef1" 7 | }, 8 | "ContactId": "@ContactRef3", 9 | "AccountId": "@AccountRef2" 10 | }, 11 | { 12 | "attributes": { 13 | "type": "AccountContactRelation", 14 | "referenceId": "AccountContactRelationRef2" 15 | }, 16 | "ContactId": "@ContactRef2", 17 | "AccountId": "@AccountRef2" 18 | }, 19 | { 20 | "attributes": { 21 | "type": "AccountContactRelation", 22 | "referenceId": "AccountContactRelationRef3" 23 | }, 24 | "ContactId": "@ContactRef2", 25 | "AccountId": "@AccountRef4" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/junction/Contact.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Contact", 6 | "referenceId": "ContactRef1" 7 | }, 8 | "LastName": "Person C", 9 | "AccountId": "@AccountRef5" 10 | }, 11 | { 12 | "attributes": { 13 | "type": "Contact", 14 | "referenceId": "ContactRef2" 15 | }, 16 | "LastName": "Person X", 17 | "AccountId": "@AccountRef5" 18 | }, 19 | { 20 | "attributes": { 21 | "type": "Contact", 22 | "referenceId": "ContactRef3" 23 | }, 24 | "LastName": "Person A", 25 | "AccountId": "@AccountRef5" 26 | }, 27 | { 28 | "attributes": { 29 | "type": "Contact", 30 | "referenceId": "ContactRef4" 31 | }, 32 | "LastName": "Person B", 33 | "AccountId": "@AccountRef5" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/missingRef/Account-Opportunity-plan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sobject": "Account", 4 | "saveRefs": true, 5 | "resolveRefs": false, 6 | "files": ["Account.json"] 7 | }, 8 | { 9 | "sobject": "Opportunity", 10 | "saveRefs": true, 11 | "resolveRefs": true, 12 | "files": ["Opportunity.json"] 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/missingRef/Account.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Account", 6 | "referenceId": "AccountRef1" 7 | }, 8 | "Name": "Test" 9 | }, 10 | { 11 | "attributes": { 12 | "type": "Account", 13 | "referenceId": "AccountRef2" 14 | }, 15 | "Name": "Other Test Account" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/missingRef/Opportunity.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Opportunity", 6 | "referenceId": "OpportunityRef1" 7 | }, 8 | "Name": "Oppty With Bad Ref", 9 | "StageName": "Qualification", 10 | "CloseDate": "2024-02-29", 11 | "AccountId": "@AccountRef2000" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/missingSelfRef/Account-plan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sobject": "Account", 4 | "saveRefs": true, 5 | "resolveRefs": true, 6 | "files": ["Account.json"] 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/missingSelfRef/Account.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Account", 6 | "referenceId": "AccountRef1" 7 | }, 8 | "Name": "Global Media" 9 | }, 10 | { 11 | "attributes": { 12 | "type": "Account", 13 | "referenceId": "AccountRef2" 14 | }, 15 | "Name": "Acme" 16 | }, 17 | { 18 | "attributes": { 19 | "type": "Account", 20 | "referenceId": "AccountRef3" 21 | }, 22 | "Name": "salesforce.com" 23 | }, 24 | { 25 | "attributes": { 26 | "type": "Account", 27 | "referenceId": "AccountRef4" 28 | }, 29 | "Name": "sample" 30 | }, 31 | { 32 | "attributes": { 33 | "type": "Account", 34 | "referenceId": "AccountRef5" 35 | }, 36 | "Name": "Grandchild", 37 | "ParentId": "@AccountRef2000" 38 | }, 39 | { 40 | "attributes": { 41 | "type": "Account", 42 | "referenceId": "AccountRef6" 43 | }, 44 | "Name": "Global Media" 45 | }, 46 | { 47 | "attributes": { 48 | "type": "Account", 49 | "referenceId": "AccountRef7" 50 | }, 51 | "Name": "Acme" 52 | }, 53 | { 54 | "attributes": { 55 | "type": "Account", 56 | "referenceId": "AccountRef8" 57 | }, 58 | "Name": "salesforce.com" 59 | }, 60 | { 61 | "attributes": { 62 | "type": "Account", 63 | "referenceId": "AccountRef9" 64 | }, 65 | "Name": "sample" 66 | }, 67 | { 68 | "attributes": { 69 | "type": "Account", 70 | "referenceId": "AccountRef10" 71 | }, 72 | "Name": "Child", 73 | "ParentId": "@AccountRef11" 74 | }, 75 | { 76 | "attributes": { 77 | "type": "Account", 78 | "referenceId": "AccountRef11" 79 | }, 80 | "Name": "Original" 81 | }, 82 | { 83 | "attributes": { 84 | "type": "Account", 85 | "referenceId": "AccountRef12" 86 | }, 87 | "Name": "Another Sample Account" 88 | } 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/moreThan200/Account-plan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sobject": "Account", 4 | "saveRefs": false, 5 | "resolveRefs": false, 6 | "files": ["Account.json"] 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/self-referencing/Account-plan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sobject": "Account", 4 | "saveRefs": true, 5 | "resolveRefs": true, 6 | "files": ["Account.json"] 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /test/test-files/data-project/data/self-referencing/Account.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Account", 6 | "referenceId": "AccountRef1" 7 | }, 8 | "Name": "Global Media" 9 | }, 10 | { 11 | "attributes": { 12 | "type": "Account", 13 | "referenceId": "AccountRef2" 14 | }, 15 | "Name": "Acme" 16 | }, 17 | { 18 | "attributes": { 19 | "type": "Account", 20 | "referenceId": "AccountRef3" 21 | }, 22 | "Name": "salesforce.com" 23 | }, 24 | { 25 | "attributes": { 26 | "type": "Account", 27 | "referenceId": "AccountRef4" 28 | }, 29 | "Name": "sample" 30 | }, 31 | { 32 | "attributes": { 33 | "type": "Account", 34 | "referenceId": "AccountRef5" 35 | }, 36 | "Name": "Grandchild", 37 | "ParentId": "@AccountRef10" 38 | }, 39 | { 40 | "attributes": { 41 | "type": "Account", 42 | "referenceId": "AccountRef6" 43 | }, 44 | "Name": "Global Media" 45 | }, 46 | { 47 | "attributes": { 48 | "type": "Account", 49 | "referenceId": "AccountRef7" 50 | }, 51 | "Name": "Acme" 52 | }, 53 | { 54 | "attributes": { 55 | "type": "Account", 56 | "referenceId": "AccountRef8" 57 | }, 58 | "Name": "salesforce.com" 59 | }, 60 | { 61 | "attributes": { 62 | "type": "Account", 63 | "referenceId": "AccountRef9" 64 | }, 65 | "Name": "sample" 66 | }, 67 | { 68 | "attributes": { 69 | "type": "Account", 70 | "referenceId": "AccountRef10" 71 | }, 72 | "Name": "Child", 73 | "ParentId": "@AccountRef11" 74 | }, 75 | { 76 | "attributes": { 77 | "type": "Account", 78 | "referenceId": "AccountRef11" 79 | }, 80 | "Name": "Original" 81 | }, 82 | { 83 | "attributes": { 84 | "type": "Account", 85 | "referenceId": "AccountRef12" 86 | }, 87 | "Name": "Another Sample Account" 88 | } 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /test/test-files/data-project/force-app/main/default/classes/MyClass.cls: -------------------------------------------------------------------------------- 1 | public inherited sharing class MyClass { 2 | public MyClass() { 3 | 4 | } 5 | 6 | public String toDO() { 7 | return 'todo'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/test-files/data-project/force-app/main/default/classes/MyClass.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /test/test-files/data-project/force-app/main/default/classes/MyClassTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * This class contains unit tests for validating the behavior of Apex classes 3 | * and triggers. 4 | * 5 | * Unit tests are class methods that verify whether a particular piece 6 | * of code is working properly. Unit test methods take no arguments, 7 | * commit no data to the database, and are flagged with the testMethod 8 | * keyword in the method definition. 9 | * 10 | * All test methods in an org are executed whenever Apex code is deployed 11 | * to a production org to confirm correctness, ensure code 12 | * coverage, and prevent regressions. All Apex classes are 13 | * required to have at least 75% code coverage in order to be deployed 14 | * to a production org. In addition, all triggers must have some code coverage. 15 | * 16 | * The @isTest class annotation indicates this class only contains test 17 | * methods. Classes defined with the @isTest annotation do not count against 18 | * the org size limit for all Apex scripts. 19 | * 20 | * See the Apex Language Reference for more information about Testing and Code Coverage. 21 | */ 22 | @isTest 23 | private class MyClassTest { 24 | 25 | @isTest 26 | static void myUnitTest() { 27 | MyClass mc = new MyClass(); 28 | mc.toDO(); 29 | } 30 | } -------------------------------------------------------------------------------- /test/test-files/data-project/force-app/main/default/classes/MyClassTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /test/test-files/data-project/force-app/main/default/objects/Test_Object__c/Test_Object__c.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Deployed 4 | 5 | Test Objects 6 | added from sfdx plugin 7 | 8 | Text 9 | 10 | 11 | ReadWrite 12 | true 13 | true 14 | true 15 | true 16 | true 17 | true 18 | true 19 | true 20 | -------------------------------------------------------------------------------- /test/test-files/data-project/force-app/main/default/objects/Test_Object__c/fields/Bool__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Checkbox 5 | Bool__c 6 | false 7 | -------------------------------------------------------------------------------- /test/test-files/data-project/force-app/main/default/permissionsets/TestPerm.permissionset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | 6 | true 7 | true 8 | true 9 | true 10 | true 11 | Test_Object__c 12 | true 13 | 14 | 15 | true 16 | true 17 | Test_Object__c.Bool__c 18 | 19 | -------------------------------------------------------------------------------- /test/test-files/data-project/sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app", 5 | "default": true 6 | } 7 | ], 8 | "namespace": "", 9 | "sfdcLoginUrl": "https://login.salesforce.com", 10 | "sourceApiVersion": "51.0" 11 | } 12 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@salesforce/dev-config/tsconfig-test-strict-esm", 3 | "include": ["./**/*.ts"], 4 | "compilerOptions": { 5 | "skipLibCheck": true, 6 | "sourceMap": true, 7 | "baseUrl": "..", 8 | "paths": { 9 | "@salesforce/kit": ["node_modules/@salesforce/kit"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@salesforce/dev-config/tsconfig-strict-esm", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src", 6 | "skipLibCheck": true, 7 | "declaration": false, 8 | "baseUrl": ".", 9 | "paths": { 10 | "@salesforce/kit": ["node_modules/@salesforce/kit"] 11 | } 12 | }, 13 | "include": ["./src/**/*.ts"] 14 | } 15 | --------------------------------------------------------------------------------