├── .eslintignore
├── src
├── index.ts
├── common
│ ├── constants.ts
│ ├── utils
│ │ ├── sfCommandConfig.ts
│ │ ├── queryRunner.ts
│ │ ├── wrapChildprocess.ts
│ │ └── fileUtils.ts
│ ├── comms
│ │ ├── eventBus.ts
│ │ └── processingEvents.ts
│ ├── validationResult.ts
│ ├── reporters
│ │ ├── csvResultsReporter.ts
│ │ ├── humanResultsReporter.ts
│ │ └── resultsReporter.ts
│ ├── migrationPlanLoader.ts
│ ├── planCache.ts
│ ├── jscSfCommandFlags.ts
│ ├── packageManifestDirectory.ts
│ ├── packageManifestBuilder.ts
│ ├── apex-scheduler
│ │ └── stopSingleJobTask.ts
│ └── metadata
│ │ └── toolingApiHelper.ts
├── types
│ ├── sfStandardApiTypes.ts
│ ├── orgManifestGlobalConstants.ts
│ ├── scheduledApexTypes.ts
│ ├── migrationPlanObjectData.ts
│ ├── platformTypes.ts
│ ├── orgManifestOutputSchema.ts
│ └── orgManifestInputSchema.ts
├── garbage-collection
│ ├── packageManifestTypes.ts
│ ├── entityDefinitionHandler.ts
│ ├── entity-handlers
│ │ ├── customObject.ts
│ │ ├── fullNameSingleRecord.ts
│ │ ├── nameEntity.ts
│ │ ├── developerNameEntity.ts
│ │ ├── sobjectBasedDevNameEntity.ts
│ │ ├── outdatedFlowVersions.ts
│ │ ├── dynamicDevNamedEntityRelated.ts
│ │ ├── customField.ts
│ │ ├── approvalProcessDefinition.ts
│ │ ├── layout.ts
│ │ ├── customMetadataRecord.ts
│ │ └── index.ts
│ ├── packageMemberFilter.ts
│ ├── queries.ts
│ ├── customObjects.ts
│ └── packageGarbageTypes.ts
├── field-usage
│ └── fieldUsageTypes.ts
├── release-manifest
│ ├── artifact-deploy-strategies
│ │ └── artifactDeployStrategy.ts
│ ├── artifactDeploySfCommand.ts
│ └── releaseManifestLoader.ts
└── commands
│ └── jsc
│ ├── maintain
│ └── flow-export
│ │ ├── unused.ts
│ │ └── obsolete.ts
│ ├── apex
│ └── schedule
│ │ └── start.ts
│ └── manifest
│ └── validate.ts
├── test
├── data
│ ├── file-utils
│ │ ├── empty-file.yaml
│ │ ├── some-csv-file.csv
│ │ ├── invalid-types.yaml
│ │ ├── invalid.yaml
│ │ └── valid.yaml
│ ├── manifests
│ │ ├── empty-artifacts-invalid.yaml
│ │ ├── minimal.yaml
│ │ ├── invalid.yaml
│ │ ├── invalid-path-no-envs.yaml
│ │ ├── simple-multi-step.yaml
│ │ ├── cron-job-manifest.yaml
│ │ ├── complex-with-global-options.yaml
│ │ ├── with-flags.yaml
│ │ └── complex-with-envs.yaml
│ ├── test-sfdx-project
│ │ ├── jobs
│ │ │ ├── empty-jobs.yaml
│ │ │ ├── invalid-job-config.yaml
│ │ │ ├── scheduled-jobs.yaml
│ │ │ └── updated-scheduled-jobs.yaml
│ │ ├── src
│ │ │ ├── classes
│ │ │ │ ├── TestClassForUnpackagedDeploys.cls
│ │ │ │ ├── TestJob.cls
│ │ │ │ ├── TestSchedulable2.cls
│ │ │ │ ├── TestSchedulable3.cls
│ │ │ │ ├── TestJob.cls-meta.xml
│ │ │ │ ├── TestSchedulable2.cls-meta.xml
│ │ │ │ ├── TestSchedulable3.cls-meta.xml
│ │ │ │ └── TestClassForUnpackagedDeploys.cls-meta.xml
│ │ │ ├── objects
│ │ │ │ └── Account
│ │ │ │ │ └── recordTypes
│ │ │ │ │ ├── Partner.recordType-meta.xml
│ │ │ │ │ └── Customer.recordType-meta.xml
│ │ │ └── flows
│ │ │ │ ├── Active_Test_Flow.flow-meta.xml
│ │ │ │ └── Inactive_Test_Flow.flow-meta.xml
│ │ ├── manifest.yml
│ │ ├── config
│ │ │ └── default-scratch-def.json
│ │ ├── data
│ │ │ ├── plans
│ │ │ │ └── minimal-plan.json
│ │ │ ├── contacts.json
│ │ │ └── accounts.json
│ │ ├── sfdx-project.json
│ │ ├── export-plans
│ │ │ ├── plan-for-empty-bind.yml
│ │ │ ├── plan-with-invalid-bind.yml
│ │ │ └── test-plan.yml
│ │ └── scripts
│ │ │ ├── plain-scratch-setup.sh
│ │ │ └── scratch-setup.sh
│ ├── query-results
│ │ ├── empty-result.json
│ │ └── subscriber-package.json
│ ├── soql
│ │ ├── accounts.sql
│ │ ├── custom-fields.sql
│ │ ├── users.sql
│ │ └── package-members.sql
│ ├── plans
│ │ ├── invalid-plan.yaml
│ │ ├── duplicate-parent-ids.yaml
│ │ ├── unknown-parent-ids.yaml
│ │ ├── metadata-plan.yaml
│ │ ├── test-plan.yaml
│ │ └── complex-plan-all-data.yaml
│ ├── garbage-collection
│ │ ├── package-2.json
│ │ ├── workflow-field-update-defs.json
│ │ ├── all-quick-actions.json
│ │ ├── subscriber-package.json
│ │ ├── package-members
│ │ │ ├── quick-action.json
│ │ │ ├── workflow-field-updates.json
│ │ │ ├── label-and-list-view.json
│ │ │ ├── layouts.json
│ │ │ ├── cmd-records.json
│ │ │ ├── custom-fields.json
│ │ │ ├── workflow-alerts.json
│ │ │ └── mixed-with-package-infos.json
│ │ ├── custom-labels.json
│ │ ├── layouts.json
│ │ ├── label-listview-entity-definitions.json
│ │ ├── packaged-flows.json
│ │ ├── cmd-m01-records.json
│ │ ├── filtered-entity-definitions.json
│ │ ├── workflow-alert-definitions.json
│ │ └── cmd-m00-records.json
│ ├── apex-schedule-service
│ │ ├── cron-trigger-details.json
│ │ ├── schedule-stop-success.json
│ │ ├── schedule-start-success.json
│ │ ├── job-is-already-aborted.json
│ │ ├── is-already-scheduled-error.json
│ │ └── invalid-cron-error.json
│ ├── flow-export
│ │ ├── obsolete-flows.json
│ │ └── unused-flows.json
│ ├── api
│ │ └── queryResults.ts
│ └── describes
│ │ └── mockDescribeResults.ts
├── tsconfig.json
├── .eslintrc.cjs
├── common
│ ├── planCache.test.ts
│ ├── packageManifestBuilder.test.ts
│ ├── migrationPlan.test.ts
│ ├── describeApi.test.ts
│ └── toolingApiConnection.test.ts
├── mock-utils
│ ├── sfQueryApiMocks.ts
│ ├── apexSchedulerMocks.ts
│ ├── garbageCollectionMocks.ts
│ └── flowExportTestContext.ts
└── commands
│ └── jsc
│ └── manifest
│ └── rollout.nut.ts
├── .husky
├── pre-push
└── pre-commit
├── .prettierrc.json
├── bin
├── run.cmd
├── dev.cmd
├── run.js
└── dev.js
├── commitlint.config.cjs
├── .lintstagedrc.cjs
├── .nycrc
├── .prettierignore
├── messages
├── fileutils.md
├── sobjectanalyser.md
├── exportplan.md
├── jsc.manifest.validate.md
├── orgmanifest.md
├── apexscheduler.md
├── jsc.data.export.md
├── jsc.maintain.flow-export.unused.md
├── jsc.maintain.flow-export.obsolete.md
├── jsc.apex.schedule.export.md
├── jsc.manifest.rollout.md
├── jsc.maintain.common.md
├── jsc.apex.schedule.manage.md
├── garbagecollection.md
├── jsc.maintain.garbage.collect.md
├── jsc.apex.schedule.start.md
└── jsc.apex.schedule.stop.md
├── .mocharc.json
├── .eslintrc.cjs
├── tsconfig.json
├── .vscode
├── extensions.json
├── tasks.json
├── settings.json
└── launch.json
├── .sfdevrc.json
├── .editorconfig
├── .release-it.json
├── .github
├── workflows
│ └── tests.yml
└── FUNDING.yml
└── .gitignore
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.cjs/
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export default {};
2 |
--------------------------------------------------------------------------------
/test/data/file-utils/empty-file.yaml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | yarn build && yarn test
2 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | "@salesforce/prettier-config"
2 |
--------------------------------------------------------------------------------
/bin/run.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | node "%~dp0\run" %*
4 |
--------------------------------------------------------------------------------
/test/data/manifests/empty-artifacts-invalid.yaml:
--------------------------------------------------------------------------------
1 | artifacts:
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | yarn lint && yarn pretty-quick --staged
2 |
--------------------------------------------------------------------------------
/src/common/constants.ts:
--------------------------------------------------------------------------------
1 | export const LOCAL_CACHE_DIR = '.jsc';
2 |
--------------------------------------------------------------------------------
/test/data/file-utils/some-csv-file.csv:
--------------------------------------------------------------------------------
1 | Col1,Col2
2 | "Data 1","Data 2"
--------------------------------------------------------------------------------
/commitlint.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/jobs/empty-jobs.yaml:
--------------------------------------------------------------------------------
1 | options:
2 | stop_other_jobs: true
3 |
--------------------------------------------------------------------------------
/.lintstagedrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | '**/*.{js,json,md}?(x)': () => 'npm run reformat',
3 | };
4 |
--------------------------------------------------------------------------------
/bin/dev.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %*
4 |
--------------------------------------------------------------------------------
/test/data/query-results/empty-result.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [],
3 | "totalSize": 0,
4 | "done": true
5 | }
6 |
--------------------------------------------------------------------------------
/src/common/utils/sfCommandConfig.ts:
--------------------------------------------------------------------------------
1 | export type SfCommandConfig = {
2 | args: string[];
3 | name?: string;
4 | };
5 |
--------------------------------------------------------------------------------
/test/data/soql/accounts.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | Id,
3 | Name,
4 | BillingStreet
5 | FROM
6 | Account
7 | LIMIT
8 | 9500
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/src/classes/TestClassForUnpackagedDeploys.cls:
--------------------------------------------------------------------------------
1 | public class TestClassForUnpackagedDeploys {
2 | }
3 |
--------------------------------------------------------------------------------
/src/common/comms/eventBus.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'node:events';
2 |
3 | export const eventBus = new EventEmitter();
4 |
--------------------------------------------------------------------------------
/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "check-coverage": true,
3 | "lines": 75,
4 | "statements": 75,
5 | "functions": 75,
6 | "branches": 75
7 | }
8 |
--------------------------------------------------------------------------------
/test/data/manifests/minimal.yaml:
--------------------------------------------------------------------------------
1 | artifacts:
2 | basic_happy_soup:
3 | type: Unpackaged
4 | path: test/data/mock-src/unpackaged/my-happy-soup
--------------------------------------------------------------------------------
/test/data/manifests/invalid.yaml:
--------------------------------------------------------------------------------
1 | not_artifacts_key:
2 | basic_happy_soup:
3 | type: Unpackaged
4 | path: test/data/mock-src/unpackaged/my-happy-soup
--------------------------------------------------------------------------------
/src/types/sfStandardApiTypes.ts:
--------------------------------------------------------------------------------
1 | export type QueryError = {
2 | errorCode: string;
3 | name: string;
4 | data: { message: string; errorCode: string };
5 | };
6 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # examples in these messages use wild-cards, that prettier escapes
2 | messages/jsc.apex.schedule.start.md
3 | messages/jsc.maintain.field-usage.analyse.md
4 |
--------------------------------------------------------------------------------
/test/data/soql/custom-fields.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | Id,
3 | DeveloperName,
4 | TableEnumOrId,
5 | ManageableState,
6 | EntityDefinition.DeveloperName
7 | FROM
8 | CustomField
--------------------------------------------------------------------------------
/messages/fileutils.md:
--------------------------------------------------------------------------------
1 | # InvalidFilePath
2 |
3 | Failed to read file from path: %s.
4 |
5 | # InvalidSchema
6 |
7 | Cannot parse file contents to schema. Review these errors: %s
8 |
--------------------------------------------------------------------------------
/test/data/file-utils/invalid-types.yaml:
--------------------------------------------------------------------------------
1 | options:
2 | boolProp: string
3 | stringProp: 1
4 | record:
5 | Entry 1:
6 | prop1: entry 1 prop
7 | prop2: entry 1 prop2
8 |
--------------------------------------------------------------------------------
/test/data/file-utils/invalid.yaml:
--------------------------------------------------------------------------------
1 | invalidProperty:
2 | boolProp: true
3 | stringProp: test
4 | record:
5 | Entry 1:
6 | prop1: entry 1 prop
7 | prop2: entry 1 prop2
8 |
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": ["tsx"],
3 | "extension": ["ts"],
4 | "watch-extensions": "ts",
5 | "recursive": true,
6 | "reporter": "spec",
7 | "timeout": 600000
8 | }
9 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['eslint-config-salesforce-typescript', 'plugin:sf-plugin/recommended'],
3 | root: true,
4 | rules: {
5 | header: 'off',
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@salesforce/dev-config/tsconfig-test-strict-esm",
3 | "include": ["./**/*.ts"],
4 | "compilerOptions": {
5 | "skipLibCheck": true
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/jobs/invalid-job-config.yaml:
--------------------------------------------------------------------------------
1 | options:
2 | stop_other_jobs: false
3 | jobs:
4 | 'Testing Job':
5 | class: SomeInvalidApexClass
6 | expression: '0 0 1 * * ?'
7 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/src/classes/TestJob.cls:
--------------------------------------------------------------------------------
1 | public class TestJob implements Schedulable {
2 | public void execute(SchedulableContext sc) {
3 | System.debug('Does nothing');
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@salesforce/dev-config/tsconfig-strict-esm",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src"
6 | },
7 | "include": ["./src/**/*.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "esbenp.prettier-vscode",
4 | "hbenl.vscode-mocha-test-adapter",
5 | "mhutchie.git-graph",
6 | "GitHub.vscode-github-actions"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.sfdevrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "test": {
3 | "testsPath": "test/**/*.test.ts"
4 | },
5 | "wireit": {
6 | "test": {
7 | "dependencies": ["test:compile", "test:only", "lint"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/manifest.yml:
--------------------------------------------------------------------------------
1 | artifacts:
2 | apex_utils:
3 | type: UnlockedPackage
4 | package_id: 0Ho6f000000TN1jCAG
5 | version: 0.2.0
6 | unpackaged:
7 | type: Unpackaged
8 | path: src
9 |
--------------------------------------------------------------------------------
/test/data/plans/invalid-plan.yaml:
--------------------------------------------------------------------------------
1 | name: Invalid Test Plan
2 | objects:
3 | # account does not have query or file
4 | - objectName: Account
5 | - objectName: Contact
6 | queryString: SELECT Id,Invalid__x FROM Contact
7 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/src/classes/TestSchedulable2.cls:
--------------------------------------------------------------------------------
1 | public class TestSchedulable2 implements Schedulable {
2 | public void execute(SchedulableContext sc) {
3 | System.debug('Does nothing');
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/src/classes/TestSchedulable3.cls:
--------------------------------------------------------------------------------
1 | public class TestSchedulable3 implements Schedulable {
2 | public void execute(SchedulableContext sc) {
3 | System.debug('Does nothing');
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/test/data/soql/users.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | Id,
3 | IsActive,
4 | FirstName,
5 | LastName,
6 | Username,
7 | Email,
8 | UserRole.Name,
9 | Profile.Name
10 | FROM
11 | User
12 | WHERE
13 | CompanyName = 'The Mobility House LLC'
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/config/default-scratch-def.json:
--------------------------------------------------------------------------------
1 | {
2 | "orgName": "JSC - CLI Plugin Scratch Org",
3 | "edition": "Enterprise",
4 | "features": [],
5 | "country": "US",
6 | "language": "en_US",
7 | "settings": {}
8 | }
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/src/classes/TestJob.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 61.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/src/classes/TestSchedulable2.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 61.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/src/classes/TestSchedulable3.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 61.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/test/data/manifests/invalid-path-no-envs.yaml:
--------------------------------------------------------------------------------
1 | environments:
2 | prod: test@example.com
3 | artifacts:
4 | basic_happy_soup:
5 | type: Unpackaged
6 | path:
7 | staging: test/data/mock-src/unpackaged/qa
8 | prod: test/data/mock-src/unpackaged/prod-only
--------------------------------------------------------------------------------
/test/data/soql/package-members.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | Id,
3 | MaxPackageVersion.Name,
4 | SubscriberPackage.Name,
5 | SubjectId,
6 | SubjectKeyPrefix
7 | FROM
8 | Package2Member
9 | WHERE
10 | MaxPackageVersionId != NULL
11 | ORDER BY
12 | SubjectKeyPrefix
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/src/classes/TestClassForUnpackagedDeploys.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 61.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/src/objects/Account/recordTypes/Partner.recordType-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Partner
4 | true
5 |
6 |
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/src/objects/Account/recordTypes/Customer.recordType-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Customer
4 | true
5 |
6 |
7 |
--------------------------------------------------------------------------------
/test/data/plans/duplicate-parent-ids.yaml:
--------------------------------------------------------------------------------
1 | name: Duplicate Exports
2 | objects:
3 | - objectName: Account
4 | queryString: SELECT Id FROM Account
5 | exports:
6 | Id: accIds
7 | - objectName: Contact
8 | query:
9 | fetchAllFields: true
10 | exports:
11 | AccountId: accIds
12 |
--------------------------------------------------------------------------------
/test/data/plans/unknown-parent-ids.yaml:
--------------------------------------------------------------------------------
1 | name: Invalid Parent Binds
2 | objects:
3 | - objectName: Account
4 | query:
5 | fetchAllFields: true
6 | - objectName: Contact
7 | query:
8 | fetchAllFields: true
9 | bind:
10 | variable: myAccountIds
11 | field: AccountId
12 |
--------------------------------------------------------------------------------
/messages/sobjectanalyser.md:
--------------------------------------------------------------------------------
1 | # info.not-a-custom-field
2 |
3 | --custom-fields-only specified
4 |
5 | # info.is-calculated
6 |
7 | --exclude-formulas specified
8 |
9 | # info.type-not-supported
10 |
11 | Type not supported for analysis
12 |
13 | # info.not-filterable
14 |
15 | Field not filterable in SOQL
16 |
--------------------------------------------------------------------------------
/test/data/file-utils/valid.yaml:
--------------------------------------------------------------------------------
1 | options:
2 | boolProp: true
3 | stringProp: test
4 | record:
5 | Entry 1:
6 | prop1: entry 1 prop
7 | prop2: entry 1 prop2
8 | Entry 2:
9 | prop1: entry 2 prop
10 | prop2: entry 2 prop2
11 | Entry 3:
12 | prop1: entry 3 prop
13 | prop2: entry 3 prop2
14 |
--------------------------------------------------------------------------------
/src/types/orgManifestGlobalConstants.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export const DeployStrategies = z.enum(['SourceDeploy', 'PackageInstall', 'CronJobSchedule']);
4 | export const ArtifactTypes = z.enum(['UnlockedPackage', 'Unpackaged', 'CronJob']);
5 | export const DeployStatus = z.enum(['Pending', 'Resolved', 'Skipped', 'Success', 'Failed']);
6 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/data/plans/minimal-plan.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "sobject": "Account",
4 | "saveRefs": true,
5 | "resolveRefs": false,
6 | "files": ["../accounts.json"]
7 | },
8 | {
9 | "sobject": "Contact",
10 | "saveRefs": true,
11 | "resolveRefs": true,
12 | "files": ["../contacts.json"]
13 | }
14 | ]
15 |
--------------------------------------------------------------------------------
/test/data/manifests/simple-multi-step.yaml:
--------------------------------------------------------------------------------
1 | artifacts:
2 | basic_happy_soup:
3 | type: Unpackaged
4 | path: test/data/mock-src/unpackaged/my-happy-soup
5 | apex_utils:
6 | type: UnlockedPackage
7 | package_id: 0Ho690000000000AAA
8 | installation_key: APEX_UTILS_INSTALLATION_KEY
9 | version: 1.28.0
10 | skip_if_installed: false
--------------------------------------------------------------------------------
/test/data/manifests/cron-job-manifest.yaml:
--------------------------------------------------------------------------------
1 | artifacts:
2 | scheduled_jobs:
3 | type: CronJob
4 | jobs:
5 | Test Job 1:
6 | class: TestJob
7 | expression: 0 0 0 1 * * *
8 | Test Job 2:
9 | class: TestSchedulable2
10 | expression: 0 0 1 ? * * *
11 | Test Job 3:
12 | class: TestSchedulable2
13 | expression: 0 0 2 ? * * *
14 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/package-2.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Package2",
6 | "url": "/services/data/v62.0/tooling/sobjects/Package2/0Ho6f000000TN1eCAG"
7 | },
8 | "Id": "0Ho6f000000TN1eCAG",
9 | "SubscriberPackageId": "0330X0000000000AAA"
10 | }
11 | ],
12 | "totalSize": 1,
13 | "done": true
14 | }
15 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/sfdx-project.json:
--------------------------------------------------------------------------------
1 | {
2 | "packageDirectories": [
3 | {
4 | "path": "src",
5 | "default": true
6 | }
7 | ],
8 | "namespace": "",
9 | "sfdcLoginUrl": "https://tmh.my.salesforce.com",
10 | "sourceApiVersion": "61.0",
11 | "packageAliases": {
12 | "Test Package @ 0.1.0": "04tPl0000006rJNIAY",
13 | "Test Package @ LATEST": "04tPl0000006s5lIAA"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/common/validationResult.ts:
--------------------------------------------------------------------------------
1 | export default class ValidationResult {
2 | public issues: ValidationIssue[] = [];
3 | public infos: string[] = [];
4 |
5 | public isValid(): boolean {
6 | return this.issues.length === 0;
7 | }
8 | }
9 |
10 | export type ValidationIssue = {
11 | issueType: ValidationIssueType;
12 | message: string;
13 | };
14 |
15 | export enum ValidationIssueType {
16 | 'generic',
17 | }
18 |
--------------------------------------------------------------------------------
/test/data/plans/metadata-plan.yaml:
--------------------------------------------------------------------------------
1 | name: Metadata Test Plan
2 | objects:
3 | - objectName: Package2Member
4 | isToolingObject: true
5 | queryFile: test/data/soql/package-members.sql
6 | - objectName: CustomObject
7 | isToolingObject: true
8 | queryString: SELECT Id,DeveloperName FROM CustomObject
9 | - objectName: CustomField
10 | isToolingObject: true
11 | queryFile: test/data/soql/custom-fields.sql
--------------------------------------------------------------------------------
/messages/exportplan.md:
--------------------------------------------------------------------------------
1 | # TooManyQueriesDefined
2 |
3 | More than one query provided. queryString OR queryFile or queryObject are allowed.
4 |
5 | # invalid-query-syntax
6 |
7 | Invalid query syntax: %s.
8 |
9 | # NoQueryDefinedForSObject
10 |
11 | No query defined for: %s
12 |
13 | # InvalidSObjectName
14 |
15 | Failed to fetch describe for %s: %s
16 |
17 | # InvalidRecordTypeId
18 |
19 | Failed to fetch metadata for record type id: %s. Does not exist.
20 |
--------------------------------------------------------------------------------
/test/data/manifests/complex-with-global-options.yaml:
--------------------------------------------------------------------------------
1 | artifacts:
2 | apex_utils:
3 | type: UnlockedPackage
4 | package_id: 0Ho690000000000AAA
5 | installation_key: APEX_UTILS_INSTALLATION_KEY
6 | version: 1.28.0
7 | skip_if_installed: true
8 | lwc_utils:
9 | type: UnlockedPackage
10 | package_id: 0Ho690000000001AAA
11 | installation_key: LWC_UTILS_INSTALLATION_KEY
12 | version: 0.12.0
13 | options:
14 | skip_if_installed: false
--------------------------------------------------------------------------------
/test/data/garbage-collection/workflow-field-update-defs.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "WorkflowFieldUpdate",
6 | "url": "/services/data/v63.0/tooling/sobjects/WorkflowFieldUpdate/04Y0X0000000gb0UAA"
7 | },
8 | "Id": "04Y0X0000000gb0UAA",
9 | "Name": "Set some test value",
10 | "FullName": "Account.My_Test_Field_Update"
11 | }
12 | ],
13 | "totalSize": 1,
14 | "done": true
15 | }
16 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "problemMatcher": "$tsc",
4 | "tasks": [
5 | {
6 | "label": "Compile tests",
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", "pretest"],
18 | "isBackground": false
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/jobs/scheduled-jobs.yaml:
--------------------------------------------------------------------------------
1 | options:
2 | stop_other_jobs: true
3 | jobs:
4 | 'Name of my job':
5 | class: TestJob
6 | expression: '0 0 1 * * ?'
7 | 'My job 2':
8 | class: TestJob
9 | expression: '0 0 2 * * ?'
10 | Yet another job:
11 | class: TestSchedulable2
12 | expression: '0 0 1 * * ?'
13 | or_name_job_like_this:
14 | class: TestSchedulable2
15 | expression: '0 0 1 * * ?'
16 | TestSchedulable3:
17 | expression: '0 0 1 * * ?'
18 |
--------------------------------------------------------------------------------
/messages/jsc.manifest.validate.md:
--------------------------------------------------------------------------------
1 | # summary
2 |
3 | Validate a manifest file. Same result as running "rollout" with "--validate-only".
4 |
5 | # description
6 |
7 | The manifest file is validated against a DevHub and Target Org. It tries to resolve package versions and deploy paths for all artifacts, but does not attempt to rollout the artifacts to the target org.
8 |
9 | All artifacts are returned as RESOLVED, if validation succeeds.
10 |
11 | # examples
12 |
13 | - <%= config.bin %> <%= command.id %>
14 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/all-quick-actions.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "QuickActionDefinition",
6 | "url": "/services/data/v62.0/tooling/sobjects/QuickActionDefinition/09D0X000002XMPUUA4"
7 | },
8 | "Id": "09D0X000002XMPUUA4",
9 | "DeveloperName": "New_ChargePilot_Contract",
10 | "EntityDefinitionId": "Account",
11 | "SobjectType": "Account"
12 | }
13 | ],
14 | "totalSize": 1,
15 | "done": true
16 | }
17 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/release-it/schema/release-it.json",
3 | "github": {
4 | "release": true,
5 | "releaseName": "${version}"
6 | },
7 | "git": {
8 | "commitMessage": "admin: release v${version}",
9 | "push": true,
10 | "tag": true
11 | },
12 | "npm": {
13 | "publish": true
14 | },
15 | "plugins": {
16 | "@release-it/conventional-changelog": {
17 | "preset": {
18 | "name": "angular"
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/test/data/query-results/subscriber-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "SubscriberPackage",
6 | "url": "/services/data/v63.0/tooling/sobjects/SubscriberPackage/0336f000000G8roAAC"
7 | },
8 | "Id": "0336f000000G8roAAC",
9 | "Name": "JS Apex Utils",
10 | "Description": "Awesome Apex and Visualforce utilities",
11 | "IsPackageValid": true,
12 | "NamespacePrefix": null
13 | }
14 | ],
15 | "totalSize": 1,
16 | "done": true
17 | }
18 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/subscriber-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "SubscriberPackage",
6 | "url": "/services/data/v63.0/tooling/sobjects/SubscriberPackage/0330X0000000000AAA"
7 | },
8 | "Id": "0330X0000000000AAA",
9 | "Name": "My Test Package",
10 | "Description": "Awesome Apex and Visualforce utilities",
11 | "IsPackageValid": true,
12 | "NamespacePrefix": null
13 | }
14 | ],
15 | "totalSize": 1,
16 | "done": true
17 | }
18 |
--------------------------------------------------------------------------------
/test/data/plans/test-plan.yaml:
--------------------------------------------------------------------------------
1 | name: Test Plan
2 | objects:
3 | - objectName: Account
4 | queryFile: test/data/soql/accounts.sql
5 | exports:
6 | Id: myAccountIds
7 | - objectName: Contact
8 | query:
9 | fetchAllFields: true
10 | bind:
11 | field: AccountId
12 | variable: myAccountIds
13 | - objectName: Order
14 | queryString: SELECT Id,AccountId,BillToContactId FROM Order LIMIT 100
15 | - objectName: Opportunity
16 | queryString: SELECT Id,AccountId FROM Opportunity LIMIT 10
17 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/jobs/updated-scheduled-jobs.yaml:
--------------------------------------------------------------------------------
1 | options:
2 | stop_other_jobs: false
3 | jobs:
4 | 'Name of my job':
5 | class: TestJob
6 | expression: '0 0 1 * * ?'
7 | 'My job 2':
8 | class: TestJob
9 | expression: '0 0 2 * * ?'
10 | Yet another job:
11 | class: TestSchedulable2
12 | expression: '0 0 1 * * ?'
13 | or_name_job_like_this:
14 | class: TestSchedulable2
15 | expression: '0 0 2 * * ?'
16 | TestSchedulable3:
17 | expression: '0 0 2 * * ?'
18 | TestJob:
19 | expression: '0 0 3 * * ?'
20 |
--------------------------------------------------------------------------------
/.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 | "editor.defaultFormatter": "esbenp.prettier-vscode",
16 | "mochaExplorer.files": "test/**/*.test.ts",
17 | "mochaExplorer.require": "tsx",
18 | "github-actions.workflows.pinned.workflows": [".github/workflows/tests.yml"]
19 | }
20 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/export-plans/plan-for-empty-bind.yml:
--------------------------------------------------------------------------------
1 | name: Plan With Empty Binds
2 | objects:
3 | - objectName: User
4 | queryString: SELECT Id FROM User LIMIT 0
5 | exports:
6 | Id: emptyOwnerIds
7 | - objectName: Account
8 | query:
9 | fetchAllFields: false
10 | bind:
11 | field: OwnerId
12 | variable: emptyOwnerIds
13 | exports:
14 | Id: emptyAccountIds
15 | - objectName: Contact
16 | query:
17 | fetchAllFields: false
18 | bind:
19 | field: AccountId
20 | variable: emptyAccountIds
21 |
--------------------------------------------------------------------------------
/test/data/manifests/with-flags.yaml:
--------------------------------------------------------------------------------
1 | environments:
2 | dev: admin-salesforce@mobilityhouse.com.dev
3 | qa: admin@example.com.qa
4 | prod: admin@example.com
5 | artifacts:
6 | org_shape_settings:
7 | type: Unpackaged
8 | path: test/data/mock-src/unpackaged/org-shape
9 | flags: ignore-conflicts concise
10 | apex_utils:
11 | type: UnlockedPackage
12 | package_id: 0Ho690000000000AAA
13 | installation_key: APEX_UTILS_INSTALLATION_KEY
14 | version: 1.28.0
15 | skip_if_installed: false
16 | flags: upgrade-type=DeprecateOnly wait=20 apex-compile=package
17 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/export-plans/plan-with-invalid-bind.yml:
--------------------------------------------------------------------------------
1 | name: Plan With Invalid Binds
2 | objects:
3 | - objectName: User
4 | queryString: SELECT Id FROM User LIMIT 0
5 | exports:
6 | Id: emptyOwnerIds
7 | - objectName: Account
8 | query:
9 | fetchAllFields: false
10 | bind:
11 | field: OwnerId
12 | variable: emptyOwnerIds
13 | exports:
14 | Id: emptyAccountIds
15 | - objectName: Contact
16 | query:
17 | fetchAllFields: false
18 | bind:
19 | field: InvalidParentId__c
20 | variable: emptyAccountIds
21 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/package-members/quick-action.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Package2Member",
6 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v0X0000004CxiQAE"
7 | },
8 | "Id": "03v0X0000004CxiQAE",
9 | "CurrentPackageVersionId": "04t670000015YcsAAE",
10 | "MaxPackageVersionId": "04t670000015YcsAAE",
11 | "SubjectId": "09D0X000002XMPUUA4",
12 | "SubjectKeyPrefix": "09D",
13 | "SubjectManageableState": "deprecatedEditable"
14 | }
15 | ],
16 | "totalSize": 1,
17 | "done": true
18 | }
19 |
--------------------------------------------------------------------------------
/src/common/comms/processingEvents.ts:
--------------------------------------------------------------------------------
1 | export enum ProcessingStatus {
2 | Started,
3 | InProgress,
4 | Completed,
5 | }
6 |
7 | export type CommandStatusEvent = {
8 | status: ProcessingStatus;
9 | message?: string;
10 | exitCode?: number;
11 | exitDetails?: unknown;
12 | };
13 |
14 | export type PlanObjectEvent = CommandStatusEvent & {
15 | objectName: string;
16 | totalBatches: number;
17 | batchesCompleted: number;
18 | totalRecords: number;
19 | files: string[];
20 | };
21 |
22 | export type PlanObjectValidationEvent = CommandStatusEvent & {
23 | objectName: string;
24 | planName: string;
25 | };
26 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/package-members/workflow-field-updates.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Package2Member",
6 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v0X0000004D6kQAE"
7 | },
8 | "Id": "03v0X0000004D6kQAE",
9 | "CurrentPackageVersionId": "04t4P0000000002AAA",
10 | "MaxPackageVersionId": "04t4P0000000002AAA",
11 | "SubjectId": "04Y0X0000000gb0UAA",
12 | "SubjectKeyPrefix": "04Y",
13 | "SubjectManageableState": "deprecatedEditable"
14 | }
15 | ],
16 | "totalSize": 1,
17 | "done": true
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 | on:
3 | push:
4 | branches-ignore: [main]
5 | workflow_dispatch:
6 |
7 | jobs:
8 | unit-tests:
9 | uses: salesforcecli/github-workflows/.github/workflows/unitTest.yml@main
10 | nuts:
11 | needs: unit-tests
12 | uses: salesforcecli/github-workflows/.github/workflows/nut.yml@main
13 | secrets:
14 | TESTKIT_HUB_USERNAME: ${{ secrets.TESTKIT_HUB_USERNAME }}
15 | TESTKIT_AUTH_URL: ${{ secrets.TESTKIT_AUTH_URL }}
16 | strategy:
17 | matrix:
18 | os: [ubuntu-latest, windows-latest]
19 | fail-fast: false
20 | with:
21 | os: ${{ matrix.os }}
22 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/custom-labels.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "ExternalString",
6 | "url": "/services/data/v62.0/tooling/sobjects/ExternalString/1014P00000B3laTQAR"
7 | },
8 | "Id": "1014P00000B3laTQAR",
9 | "Name": "Test_Label_1"
10 | },
11 | {
12 | "attributes": {
13 | "type": "ExternalString",
14 | "url": "/services/data/v62.0/tooling/sobjects/ExternalString/1014P00000B3v7KQAR"
15 | },
16 | "Id": "1014P00000B3v7KQAR",
17 | "Name": "Test_Label_2"
18 | }
19 | ],
20 | "totalSize": 2,
21 | "done": true
22 | }
23 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/data/contacts.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Contact",
6 | "referenceId": "ContactRef1"
7 | },
8 | "Salutation": "Mr.",
9 | "FirstName": "William",
10 | "LastName": "Adama",
11 | "Email": "w.adama@galactica.com",
12 | "AccountId": "@AccountRef1"
13 | },
14 | {
15 | "attributes": {
16 | "type": "Contact",
17 | "referenceId": "ContactRef2"
18 | },
19 | "Salutation": "Ms.",
20 | "FirstName": "Laura",
21 | "LastName": "Roslin",
22 | "Email": "l.roslin@colonial-one.com",
23 | "AccountId": "@AccountRef2"
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/garbage-collection/packageManifestTypes.ts:
--------------------------------------------------------------------------------
1 | // copied from https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/common/constants.ts
2 | // all credits to mshanemc
3 | export const XML_DECL = '\n';
4 | export const XML_NS_URL = 'http://soap.sforce.com/2006/04/metadata';
5 | export const XML_NS_KEY = '@_xmlns';
6 | export const XML_COMMENT_PROP_NAME = '#xml__comment';
7 |
8 | export type PackageTypeMembers = {
9 | name: string;
10 | members: string[];
11 | };
12 |
13 | export type PackageManifestObject = {
14 | Package: {
15 | types: PackageTypeMembers[];
16 | version: string;
17 | fullName?: string;
18 | [XML_NS_KEY]?: string;
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/test/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: '../.eslintrc.cjs',
3 | // Allow describe and it
4 | env: { mocha: true },
5 | rules: {
6 | // Allow assert style expressions. i.e. expect(true).to.be.true
7 | 'no-unused-expressions': 'off',
8 |
9 | // It is common for tests to stub out method.
10 |
11 | // Return types are defined by the source code. Allows for quick overwrites.
12 | '@typescript-eslint/explicit-function-return-type': 'off',
13 | // Mocked out the methods that shouldn't do anything in the tests.
14 | '@typescript-eslint/no-empty-function': 'off',
15 | // Easily return a promise in a mocked method.
16 | '@typescript-eslint/require-await': 'off',
17 | header: 'off',
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/export-plans/test-plan.yml:
--------------------------------------------------------------------------------
1 | name: Test Data Export Plan
2 | objects:
3 | - objectName: Account
4 | queryString: SELECT Id,Name FROM Account WHERE Type = 'Civilian Ship'
5 | exports:
6 | Id: testAccIds
7 | - objectName: Contact
8 | query:
9 | fetchAllFields: true
10 | bind:
11 | field: AccountId
12 | variable: testAccIds
13 | exports:
14 | Id: contactIds
15 | - objectName: Order
16 | query:
17 | fetchAllFields: true
18 | bind:
19 | field: AccountId
20 | variable: testAccIds
21 | - objectName: Opportunity
22 | query:
23 | fetchAllFields: true
24 | bind:
25 | field: ContactId
26 | variable: contactIds
27 |
--------------------------------------------------------------------------------
/messages/orgmanifest.md:
--------------------------------------------------------------------------------
1 | # errors.no-env-configured-with-strict-validation
2 |
3 | No environment configured for target org %s, but strict validation was set.
4 |
5 | # errors.no-released-package-version
6 |
7 | No released version found for package id %s and version %s on devhub %s.
8 |
9 | # errors.package-requires-install-key
10 |
11 | The package version %s (%s) requires an installation key, but no key export was specified for the artifact. Specify the environment variable that holds the installation key in the property installation_key.
12 |
13 | # errors.install-key-defined-but-empty
14 |
15 | Installation key specified with %s, but the corresponding environment variable is not set.
16 |
17 | # errors.source-path-is-empty
18 |
19 | Artifact %s specified an empty path: %s
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # -- CLEAN
2 | tmp/
3 | .jsc
4 | exports/
5 | .sf/
6 | .sfdx/
7 |
8 | # use yarn by default, so ignore npm
9 | package-lock.json
10 |
11 | # never checkin npm config
12 | .npmrc
13 |
14 | # debug logs
15 | npm-error.log
16 | yarn-error.log
17 |
18 | # compile source
19 | lib
20 |
21 | # test artifacts
22 | *xunit.xml
23 | *checkstyle.xml
24 | *unitcoverage
25 | .nyc_output
26 | coverage
27 | test_session*
28 |
29 | # generated docs
30 | docs
31 |
32 | # ignore sfdx-trust files
33 | *.tgz
34 | *.sig
35 | package.json.bak.
36 |
37 | npm-shrinkwrap.json
38 | oclif.manifest.json
39 | oclif.lock
40 |
41 | # -- CLEAN ALL
42 | *.tsbuildinfo
43 | .eslintcache
44 | .wireit
45 | node_modules
46 |
47 | # --
48 | # put files here you don't want cleaned with sf-clean
49 |
50 | # os specific files
51 | .DS_Store
52 | .idea
53 |
--------------------------------------------------------------------------------
/src/common/reporters/csvResultsReporter.ts:
--------------------------------------------------------------------------------
1 | import { json2csv } from 'json-2-csv';
2 | import ResultsReporter, { FormattingOptions } from './resultsReporter.js';
3 |
4 | export type CsvFormattingOptions = FormattingOptions & {
5 | /**
6 | * Render a title for the table
7 | */
8 | title?: string;
9 | /**
10 | * Explicitly remove columns from display.
11 | */
12 | excludeColumns?: string[];
13 | };
14 |
15 | /**
16 | * Prints input data to the standard UX table.
17 | */
18 | export default class CsvResultsReporter> extends ResultsReporter {
19 | public constructor(public data: T[], public options?: CsvFormattingOptions) {
20 | super(data, options);
21 | }
22 |
23 | public print(): void {
24 | const csvOutput = json2csv(this.data);
25 | this.ux.log(csvOutput);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/messages/apexscheduler.md:
--------------------------------------------------------------------------------
1 | # GenericCompileFail
2 |
3 | Failed to compile the code to schedule apex class. Diagnostic details: %s.
4 |
5 | # Unexpected
6 |
7 | An unexpected error happened: %s
8 |
9 | # UnableToParseJobId
10 |
11 | Unable to retrieve Job Id from result.
12 |
13 | # SystemAsyncException
14 |
15 | %s
16 |
17 | # FailedToRetrieveJobDetails
18 |
19 | Unexpected error happend when trying to retrieve details about job %s. Please check Setup > Scheduled Jobs and try again
20 |
21 | # InvalidCronExpression
22 |
23 | Failed to parse the cron expression "%s": %s
24 |
25 | # JobAlreadyAborted
26 |
27 | %s
28 |
29 | # JobManagementFailure
30 |
31 | An unexpected error happened when managing jobs. This probably means not all jobs were started or stopped correctly. Verify the results on the org, fix the following error, and try again: %s
32 |
--------------------------------------------------------------------------------
/messages/jsc.data.export.md:
--------------------------------------------------------------------------------
1 | # summary
2 |
3 | Export all data from a plan definition.
4 |
5 | # description
6 |
7 | Takes a plan definition and exports all data from the source org. The created files are
8 | compatible with the "data import tree" command. Lookups are automatically resolved to
9 | referenceIds to retain relationships. This command allows tree exports that are orders
10 | of magnitute more complex than the basic "data export tree".
11 |
12 | # flags.source-org.summary
13 |
14 | The source org from where data is exported.
15 |
16 | # flags.plan.summary
17 |
18 | Path to the plan file that defines the export.
19 |
20 | # flags.output-dir.summary
21 |
22 | Output directory to export all fields.
23 |
24 | # flags.validate-only.summary
25 |
26 | Does not retrieve records. Only validates the plan.
27 |
28 | # examples
29 |
30 | - <%= config.bin %> <%= command.id %>
31 |
--------------------------------------------------------------------------------
/src/garbage-collection/entityDefinitionHandler.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/consistent-type-definitions */
2 | import QueryBuilder from '../common/utils/queryBuilder.js';
3 | import { Package2Member } from '../types/sfToolingApiTypes.js';
4 | import { PackageGarbageContainer } from './packageGarbageTypes.js';
5 |
6 | export interface EntityDefinitionHandler {
7 | resolve(packageMembers: Package2Member[]): Promise;
8 | }
9 |
10 | export function buildSubjectIdFilter(packageMembers: Package2Member[]): string {
11 | return QueryBuilder.buildParamListFilter('Id', extractSubjectIds(packageMembers));
12 | }
13 |
14 | export function extractSubjectIds(packageMembers: Package2Member[]): string[] {
15 | const subjectIds: string[] = [];
16 | packageMembers.forEach((member) => {
17 | subjectIds.push(member.SubjectId);
18 | });
19 | return subjectIds;
20 | }
21 |
--------------------------------------------------------------------------------
/messages/jsc.maintain.flow-export.unused.md:
--------------------------------------------------------------------------------
1 | # summary
2 |
3 | Exports unpackaged unused flows from a target org.
4 |
5 | # description
6 |
7 | Finds versions from completely inactive flows that are not part of a package. The export contains all versions of the inactive flow. This is a complimentary command to the garbage collector, which exclusively analyses packaged flows.
8 |
9 | # flags.target-org.summary
10 |
11 | Target org to analyse.
12 |
13 | # examples
14 |
15 | - Analyse MyTargetOrg and export all unused flow versions to destructiveChanges.xml in tmp.
16 |
17 | <%= config.bin %> <%= command.id %> -o MyTargetOrg --output-dir tmp --output-format DestructiveChangesXML
18 |
19 | - Analyse MyTargetOrg and print a table with all unused flow versions
20 |
21 | <%= config.bin %> <%= command.id %> -o MyTargetOrg
22 |
23 | # success.no-unused-flows-found
24 |
25 | No unused flows found. You're all set.
26 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [j-schreiber]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/src/common/reporters/humanResultsReporter.ts:
--------------------------------------------------------------------------------
1 | import ResultsReporter, { FormattingOptions } from './resultsReporter.js';
2 |
3 | export type HumanFormattingOptions = FormattingOptions & {
4 | /**
5 | * Render a title for the table
6 | */
7 | title?: string;
8 | /**
9 | * Explicitly remove columns from display.
10 | */
11 | excludeColumns?: string[];
12 | };
13 |
14 | /**
15 | * Prints input data to the standard UX table.
16 | */
17 | export default class HumanResultsReporter> extends ResultsReporter {
18 | public constructor(public data: T[], public options?: HumanFormattingOptions) {
19 | super(data, options);
20 | }
21 |
22 | public print(): void {
23 | this.ux.table({
24 | data: this.data,
25 | columns: this.columns,
26 | title: this.options?.title,
27 | titleOptions: { bold: true, underline: true },
28 | });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/messages/jsc.maintain.flow-export.obsolete.md:
--------------------------------------------------------------------------------
1 | # summary
2 |
3 | Exports unpackaged obsolete flows from a target org.
4 |
5 | # description
6 |
7 | Finds and exports inactive (Obsolete or Draft) versions of unpackaged flows. The active version is never included. This is a complimentary command to the garbage collector, which exclusively analyses packaged flows.
8 |
9 | # flags.target-org.summary
10 |
11 | Target org to analyse.
12 |
13 | # examples
14 |
15 | - Analyse MyTargetOrg and export all obsolete flow versions to destructiveChanges.xml in directory tmp/dev-obsolete.
16 |
17 | <%= config.bin %> <%= command.id %> -o MyTargetOrg --output-dir tmp/dev-obsolete --output-format DestructiveChangesXML
18 |
19 | - Analyse MyTargetOrg and print a table with all obsolete flow versions
20 |
21 | <%= config.bin %> <%= command.id %> -o MyTargetOrg
22 |
23 | # success.no-obsolete-versions-found
24 |
25 | No obsolete flow versions found. You're all set.
26 |
--------------------------------------------------------------------------------
/test/data/apex-schedule-service/cron-trigger-details.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "AsyncApexJob",
6 | "url": "/services/data/v62.0/sobjects/AsyncApexJob/7079b00000lpqHLAAY"
7 | },
8 | "Id": "7079b00000lpqHLAAY",
9 | "Status": "Queued",
10 | "CronTrigger": {
11 | "attributes": {
12 | "type": "CronTrigger",
13 | "url": "/services/data/v62.0/sobjects/CronTrigger/08e9b00000KiFENAA3"
14 | },
15 | "CronJobDetail": {
16 | "attributes": {
17 | "type": "CronJobDetail",
18 | "url": "/services/data/v62.0/sobjects/CronJobDetail/08a9b00000L1jvRAAR"
19 | },
20 | "Name": "Job Name 5"
21 | },
22 | "State": "WAITING",
23 | "StartTime": "2025-01-26T13:08:20.000+0000",
24 | "NextFireTime": "2025-01-27T00:05:00.000+0000"
25 | }
26 | }
27 | ],
28 | "totalSize": 1,
29 | "done": true
30 | }
31 |
--------------------------------------------------------------------------------
/src/common/utils/queryRunner.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'node:events';
2 | import { Connection } from '@salesforce/core';
3 | import { Record } from '@jsforce/jsforce-node';
4 | import { CommandStatusEvent, ProcessingStatus } from '../comms/processingEvents.js';
5 |
6 | export default class QueryRunner extends EventEmitter {
7 | public constructor(private readonly orgConnection: Connection | Connection['tooling']) {
8 | super();
9 | }
10 |
11 | public async fetchRecords(queryString: string): Promise {
12 | this.emit('queryProgress', {
13 | message: 'placeholder',
14 | status: ProcessingStatus.Started,
15 | } as CommandStatusEvent);
16 | const queryResult = await this.orgConnection.query(queryString, { autoFetch: true, maxFetch: 50_000 });
17 | this.emit('queryProgress', {
18 | message: 'placeholder',
19 | status: ProcessingStatus.Completed,
20 | } as CommandStatusEvent);
21 | return queryResult.records;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/layouts.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Layout",
6 | "url": "/services/data/v62.0/tooling/sobjects/Layout/00h0X00000XRV9HQAX"
7 | },
8 | "Id": "00h0X00000XRV9HQAX",
9 | "Name": "Service Contract Layout",
10 | "TableEnumOrId": "ServiceContract"
11 | },
12 | {
13 | "attributes": {
14 | "type": "Layout",
15 | "url": "/services/data/v62.0/tooling/sobjects/Layout/00h0X00000Y4ruHQAR"
16 | },
17 | "Id": "00h0X00000Y4ruHQAR",
18 | "Name": "Organization Profile Layout",
19 | "TableEnumOrId": "01I4P000000wXoVUAU"
20 | },
21 | {
22 | "attributes": {
23 | "type": "Layout",
24 | "url": "/services/data/v62.0/tooling/sobjects/Layout/00h0X00000Y4ruhQAB"
25 | },
26 | "Id": "00h0X00000Y4ruhQAB",
27 | "Name": "Price Book Layout",
28 | "TableEnumOrId": "Pricebook2"
29 | }
30 | ],
31 | "totalSize": 3,
32 | "done": true
33 | }
34 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/label-listview-entity-definitions.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "EntityDefinition",
6 | "url": "/services/data/v62.0/tooling/sobjects/EntityDefinition/ExternalString"
7 | },
8 | "Id": "000000000000000AAA",
9 | "DurableId": "ExternalString",
10 | "QualifiedApiName": "ExternalString",
11 | "DeveloperName": "ExternalString",
12 | "MasterLabel": "Custom Label",
13 | "KeyPrefix": "101",
14 | "IsRetrieveable": true
15 | },
16 | {
17 | "attributes": {
18 | "type": "EntityDefinition",
19 | "url": "/services/data/v62.0/tooling/sobjects/EntityDefinition/ListView"
20 | },
21 | "Id": "000000000000000AAA",
22 | "DurableId": "ListView",
23 | "QualifiedApiName": "ListView",
24 | "DeveloperName": "ListView",
25 | "MasterLabel": "List View",
26 | "KeyPrefix": "00B",
27 | "IsRetrieveable": true
28 | }
29 | ],
30 | "totalSize": 3,
31 | "done": true
32 | }
33 |
--------------------------------------------------------------------------------
/src/field-usage/fieldUsageTypes.ts:
--------------------------------------------------------------------------------
1 | import { Optional } from '@jsforce/jsforce-node';
2 |
3 | export type SObjectAnalysisResult = {
4 | /** Map of record types (by developer name) and scoped analysis results */
5 | recordTypes: Record;
6 |
7 | /** Total number of records for the entire sobject */
8 | totalRecords: number;
9 | };
10 |
11 | /**
12 | * Usage stats for a record type. All analysed and skipped fields
13 | * and additional information about the record type.
14 | */
15 | export type FieldUsageTable = {
16 | totalRecords: number;
17 | isActive: boolean;
18 | analysedFields: FieldUsageStats[];
19 | skippedFields: FieldSkippedInfo[];
20 | };
21 |
22 | export type FieldUsageStats = {
23 | name: string;
24 | type: string;
25 | absolutePopulated: number;
26 | percentagePopulated: number;
27 | defaultValue?: Optional;
28 | histories?: number;
29 | lastUpdated?: string;
30 | };
31 |
32 | export type FieldSkippedInfo = {
33 | name: string;
34 | type: string;
35 | reason: string;
36 | };
37 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/data/accounts.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Account",
6 | "referenceId": "AccountRef1"
7 | },
8 | "Name": "Starship Galactica Ltd.",
9 | "Type": "Warship",
10 | "BillingStreet": "Allersberger Str. 8-10",
11 | "BillingPostalCode": "90461",
12 | "BillingCity": "Nürnberg"
13 | },
14 | {
15 | "attributes": {
16 | "type": "Account",
17 | "referenceId": "AccountRef2"
18 | },
19 | "Name": "Colonial One",
20 | "Type": "Civilian Ship",
21 | "BillingStreet": "Bayerstraße 10",
22 | "BillingPostalCode": "80335",
23 | "BillingCity": "München"
24 | },
25 | {
26 | "attributes": {
27 | "type": "Account",
28 | "referenceId": "AccountRef3"
29 | },
30 | "Name": "Cloud 9 GmbH",
31 | "Type": "Civilian Ship",
32 | "BillingStreet": "Viktoriastraße 7-5",
33 | "BillingPostalCode": "86150",
34 | "BillingCity": "Augsburg"
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/src/common/utils/wrapChildprocess.ts:
--------------------------------------------------------------------------------
1 | import util from 'node:util';
2 | import childProcess from 'node:child_process';
3 | import { SfCommandConfig } from './sfCommandConfig.js';
4 |
5 | const exec = util.promisify(childProcess.exec);
6 |
7 | export default class OclifUtils {
8 | public static async execCoreCommand(conf: SfCommandConfig): Promise {
9 | const commandName = `sf ${conf.name!.split(':').join(' ')} ${[...conf.args, '--json'].join(' ')}`;
10 | try {
11 | // could use stdout and stderr callbacks to bubble events
12 | // up to the parent command for display
13 | const cmdOutput = await exec(commandName);
14 | return { status: 0, result: JSON.parse(cmdOutput.stdout) };
15 | } catch (e) {
16 | if (e instanceof Error && 'stdout' in e) {
17 | return { status: 1, result: JSON.parse(e.stdout as string) };
18 | } else {
19 | return { status: 1, result: e };
20 | }
21 | }
22 | }
23 | }
24 |
25 | export type CommandResult = {
26 | status: number | null;
27 | result: unknown;
28 | };
29 |
--------------------------------------------------------------------------------
/src/release-manifest/artifact-deploy-strategies/artifactDeployStrategy.ts:
--------------------------------------------------------------------------------
1 | import { Connection } from '@salesforce/core';
2 | import { ZArtifactDeployResultType } from '../../types/orgManifestOutputSchema.js';
3 |
4 | export type ArtifactDeployStrategy = {
5 | /**
6 | * Used by resolve API to determine, if the manifest loads at least
7 | * one deploy step that requires to be run from an sfdx project.
8 | */
9 | requiresSfdxProject: boolean;
10 |
11 | /**
12 | * Return the internal state of the job, for inspection
13 | */
14 | getStatus(): Partial;
15 |
16 | /**
17 | * Deploys the artifact step with the current internal state.
18 | * Throws an exception, if called on a step that is not resolved.
19 | */
20 | deploy(): Promise;
21 |
22 | /**
23 | * Prepare internal state of the step before "deploy" is run.
24 | * Requires both orgs connections ?? -> need to check
25 | *
26 | * @param targetOrg
27 | * @param devhubOrg
28 | */
29 | resolve(targetOrg: Connection, devhubOrg: Connection): Promise;
30 | };
31 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/package-members/label-and-list-view.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Package2Member",
6 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v4P0000004JPzQAM"
7 | },
8 | "Id": "03v4P0000004JPzQAM",
9 | "CurrentPackageVersionId": "04t4P0000000003AAA",
10 | "MaxPackageVersionId": "04t4P0000000003AAA",
11 | "SubjectId": "1014P00000B3v7KQAR",
12 | "SubjectKeyPrefix": "101",
13 | "SubjectManageableState": "deprecatedEditable",
14 | "SubscriberPackageId": "0330X0000000000AAA"
15 | },
16 | {
17 | "attributes": {
18 | "type": "Package2Member",
19 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v4P0000004JGmQAM"
20 | },
21 | "Id": "03v4P0000004JGmQAM",
22 | "CurrentPackageVersionId": "04t4P0000000001AAA",
23 | "MaxPackageVersionId": "04t4P0000000001AAA",
24 | "SubjectId": "00B4P000009HFvqUAG",
25 | "SubjectKeyPrefix": "00B",
26 | "SubjectManageableState": "deprecatedEditable",
27 | "SubscriberPackageId": "0330X0000000000AAA"
28 | }
29 | ],
30 | "totalSize": 2,
31 | "done": true
32 | }
33 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/packaged-flows.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Package2Member",
6 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v67000000Go9rAAC"
7 | },
8 | "Id": "03v67000000Go9rAAC",
9 | "CurrentPackageVersionId": "04tWo0000001111AAA",
10 | "SubscriberPackageId": "0330X0000000000AAA",
11 | "MaxPackageVersionId": null,
12 | "MaxPackageVersion": null,
13 | "SubjectId": "3009Q00000EZMYfQAP",
14 | "SubjectKeyPrefix": "300",
15 | "SubjectManageableState": "installedEditable"
16 | },
17 | {
18 | "attributes": {
19 | "type": "Package2Member",
20 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v67000000PBEVAA4"
21 | },
22 | "Id": "03v67000000PBEVAA4",
23 | "CurrentPackageVersionId": "04tbO0000002222AAA",
24 | "SubscriberPackageId": "0330X0000000001AAA",
25 | "MaxPackageVersionId": null,
26 | "MaxPackageVersion": null,
27 | "SubjectId": "3007a000000PIKoAAO",
28 | "SubjectKeyPrefix": "300",
29 | "SubjectManageableState": "installedEditable"
30 | }
31 | ],
32 | "totalSize": 2,
33 | "done": true
34 | }
35 |
--------------------------------------------------------------------------------
/test/data/apex-schedule-service/schedule-stop-success.json:
--------------------------------------------------------------------------------
1 | {
2 | "compiled": true,
3 | "success": true,
4 | "logs": "62.0 APEX_CODE,DEBUG;APEX_PROFILING,INFO\nExecute Anonymous: System.abortJob('08e9b00000L02llAAB');\n13:03:15.23 (23772157)|USER_INFO|[EXTERNAL]|0059b00000EyPaY|test-hm5zkhjbc6hb@example.com|(GMT+01:00) Central European Standard Time (Europe/Berlin)|GMT+01:00\n13:03:15.23 (23792666)|EXECUTION_STARTED\n13:03:15.23 (23800684)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex\n13:03:15.108 (108733192)|CUMULATIVE_LIMIT_USAGE\n13:03:15.108 (108733192)|LIMIT_USAGE_FOR_NS|(default)|\n Number of SOQL queries: 0 out of 100\n Number of query rows: 0 out of 50000\n Number of SOSL queries: 0 out of 20\n Number of DML statements: 1 out of 150\n Number of Publish Immediate DML: 0 out of 150\n Number of DML rows: 1 out of 10000\n Maximum CPU time: 0 out of 10000\n Maximum heap size: 0 out of 6000000\n Number of callouts: 0 out of 100\n Number of Email Invocations: 0 out of 10\n Number of future calls: 0 out of 50\n Number of queueable jobs added to the queue: 0 out of 50\n Number of Mobile Apex push calls: 0 out of 10\n\n13:03:15.108 (108733192)|CUMULATIVE_LIMIT_USAGE_END\n\n13:03:15.23 (108802455)|CODE_UNIT_FINISHED|execute_anonymous_apex\n13:03:15.23 (108822073)|EXECUTION_FINISHED\n"
5 | }
6 |
--------------------------------------------------------------------------------
/src/garbage-collection/entity-handlers/customObject.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { Connection } from '@salesforce/core';
3 | import { Package2Member } from '../../types/sfToolingApiTypes.js';
4 | import { EntityDefinitionHandler } from '../entityDefinitionHandler.js';
5 | import { PackageGarbage, PackageGarbageContainer } from '../packageGarbageTypes.js';
6 | import ToolingApiConnection from '../toolingApiConnection.js';
7 |
8 | export class CustomObject implements EntityDefinitionHandler {
9 | public constructor(private readonly queryConnection: Connection) {}
10 |
11 | public async resolve(packageMembers: Package2Member[]): Promise {
12 | const garbageList: PackageGarbage[] = [];
13 | const objectsByDurableId = await ToolingApiConnection.getInstance(
14 | this.queryConnection
15 | ).fetchObjectDefinitionsByDurableId();
16 | packageMembers.forEach((member) => {
17 | const definition = objectsByDurableId.get(member.SubjectId.substring(0, 15));
18 | if (definition) {
19 | garbageList.push(new PackageGarbage(member, definition.DeveloperName, definition.QualifiedApiName));
20 | }
21 | });
22 | return { metadataType: 'CustomObject', componentCount: garbageList.length, components: garbageList };
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/messages/jsc.apex.schedule.export.md:
--------------------------------------------------------------------------------
1 | # summary
2 |
3 | List all scheduled jobs on the target org.
4 |
5 | # description
6 |
7 | Export all jobs currently scheduled on the target org or specify additional filters to narrow down search results.
8 |
9 | # flags.target-org.summary
10 |
11 | Target org to check.
12 |
13 | # flags.apex-class-name.summary
14 |
15 | Only list jobs from a specific apex class.
16 |
17 | # flags.job-name.summary
18 |
19 | Only list jobs with a specific job name. Supports partial matches.
20 |
21 | # flags.output-dir.summary
22 |
23 | Writes exported jobs to a config file that can be used with "manage" command.
24 |
25 | # flags.concise.summary
26 |
27 | Minimize columns displayed in output table.
28 |
29 | # examples
30 |
31 | - List all jobs on the target org
32 |
33 | <%= config.bin %> <%= command.id %> -o MyTargetOrg
34 |
35 | - List jobs that match apex class and job name
36 |
37 | <%= config.bin %> <%= command.id %> -o MyTargetOrg -c MyScheduledJobClass -n "Scheduled Job Name"
38 |
39 | - List jobs that start with "Auto" and export them to tmp/dev/jobs.yaml
40 |
41 | <%= config.bin %> <%= command.id %> -j "Auto" -d tmp/dev
42 |
43 | # info.wrote-output-to-file
44 |
45 | Successfully wrote export to config file: %s
46 |
47 | # info.no-scheduled-jobs-found
48 |
49 | No scheduled jobs found on org. Nothing to show.
50 |
--------------------------------------------------------------------------------
/src/common/migrationPlanLoader.ts:
--------------------------------------------------------------------------------
1 | import { Connection, SfError } from '@salesforce/core';
2 | import { ZMigrationPlan, ZMigrationPlanType } from '../types/migrationPlanObjectData.js';
3 | import MigrationPlan from './migrationPlan.js';
4 | import { parseYaml } from './utils/fileUtils.js';
5 |
6 | export default class MigrationPlanLoader {
7 | public static async loadPlan(filePath: string, sourcecon: Connection): Promise {
8 | const planData = parseYaml(filePath, ZMigrationPlan);
9 | this.assertVariableExports(planData);
10 | const plan = new MigrationPlan(planData, sourcecon);
11 | await plan.load();
12 | return plan;
13 | }
14 |
15 | private static assertVariableExports(plan: ZMigrationPlanType): void {
16 | const exportedVariables: string[] = [];
17 | plan.objects.forEach((objectDef) => {
18 | if (objectDef.query?.bind) {
19 | if (!exportedVariables.includes(objectDef.query.bind.variable)) {
20 | throw new SfError(
21 | `${objectDef.objectName} references a parent bind that was not defined: ${objectDef.query.bind.variable}`,
22 | 'InvalidPlanFileSyntax'
23 | );
24 | }
25 | }
26 | if (objectDef.exports) {
27 | Object.values(objectDef.exports).forEach((exportedVars) => exportedVariables.push(exportedVars));
28 | }
29 | });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/package-members/layouts.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Package2Member",
6 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v0X0000004CxhQAE"
7 | },
8 | "Id": "03v0X0000004CxhQAE",
9 | "CurrentPackageVersionId": "04t670000015YcsAAE",
10 | "MaxPackageVersionId": "04t670000015YcsAAE",
11 | "SubjectId": "00h0X00000XRV9HQAX",
12 | "SubjectKeyPrefix": "00h"
13 | },
14 | {
15 | "attributes": {
16 | "type": "Package2Member",
17 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v0X0000004Cy0QAE"
18 | },
19 | "Id": "03v0X0000004Cy0QAE",
20 | "CurrentPackageVersionId": "04t670000015YcsAAE",
21 | "MaxPackageVersionId": "04t670000015YcsAAE",
22 | "SubjectId": "00h0X00000Y4ruhQAB",
23 | "SubjectKeyPrefix": "00h"
24 | },
25 | {
26 | "attributes": {
27 | "type": "Package2Member",
28 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v67000000sXv2AAE"
29 | },
30 | "Id": "03v67000000sXv2AAE",
31 | "CurrentPackageVersionId": "04t67000000iE0WAAU",
32 | "MaxPackageVersionId": "04t67000000iE0WAAU",
33 | "SubjectId": "00h0X00000Y4ruHQAR",
34 | "SubjectKeyPrefix": "00h"
35 | }
36 | ],
37 | "totalSize": 2,
38 | "done": true
39 | }
40 |
--------------------------------------------------------------------------------
/src/garbage-collection/packageMemberFilter.ts:
--------------------------------------------------------------------------------
1 | import { EntityDefinition, Package2, Package2Member } from '../types/sfToolingApiTypes.js';
2 |
3 | export default class PackageMemberFilter {
4 | private readonly allowedSubscriberPackages?: string[];
5 | private readonly allowedKeyPrefixes?: string[];
6 |
7 | public constructor(packages: Package2[], entities: EntityDefinition[]) {
8 | if (packages && packages.length > 0) {
9 | this.allowedSubscriberPackages = packages.map((pgk) => pgk.SubscriberPackageId);
10 | }
11 | if (entities && entities.length > 0) {
12 | this.allowedKeyPrefixes = entities.map((entity) => entity.KeyPrefix);
13 | }
14 | }
15 |
16 | public isAllowed(member: Package2Member): boolean {
17 | return this.isAllowedSubscriberPackage(member) && this.isAllowedKeyPrefix(member);
18 | }
19 |
20 | private isAllowedKeyPrefix(member: Package2Member): boolean {
21 | if (!this.allowedKeyPrefixes || this.allowedKeyPrefixes.length === 0) {
22 | return true;
23 | }
24 | return this.allowedKeyPrefixes.includes(member.SubjectKeyPrefix);
25 | }
26 |
27 | private isAllowedSubscriberPackage(member: Package2Member): boolean {
28 | if (!this.allowedSubscriberPackages || this.allowedSubscriberPackages.length === 0) {
29 | return true;
30 | }
31 | return this.allowedSubscriberPackages.includes(member.SubscriberPackageId);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/test/data/plans/complex-plan-all-data.yaml:
--------------------------------------------------------------------------------
1 | name: Complex Test Plan ALL DATA
2 | objects:
3 | - objectName: Account
4 | query:
5 | fetchAllFields: true
6 | - objectName: Contact
7 | query:
8 | fetchAllFields: true
9 | - objectName: Case
10 | query:
11 | fetchAllFields: true
12 | - objectName: Order
13 | query:
14 | fetchAllFields: true
15 | - objectName: OrderItem
16 | query:
17 | fetchAllFields: true
18 | - objectName: Quote
19 | query:
20 | fetchAllFields: true
21 | - objectName: QuoteLineItem
22 | query:
23 | fetchAllFields: true
24 | - objectName: Opportunity
25 | query:
26 | fetchAllFields: true
27 | - objectName: OpportunityLineItem
28 | query:
29 | fetchAllFields: true
30 | - objectName: ServiceContract
31 | query:
32 | fetchAllFields: true
33 | - objectName: ContractLineItem
34 | query:
35 | fetchAllFields: true
36 | - objectName: Site__c
37 | query:
38 | fetchAllFields: true
39 | - objectName: Asset
40 | query:
41 | fetchAllFields: true
42 | - objectName: SiteContact__c
43 | query:
44 | fetchAllFields: true
45 | - objectName: Product2
46 | query:
47 | fetchAllFields: true
48 | - objectName: Pricebook2
49 | query:
50 | fetchAllFields: true
51 | - objectName: PricebookEntry
52 | query:
53 | fetchAllFields: true
54 |
--------------------------------------------------------------------------------
/.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 | "program": "${workspaceFolder}/node_modules/mocha/bin/mocha",
19 | "args": ["--inspect", "--colors", "test/**/*.test.ts"],
20 | "env": {
21 | "NODE_ENV": "development",
22 | "SFDX_ENV": "development"
23 | },
24 | "sourceMaps": true,
25 | "smartStep": true,
26 | "internalConsoleOptions": "openOnSessionStart"
27 | },
28 | {
29 | "type": "node",
30 | "request": "launch",
31 | "name": "Run Current Test",
32 | "program": "${workspaceFolder}/node_modules/mocha/bin/mocha",
33 | "args": ["--inspect", "--colors", "${file}"],
34 | "env": {
35 | "NODE_ENV": "development",
36 | "SFDX_ENV": "development"
37 | },
38 | "sourceMaps": true,
39 | "smartStep": true,
40 | "internalConsoleOptions": "openOnSessionStart"
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/test/data/manifests/complex-with-envs.yaml:
--------------------------------------------------------------------------------
1 | environments:
2 | dev: admin-salesforce@mobilityhouse.com.dev
3 | qa: admin@example.com.qa
4 | prod: admin@example.com
5 | artifacts:
6 | org_shape_settings:
7 | type: Unpackaged
8 | path: test/data/mock-src/unpackaged/org-shape
9 | apex_utils:
10 | type: UnlockedPackage
11 | package_id: 0Ho690000000000AAA
12 | installation_key: APEX_UTILS_INSTALLATION_KEY
13 | version: 1.28.0
14 | skip_if_installed: false
15 | lwc_utils:
16 | type: UnlockedPackage
17 | package_id: 0Ho690000000001AAA
18 | installation_key: LWC_UTILS_INSTALLATION_KEY
19 | version: 0.12.0
20 | core_crm:
21 | type: UnlockedPackage
22 | package_id: 0Ho690000000002AAA
23 | installation_key: CORE_INSTALLATION_KEY
24 | version: 2.4.2
25 | core_crm_overrides:
26 | type: Unpackaged
27 | path:
28 | dev: test/data/mock-src/package-overrides/core-crm/dev
29 | qa: test/data/mock-src/package-overrides/core-crm/dev
30 | prod: test/data/mock-src/package-overrides/core-crm/prod
31 | core_crm_extensions:
32 | type: Unpackaged
33 | path: test/data/mock-src/package-extensions/core-crm
34 | pims:
35 | type: UnlockedPackage
36 | installation_key: PIMS_INSTALLATION_KEY
37 | package_id: 0Ho690000000003AAA
38 | version: 2.9.0
39 | pims_overrides:
40 | type: Unpackaged
41 | path: test/data/mock-src/package-overrides/pims
--------------------------------------------------------------------------------
/messages/jsc.manifest.rollout.md:
--------------------------------------------------------------------------------
1 | # summary
2 |
3 | Roll out a manifest. This deploys the artifacts of the manifest (unpackaged, package, etc) to the target org.
4 |
5 | # description
6 |
7 | The command takes an Org Manifest and rolls out its artifacts to a target org. Dynamic paths for unpackaged artifacts are resolved based on mapped environments, package versions are resolved based on the DevHub org.
8 |
9 | # flags.manifest.summary
10 |
11 | A manifest file that defines the desired state of the target org
12 |
13 | # flags.target-org.summary
14 |
15 | Target org (sandbox, production, etc) where artifacts of the manifest should be rolled out.
16 |
17 | # flags.devhub-org.summary
18 |
19 | Devhub that owns the packages. Needed to resolve package versions.
20 |
21 | # flags.verbose.summary
22 |
23 | Placeholder - Prints all subcommand outputs to terminal (e.g. deployed source files, package install status, etc)
24 |
25 | # flags.validate-only.summary
26 |
27 | Only validate the manifest file, do not perform any rollout actions like package installs or source deploys.
28 |
29 | # examples
30 |
31 | - <%= config.bin %> <%= command.id %>
32 |
33 | # errors.manifest-requires-project
34 |
35 | Manifest has at least one step that requires a project, but no valid sfdx project was found in this directory.
36 |
37 | # infos.target-org-info
38 |
39 | Target org for rollout: %s
40 |
41 | # infos.devhub-org-info
42 |
43 | Devhub to resolve packages: %s
44 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/cmd-m01-records.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "HandlerControl__mdt",
6 | "url": "/services/data/v62.0/sobjects/HandlerControl__mdt/m014P0000009mf9QAA"
7 | },
8 | "Id": "m014P0000009mf9QAA",
9 | "DeveloperName": "Budget"
10 | },
11 | {
12 | "attributes": {
13 | "type": "HandlerControl__mdt",
14 | "url": "/services/data/v62.0/sobjects/HandlerControl__mdt/m014P0000009mfAQAQ"
15 | },
16 | "Id": "m014P0000009mfAQAQ",
17 | "DeveloperName": "Invoice"
18 | },
19 | {
20 | "attributes": {
21 | "type": "HandlerControl__mdt",
22 | "url": "/services/data/v62.0/sobjects/HandlerControl__mdt/m014P0000009ms0QAA"
23 | },
24 | "Id": "m014P0000009ms0QAA",
25 | "DeveloperName": "InvoiceLineItem"
26 | },
27 | {
28 | "attributes": {
29 | "type": "HandlerControl__mdt",
30 | "url": "/services/data/v62.0/sobjects/HandlerControl__mdt/m014P0000009mw2QAA"
31 | },
32 | "Id": "m014P0000009mw2QAA",
33 | "DeveloperName": "InvoicePdfGeneration"
34 | },
35 | {
36 | "attributes": {
37 | "type": "HandlerControl__mdt",
38 | "url": "/services/data/v62.0/sobjects/HandlerControl__mdt/m014P0000009mfBQAQ"
39 | },
40 | "Id": "m014P0000009mfBQAQ",
41 | "DeveloperName": "TimeEntry"
42 | }
43 | ],
44 | "totalSize": 5,
45 | "done": true
46 | }
47 |
--------------------------------------------------------------------------------
/test/common/planCache.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import PlanCache from '../../src/common/planCache.js';
3 |
4 | describe('plan cache', () => {
5 | afterEach(() => {
6 | PlanCache.flush();
7 | });
8 |
9 | it('returns single chunk for id length below chunk size', () => {
10 | // Arrange
11 | const ids = [];
12 | for (let i = 0; i < PlanCache.CHUNK_SIZE; i++) {
13 | ids.push(`A${i}`);
14 | }
15 | PlanCache.push('myIds', ids);
16 |
17 | // Act
18 | const returnedIds = PlanCache.getChunks('myIds');
19 |
20 | // Assert
21 | expect(returnedIds.length).equals(1);
22 | expect(returnedIds[0].length).equals(ids.length);
23 | });
24 |
25 | it('returns multiple chunks for id length above chunk size', () => {
26 | // Arrange
27 | PlanCache.CHUNK_SIZE = 100;
28 | const ids = [];
29 | for (let i = 0; i < PlanCache.CHUNK_SIZE * 5 - 10; i++) {
30 | ids.push(`A${i}`);
31 | }
32 | PlanCache.push('myIds', ids);
33 |
34 | // Act
35 | const returnedIds = PlanCache.getChunks('myIds');
36 |
37 | // Assert
38 | expect(returnedIds.length).equals(5);
39 | expect(returnedIds[0].length).equals(PlanCache.CHUNK_SIZE);
40 | expect(returnedIds[1].length).equals(PlanCache.CHUNK_SIZE);
41 | expect(returnedIds[2].length).equals(PlanCache.CHUNK_SIZE);
42 | expect(returnedIds[3].length).equals(PlanCache.CHUNK_SIZE);
43 | expect(returnedIds[4].length).equals(PlanCache.CHUNK_SIZE - 10);
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/filtered-entity-definitions.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "EntityDefinition",
6 | "url": "/services/data/v62.0/tooling/sobjects/EntityDefinition/ExternalString"
7 | },
8 | "Id": "000000000000000AAA",
9 | "DurableId": "ExternalString",
10 | "QualifiedApiName": "ExternalString",
11 | "DeveloperName": "ExternalString",
12 | "MasterLabel": "Custom Label",
13 | "KeyPrefix": "101",
14 | "IsRetrieveable": true
15 | },
16 | {
17 | "attributes": {
18 | "type": "EntityDefinition",
19 | "url": "/services/data/v62.0/tooling/sobjects/EntityDefinition/01I4P000000wXQ1"
20 | },
21 | "Id": "000000000000000AAA",
22 | "DurableId": "01I4P000000wXQ1",
23 | "QualifiedApiName": "CompanyData__mdt",
24 | "DeveloperName": "CompanyData",
25 | "MasterLabel": "Company Data",
26 | "KeyPrefix": "m00",
27 | "IsRetrieveable": true
28 | },
29 | {
30 | "attributes": {
31 | "type": "EntityDefinition",
32 | "url": "/services/data/v62.0/tooling/sobjects/EntityDefinition/Layout"
33 | },
34 | "Id": "000000000000000AAA",
35 | "DurableId": "Layout",
36 | "QualifiedApiName": "Layout",
37 | "DeveloperName": null,
38 | "MasterLabel": "Page Layout",
39 | "KeyPrefix": "00h",
40 | "IsRetrieveable": true
41 | }
42 | ],
43 | "totalSize": 3,
44 | "done": true
45 | }
46 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/scripts/plain-scratch-setup.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | # shellcheck disable=SC1091
3 | set -e
4 |
5 | alias=TestOrg
6 | duration=7
7 | configFile='config/default-scratch-def.json'
8 | devhubusername=
9 |
10 | while getopts a:d:f:v: option; do
11 | case "${option}" in
12 | a) alias=${OPTARG} ;;
13 | d) duration=${OPTARG} ;;
14 | f) configFile=${OPTARG} ;;
15 | v) devhubusername=${OPTARG} ;;
16 | *) ;;
17 | esac
18 | done
19 |
20 | echo "============================================"
21 | echo "Creating Scratch Org with these properties:"
22 | echo "Devhub: $devhubusername"
23 | echo "Config: $configFile"
24 | echo "Duration: $duration"
25 | echo "Alias: $alias"
26 | echo "============================================"
27 |
28 | if [ -z "$devhubusername" ]; then
29 | echo "sf org create scratch -y $duration -f $configFile -a $alias -d --json"
30 | sf org create scratch -y "$duration" -f "$configFile" -a "$alias" -d --json
31 | else
32 | echo "sf org create scratch -v $devhubusername -y $duration -f $configFile -a $alias -d --json"
33 | sf org create scratch -v "$devhubusername" -y "$duration" -f "$configFile" -a "$alias" -d --json
34 | fi
35 |
36 | echo "Deploy unpackaged source"
37 | sf project deploy start --ignore-conflicts
38 |
39 | echo "Generating login link for debugging"
40 | sf org open -o "$alias" -r
41 |
42 | echo "sf org open -o $alias -p \"/lightning/setup/SetupOneHome/home\""
43 | sf org open -o "$alias" -p "/lightning/setup/SetupOneHome/home"
44 |
--------------------------------------------------------------------------------
/src/common/planCache.ts:
--------------------------------------------------------------------------------
1 | export default class PlanCache {
2 | public static CHUNK_SIZE = 400;
3 | private static cache: Map> = new Map>();
4 |
5 | public static isSet(key: string): boolean {
6 | return this.cache.has(key);
7 | }
8 |
9 | public static getChunks(key: string): string[][] {
10 | const allIds = this.getNullSafe(key);
11 | const chunks = [];
12 | for (let i = 0; i < allIds.length; i += PlanCache.CHUNK_SIZE) {
13 | chunks.push(allIds.slice(i, i + PlanCache.CHUNK_SIZE));
14 | }
15 | return chunks;
16 | }
17 |
18 | public static get(key: string): string[] | undefined {
19 | if (this.cache.get(key) !== undefined) {
20 | return Array.from(this.cache.get(key)!);
21 | }
22 | return;
23 | }
24 |
25 | public static getNullSafe(key: string): string[] {
26 | if (this.isSet(key)) {
27 | return Array.from(this.cache.get(key)!);
28 | }
29 | return [];
30 | }
31 |
32 | public static set(key: string, ids: string[]): void {
33 | this.cache.set(key, new Set(ids));
34 | }
35 |
36 | public static push(key: string, ids: string[]): void {
37 | if (this.cache.has(key)) {
38 | const cachedIds = this.cache.get(key)!;
39 | // eslint-disable-next-line @typescript-eslint/unbound-method
40 | ids.forEach(cachedIds.add, cachedIds);
41 | } else {
42 | this.set(key, ids);
43 | }
44 | }
45 |
46 | public static flush(): void {
47 | this.cache.clear();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/messages/jsc.maintain.common.md:
--------------------------------------------------------------------------------
1 | # flags.output-format.summary
2 |
3 | Specify in which manifest file the content is written.
4 |
5 | # flags.output-format.description
6 |
7 | The default option prepares a package.xml with all exported components. If you specify DestructiveChangesXML, the command creates an empty package.xml and writes all components into destructiveChanges.xml. This flag only has an effect, if the output directory is set. No source is retrieved or deployed.
8 |
9 | # flags.output-dir.summary
10 |
11 | Path where package manifests will be created.
12 |
13 | # flags.output-dir.description
14 |
15 | When provided, creates manifest file (package.xml) at the target location with all exported content. Use the --output-format flag to write contents to destructiveChanges.xml.
16 |
17 | # flags.concise.summary
18 |
19 | Summarize flow output table.
20 |
21 | # flags.concise.description
22 |
23 | Instead of showing individual exported flow versions, show aggregated information with the flow name and the total number of versions. Only modifies the formatted output table, not the JSON output or generated package manifests.
24 |
25 | # flags.result-format.summary
26 |
27 | Change the display formatting of output tables.
28 |
29 | # flags.result-format.description
30 |
31 | Changes output format of table results that are printed to stdout. Use a format that is easier to copy-paste or export into other programs that support the format. For example, use markdown to copy-paste table outputs to Obsidian or Confluence.
32 |
--------------------------------------------------------------------------------
/test/common/packageManifestBuilder.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import PackageManifestBuilder from '../../src/common/packageManifestBuilder.js';
3 |
4 | const EMPTY_PACKAGE_XML = `
5 | 62.0
6 |
7 | `;
8 |
9 | const MULTI_TYPE_PACKAGE_XML = `
10 |
11 | Flow
12 | Test_Flow-1
13 |
14 |
15 | CustomLabel
16 | Member_One
17 | Member_Two
18 |
19 | 64.0
20 |
21 | `;
22 |
23 | describe('package manifest builder', () => {
24 | it('builds empty XML with default params and no types added', () => {
25 | // Act
26 | const manifest = new PackageManifestBuilder();
27 | const xmlOutput = manifest.toXML();
28 |
29 | // Assert
30 | expect(xmlOutput).to.equal(EMPTY_PACKAGE_XML);
31 | });
32 |
33 | it('formats correct package XML from multiple types with members', () => {
34 | // Act
35 | const manifest = new PackageManifestBuilder('64.0');
36 | manifest.addMember('Flow', 'Test_Flow-1');
37 | manifest.addMember('CustomLabel', 'Member_One');
38 | manifest.addMember('CustomLabel', 'Member_Two');
39 | const xmlOutput = manifest.toXML();
40 |
41 | // Assert
42 | expect(xmlOutput).to.equal(MULTI_TYPE_PACKAGE_XML);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/types/scheduledApexTypes.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | import { z } from 'zod';
3 |
4 | export type AsyncApexJob = {
5 | Id: string;
6 | CronTriggerId: string;
7 | CronTrigger: CronTrigger;
8 | ApexClass: ApexClass;
9 | };
10 |
11 | export type AsyncApexJobFlat = {
12 | CronTriggerId: string;
13 | ApexClassName: string;
14 | CronTriggerState: string;
15 | NextFireTime: Date;
16 | StartTime: Date;
17 | CronJobDetailName: string;
18 | TimesTriggered: number;
19 | CronExpression?: string;
20 | };
21 |
22 | export type CronTrigger = {
23 | State: string;
24 | StartTime: string;
25 | NextFireTime: string;
26 | CronJobDetail: CronJobDetail;
27 | TimesTriggered: number;
28 | CronExpression?: string;
29 | };
30 |
31 | export type CronJobDetail = {
32 | Name: string;
33 | };
34 |
35 | export type ApexClass = {
36 | Name: string;
37 | };
38 |
39 | const ScheduledJobConfigOptions = z
40 | .object({
41 | stop_other_jobs: z.boolean().default(false),
42 | })
43 | .strict('Valid options are: stop_other_jobs')
44 | .default({});
45 |
46 | const SingleScheduledJobConfig = z
47 | .object({ class: z.string().optional(), expression: z.string().nonempty('A valid cron expression is required') })
48 | .strict();
49 |
50 | export const ScheduledJobConfig = z
51 | .object({
52 | options: ScheduledJobConfigOptions,
53 | jobs: z.record(SingleScheduledJobConfig).default({}),
54 | })
55 | .strict();
56 |
57 | export type ScheduledJobConfigType = z.infer;
58 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/package-members/cmd-records.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Package2Member",
6 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v4P0000004JJRQA2"
7 | },
8 | "Id": "03v4P0000004JJRQA2",
9 | "CurrentPackageVersionId": "04t4P0000000002AAA",
10 | "MaxPackageVersionId": "04t4P0000000002AAA",
11 | "SubjectId": "m004P0000009UQvQAM",
12 | "SubjectKeyPrefix": "m00",
13 | "SubjectManageableState": "deprecatedEditable"
14 | },
15 | {
16 | "attributes": {
17 | "type": "Package2Member",
18 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v4P0000004JJSQA2"
19 | },
20 | "Id": "03v4P0000004JJSQA2",
21 | "CurrentPackageVersionId": "04t4P0000000002AAA",
22 | "MaxPackageVersionId": "04t4P0000000002AAA",
23 | "SubjectId": "m004P0000009UQwQAM",
24 | "SubjectKeyPrefix": "m00",
25 | "SubjectManageableState": "deprecatedEditable"
26 | },
27 | {
28 | "attributes": {
29 | "type": "Package2Member",
30 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v4P0000004JJTQA2"
31 | },
32 | "Id": "03v4P0000004JJTQA2",
33 | "CurrentPackageVersionId": "04t4P0000000002AAA",
34 | "MaxPackageVersionId": "04t4P0000000002AAA",
35 | "SubjectId": "m014P0000009mfAQAQ",
36 | "SubjectKeyPrefix": "m01",
37 | "SubjectManageableState": "deprecatedEditable"
38 | }
39 | ],
40 | "totalSize": 3,
41 | "done": true
42 | }
43 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/package-members/custom-fields.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Package2Member",
6 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v4P0000004JHeQAM"
7 | },
8 | "Id": "03v4P0000004JHeQAM",
9 | "CurrentPackageVersionId": "04t4P0000000004AAA",
10 | "MaxPackageVersionId": "04t4P0000000004AAA",
11 | "SubjectId": "00N4P00000GRKqFUAX",
12 | "SubjectKeyPrefix": "00N",
13 | "SubjectManageableState": "deprecatedEditable"
14 | },
15 | {
16 | "attributes": {
17 | "type": "Package2Member",
18 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v4P0000004JHdQAM"
19 | },
20 | "Id": "03v4P0000004JHdQAM",
21 | "CurrentPackageVersionId": "04t4P0000000004AAA",
22 | "MaxPackageVersionId": "04t4P0000000004AAA",
23 | "SubjectId": "00N4P00000GRKqHUAX",
24 | "SubjectKeyPrefix": "00N",
25 | "SubjectManageableState": "deprecatedEditable"
26 | },
27 | {
28 | "attributes": {
29 | "type": "Package2Member",
30 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v4P0000004JHcQAM"
31 | },
32 | "Id": "03v4P0000004JHcQAM",
33 | "CurrentPackageVersionId": "04t4P0000000004AAA",
34 | "MaxPackageVersionId": "04t4P0000000004AAA",
35 | "SubjectId": "00N4P00000GRKqGUAX",
36 | "SubjectKeyPrefix": "00N",
37 | "SubjectManageableState": "deprecatedEditable"
38 | }
39 | ],
40 | "totalSize": 2,
41 | "done": true
42 | }
43 |
--------------------------------------------------------------------------------
/test/data/apex-schedule-service/schedule-start-success.json:
--------------------------------------------------------------------------------
1 | {
2 | "compiled": true,
3 | "success": true,
4 | "logs": "62.0 APEX_CODE,DEBUG;APEX_PROFILING,INFO\nExecute Anonymous: \nExecute Anonymous: String jobName = 'CaseReminderJob';\nExecute Anonymous: String cronExpression = '0 0 0 ? * * * *';\nExecute Anonymous: Id jobId = System.schedule(jobName, cronExpression, new CaseReminderJob());\nExecute Anonymous: System.debug(jobId);\n18:19:47.314 (314949726)|USER_INFO|[EXTERNAL]|0059b00000EaACh|test-sgsqwpzichd0@example.com|(GMT+01:00) Central European Standard Time (Europe/Berlin)|GMT+01:00\n18:19:47.314 (314975513)|EXECUTION_STARTED\n18:19:47.314 (315001153)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex\n18:19:47.314 (347812205)|USER_DEBUG|[5]|DEBUG|08e9b00000KiFENAA3\n18:19:47.355 (355654490)|CUMULATIVE_LIMIT_USAGE\n18:19:47.355 (355654490)|LIMIT_USAGE_FOR_NS|(default)|\n Number of SOQL queries: 0 out of 100\n Number of query rows: 0 out of 50000\n Number of SOSL queries: 0 out of 20\n Number of DML statements: 0 out of 150\n Number of Publish Immediate DML: 0 out of 150\n Number of DML rows: 0 out of 10000\n Maximum CPU time: 0 out of 10000\n Maximum heap size: 0 out of 6000000\n Number of callouts: 0 out of 100\n Number of Email Invocations: 0 out of 10\n Number of future calls: 0 out of 50\n Number of queueable jobs added to the queue: 0 out of 50\n Number of Mobile Apex push calls: 0 out of 10\n\n18:19:47.355 (355654490)|CUMULATIVE_LIMIT_USAGE_END\n\n18:19:47.314 (355759120)|CODE_UNIT_FINISHED|execute_anonymous_apex\n18:19:47.314 (355782865)|EXECUTION_FINISHED\n"
5 | }
6 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/package-members/workflow-alerts.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Package2Member",
6 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v0X0000004CyGQAU"
7 | },
8 | "Id": "03v0X0000004CyGQAU",
9 | "CurrentPackageVersionId": "04t4P0000000002AAA",
10 | "MaxPackageVersionId": "04t4P0000000002AAA",
11 | "SubjectId": "01W0X0000000Jr2UAE",
12 | "SubjectKeyPrefix": "01W",
13 | "SubjectManageableState": "deprecatedEditable"
14 | },
15 | {
16 | "attributes": {
17 | "type": "Package2Member",
18 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v0X0000004CyJQAU"
19 | },
20 | "Id": "03v0X0000004CyJQAU",
21 | "CurrentPackageVersionId": "04t4P0000000001AAA",
22 | "MaxPackageVersionId": "04t4P0000000001AAA",
23 | "SubjectId": "01W0X0000000Jr1UAE",
24 | "SubjectKeyPrefix": "01W",
25 | "SubjectManageableState": "deprecatedEditable"
26 | },
27 | {
28 | "attributes": {
29 | "type": "Package2Member",
30 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v0X0000004D4WQAU"
31 | },
32 | "Id": "03v0X0000004D4WQAU",
33 | "CurrentPackageVersionId": "04t4P0000000001AAA",
34 | "MaxPackageVersionId": "04t4P0000000001AAA",
35 | "SubjectId": "01W0X0000000Jv8UAE",
36 | "SubjectKeyPrefix": "01W",
37 | "SubjectManageableState": "deprecatedEditable"
38 | }
39 | ],
40 | "totalSize": 3,
41 | "done": true
42 | }
43 |
--------------------------------------------------------------------------------
/src/types/migrationPlanObjectData.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export type MigrationPlanObjectData = {
4 | objectName: string;
5 | queryFile?: string;
6 | queryString?: string;
7 | isToolingObject?: boolean;
8 | query?: ZQueryObjectType;
9 | };
10 |
11 | export type MigrationPlanObjectQueryResult = {
12 | isSuccess: boolean;
13 | queryString: string;
14 | totalSize: number;
15 | files: string[];
16 | executedFullQueryStrings: string[];
17 | };
18 |
19 | const ZParentBind = z.object({ field: z.string(), variable: z.string() });
20 | const ZIdExports = z.record(z.string());
21 |
22 | const ZQueryObject = z.object({
23 | fetchAllFields: z.boolean(),
24 | limit: z.number().optional(),
25 | filter: z.string().optional(),
26 | bind: ZParentBind.optional(),
27 | });
28 |
29 | const ZMigrationPlanObjectData = z.object({
30 | objectName: z.string(),
31 | queryFile: z.string().optional(),
32 | queryString: z.string().optional(),
33 | isToolingObject: z.boolean().optional(),
34 | exportIds: z.string().optional(),
35 | exports: ZIdExports.optional(),
36 | query: ZQueryObject.optional(),
37 | });
38 |
39 | export const ZMigrationPlan = z.object({
40 | name: z.string(),
41 | objects: z.array(ZMigrationPlanObjectData),
42 | });
43 |
44 | export type ZParentBindType = z.infer;
45 | export type ZIdExportsType = z.infer;
46 | export type ZQueryObjectType = z.infer;
47 | export type ZMigrationPlanObjectDataType = z.infer;
48 | export type ZMigrationPlanType = z.infer;
49 |
--------------------------------------------------------------------------------
/src/common/utils/fileUtils.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs';
2 | import yaml from 'js-yaml';
3 | import { z } from 'zod';
4 | import { Messages } from '@salesforce/core';
5 |
6 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
7 | const messages = Messages.loadMessages('@j-schreiber/sf-plugin', 'fileutils');
8 |
9 | export function pathHasNoFiles(path: string): boolean {
10 | const dirContent = fs.readdirSync(path, { recursive: true });
11 | let hasFiles = false;
12 | dirContent.forEach((dirContentPath) => {
13 | hasFiles = hasFiles || !fs.lstatSync(`${path}/${String(dirContentPath)}`).isDirectory();
14 | });
15 | return !hasFiles;
16 | }
17 |
18 | export function parseYaml(path: string, schema: T): z.infer {
19 | let fileContent;
20 | try {
21 | fileContent = yaml.load(fs.readFileSync(path, 'utf8'));
22 | } catch (error) {
23 | throw messages.createError('InvalidFilePath', [path]);
24 | }
25 | const parseResult = schema.safeParse(fileContent);
26 | if (parseResult.success) {
27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return
28 | return parseResult.data as z.infer;
29 | } else {
30 | const errsFlat = parseResult.error.flatten();
31 | const formErrors = errsFlat.formErrors.join(',');
32 | const fieldErrors = Object.entries(errsFlat.fieldErrors)
33 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
34 | .map(([errPath, errors]) => `${errPath}: ${errors?.join(', ')}`)
35 | .join('\n');
36 | const combinedMsgs = [formErrors, fieldErrors].filter(Boolean).join('\n');
37 | throw messages.createError('InvalidSchema', [combinedMsgs]);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/common/jscSfCommandFlags.ts:
--------------------------------------------------------------------------------
1 | import { Messages } from '@salesforce/core';
2 | import { Flags } from '@salesforce/sf-plugins-core';
3 |
4 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
5 | const messages = Messages.loadMessages('@j-schreiber/sf-plugin', 'jsc.maintain.common');
6 |
7 | /**
8 | * Formats for package manifests
9 | */
10 | export enum OutputFormats {
11 | PackageXML = 'PackageXML',
12 | DestructiveChangesXML = 'DestructiveChangesXML',
13 | }
14 |
15 | /**
16 | * Formats for table output to stdout
17 | */
18 | export enum ResultFormats {
19 | human = 'human',
20 | csv = 'csv',
21 | markdown = 'markdown',
22 | }
23 |
24 | export const outputFormatFlag = Flags.custom({
25 | char: 'f',
26 | summary: messages.getMessage('flags.output-format.summary'),
27 | description: messages.getMessage('flags.output-format.description'),
28 | options: Object.values(OutputFormats),
29 | dependsOn: ['output-dir'],
30 | });
31 |
32 | export const resultFormatFlag = Flags.custom({
33 | char: 'r',
34 | summary: messages.getMessage('flags.result-format.summary'),
35 | description: messages.getMessage('flags.result-format.description'),
36 | options: Object.values(ResultFormats),
37 | default: ResultFormats.human,
38 | });
39 |
40 | export const manifestOutputDirFlag = Flags.file({
41 | exists: false,
42 | summary: messages.getMessage('flags.output-dir.summary'),
43 | description: messages.getMessage('flags.output-dir.description'),
44 | char: 'd',
45 | });
46 |
47 | export const conciseFlowExportTable = Flags.boolean({
48 | summary: messages.getMessage('flags.concise.summary'),
49 | description: messages.getMessage('flags.concise.description'),
50 | });
51 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/workflow-alert-definitions.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "WorkflowAlert",
6 | "url": "/services/data/v62.0/tooling/sobjects/WorkflowAlert/01W0X0000000Jr2UAE"
7 | },
8 | "Id": "01W0X0000000Jr2UAE",
9 | "DeveloperName": "Test_Alert_1",
10 | "EntityDefinition": {
11 | "attributes": {
12 | "type": "EntityDefinition",
13 | "url": "/services/data/v62.0/tooling/sobjects/EntityDefinition/Case"
14 | },
15 | "QualifiedApiName": "Case"
16 | }
17 | },
18 | {
19 | "attributes": {
20 | "type": "WorkflowAlert",
21 | "url": "/services/data/v62.0/tooling/sobjects/WorkflowAlert/01W0X0000000Jr1UAE"
22 | },
23 | "Id": "01W0X0000000Jr1UAE",
24 | "DeveloperName": "Test_Alert_2",
25 | "EntityDefinition": {
26 | "attributes": {
27 | "type": "EntityDefinition",
28 | "url": "/services/data/v62.0/tooling/sobjects/EntityDefinition/CustomObject__c"
29 | },
30 | "QualifiedApiName": "CustomObject__c"
31 | }
32 | },
33 | {
34 | "attributes": {
35 | "type": "WorkflowAlert",
36 | "url": "/services/data/v62.0/tooling/sobjects/WorkflowAlert/01W0X0000000Jv8UAE"
37 | },
38 | "Id": "01W0X0000000Jv8UAE",
39 | "DeveloperName": "Test_Alert_3",
40 | "EntityDefinition": {
41 | "attributes": {
42 | "type": "EntityDefinition",
43 | "url": "/services/data/v62.0/tooling/sobjects/EntityDefinition/CustomObject__c"
44 | },
45 | "QualifiedApiName": "CustomObject__c"
46 | }
47 | }
48 | ],
49 | "totalSize": 3,
50 | "done": true
51 | }
52 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/src/flows/Active_Test_Flow.flow-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 64.0
4 | An empty screen flow for testing purposes.
5 | Default
6 | Active Test Flow {!$Flow.CurrentDateTime}
7 |
8 |
9 | BuilderType
10 |
11 | LightningFlowBuilder
12 |
13 |
14 |
15 | CanvasMode
16 |
17 | AUTO_LAYOUT_CANVAS
18 |
19 |
20 |
21 | OriginBuilderType
22 |
23 | LightningFlowBuilder
24 |
25 |
26 | Flow
27 |
28 | Empty_Screen
29 |
30 | 0
31 | 0
32 | true
33 | true
34 | true
35 | true
36 | false
37 |
38 |
39 | 0
40 | 0
41 |
42 | Empty_Screen
43 |
44 |
45 | Active
46 |
47 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/src/flows/Inactive_Test_Flow.flow-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 64.0
4 | An empty screen flow for testing purposes.
5 | Default
6 | Inactive Test Flow {!$Flow.CurrentDateTime}
7 |
8 |
9 | BuilderType
10 |
11 | LightningFlowBuilder
12 |
13 |
14 |
15 | CanvasMode
16 |
17 | AUTO_LAYOUT_CANVAS
18 |
19 |
20 |
21 | OriginBuilderType
22 |
23 | LightningFlowBuilder
24 |
25 |
26 | Flow
27 |
28 | Empty_Screen
29 |
30 | 0
31 | 0
32 | true
33 | true
34 | true
35 | true
36 | false
37 |
38 |
39 | 0
40 | 0
41 |
42 | Empty_Screen
43 |
44 |
45 | Draft
46 |
47 |
--------------------------------------------------------------------------------
/test/data/test-sfdx-project/scripts/scratch-setup.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | # shellcheck disable=SC1091
3 | set -e
4 |
5 | alias=TestOrg
6 | duration=7
7 | configFile='config/default-scratch-def.json'
8 | devhubusername=
9 |
10 | while getopts a:d:f:v: option; do
11 | case "${option}" in
12 | a) alias=${OPTARG} ;;
13 | d) duration=${OPTARG} ;;
14 | f) configFile=${OPTARG} ;;
15 | v) devhubusername=${OPTARG} ;;
16 | *) ;;
17 | esac
18 | done
19 |
20 | echo "============================================"
21 | echo "Creating Scratch Org with these properties:"
22 | echo "Devhub: $devhubusername"
23 | echo "Config: $configFile"
24 | echo "Duration: $duration"
25 | echo "Alias: $alias"
26 | echo "============================================"
27 |
28 | if [ -z "$devhubusername" ]; then
29 | echo "sf org create scratch -y $duration -f $configFile -a $alias -d --json"
30 | sf org create scratch -y "$duration" -f "$configFile" -a "$alias" -d --json
31 | else
32 | echo "sf org create scratch -v $devhubusername -y $duration -f $configFile -a $alias -d --json"
33 | sf org create scratch -v "$devhubusername" -y "$duration" -f "$configFile" -a "$alias" -d --json
34 | fi
35 |
36 | echo "sf package install -p Test Package @ LATEST -o $alias -w 10 --json"
37 | sf package install -p "Test Package @ LATEST" -o "$alias" -w 10 --json
38 |
39 | echo "sf package install -p Test Package @ 0.1.0 -o $alias -w 10 --json --upgrade-type DeprecateOnly"
40 | sf package install -p "Test Package @ 0.1.0" -o "$alias" -w 10 --json --upgrade-type DeprecateOnly
41 |
42 | echo "Generating login link for debugging"
43 | sf org open -o "$alias" -r
44 |
45 | echo "sf org open -o $alias -p \"/lightning/setup/SetupOneHome/home\""
46 | sf org open -o "$alias" -p "/lightning/setup/SetupOneHome/home"
47 |
--------------------------------------------------------------------------------
/test/data/flow-export/obsolete-flows.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "attributes": {
4 | "type": "Flow",
5 | "url": "/services/data/v64.0/tooling/sobjects/Flow/3013X000000rwdpQAA"
6 | },
7 | "Id": "3013X000000rwdpQAA",
8 | "DefinitionId": "3003X000000BMf8QAG",
9 | "Definition": {
10 | "attributes": {
11 | "type": "FlowDefinition",
12 | "url": "/services/data/v64.0/tooling/sobjects/FlowDefinition/3003X000000BMf8QAG"
13 | },
14 | "DeveloperName": "Test_Flow_1"
15 | },
16 | "Status": "Obsolete",
17 | "ProcessType": "AutoLaunchedFlow",
18 | "VersionNumber": 2
19 | },
20 | {
21 | "attributes": {
22 | "type": "Flow",
23 | "url": "/services/data/v64.0/tooling/sobjects/Flow/301Fg00000YpIgtIAF"
24 | },
25 | "Id": "301Fg00000YpIgtIAF",
26 | "DefinitionId": "3003X000000BMf8QAG",
27 | "Definition": {
28 | "attributes": {
29 | "type": "FlowDefinition",
30 | "url": "/services/data/v64.0/tooling/sobjects/FlowDefinition/3003X000000BMf8QAG"
31 | },
32 | "DeveloperName": "Test_Flow_1"
33 | },
34 | "Status": "Draft",
35 | "ProcessType": "AutoLaunchedFlow",
36 | "VersionNumber": 3
37 | },
38 | {
39 | "attributes": {
40 | "type": "Flow",
41 | "url": "/services/data/v64.0/tooling/sobjects/Flow/3013X000000f03KQAQ"
42 | },
43 | "Id": "3013X000000f03KQAQ",
44 | "DefinitionId": "3003X000000gO19QAE",
45 | "Definition": {
46 | "attributes": {
47 | "type": "FlowDefinition",
48 | "url": "/services/data/v64.0/tooling/sobjects/FlowDefinition/3003X000000gO19QAE"
49 | },
50 | "DeveloperName": "Test_Flow_2"
51 | },
52 | "Status": "Obsolete",
53 | "ProcessType": "Workflow",
54 | "VersionNumber": 6
55 | }
56 | ]
57 |
--------------------------------------------------------------------------------
/src/common/packageManifestDirectory.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs';
2 | import path from 'node:path';
3 | import PackageManifestBuilder from './packageManifestBuilder.js';
4 | import { OutputFormats } from './jscSfCommandFlags.js';
5 |
6 | /**
7 | * Initialise an empty manifest directory at the path. The directory
8 | * writes manifest XMLs (package.xml and destructiveChanges.xml) when
9 | * closed.
10 | */
11 | export default class PackageManifestDirectory {
12 | private packageXml: PackageManifestBuilder;
13 | private destructiveChangesXml?: PackageManifestBuilder;
14 |
15 | public constructor(public readonly directoryPath: string, public readonly outputFormat?: OutputFormats) {
16 | fs.mkdirSync(this.directoryPath, { recursive: true });
17 | this.packageXml = new PackageManifestBuilder();
18 | if (this.outputFormat === OutputFormats.DestructiveChangesXML) {
19 | this.destructiveChangesXml = new PackageManifestBuilder();
20 | }
21 | }
22 |
23 | /**
24 | * Returns the primary builder that holds contents,
25 | * based on the output format.
26 | */
27 | public getBuilder(): PackageManifestBuilder {
28 | return this.outputFormat === OutputFormats.DestructiveChangesXML ? this.destructiveChangesXml! : this.packageXml;
29 | }
30 |
31 | /**
32 | * Writes all manifest files to output dir and clears
33 | * obsolete files if they exist.
34 | */
35 | public write(): void {
36 | if (this.outputFormat === OutputFormats.DestructiveChangesXML) {
37 | this.destructiveChangesXml!.writeToXmlFile(path.join(this.directoryPath, 'destructiveChanges.xml'));
38 | } else {
39 | fs.rmSync(path.join(this.directoryPath, 'destructiveChanges.xml'), { force: true });
40 | }
41 | this.packageXml.writeToXmlFile(path.join(this.directoryPath, 'package.xml'));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/package-members/mixed-with-package-infos.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "Package2Member",
6 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v4P0000004JPzQAM"
7 | },
8 | "Id": "03v4P0000004JPzQAM",
9 | "CurrentPackageVersionId": "04t4P0000000003AAA",
10 | "MaxPackageVersionId": "04t4P0000000003AAA",
11 | "MaxPackageVersion": {
12 | "attributes": {
13 | "type": "SubscriberPackageVersion",
14 | "url": "/services/data/v62.0/tooling/sobjects/SubscriberPackageVersion/04t4P0000000003AAA"
15 | },
16 | "MajorVersion": 1,
17 | "MinorVersion": 2,
18 | "PatchVersion": 3
19 | },
20 | "SubjectId": "1014P00000B3v7KQAR",
21 | "SubjectKeyPrefix": "101",
22 | "SubjectManageableState": "deprecatedEditable",
23 | "SubscriberPackageId": "0330X0000000000AAA"
24 | },
25 | {
26 | "attributes": {
27 | "type": "Package2Member",
28 | "url": "/services/data/v62.0/tooling/sobjects/Package2Member/03v0X0000004CxhQAE"
29 | },
30 | "Id": "03v0X0000004CxhQAE",
31 | "CurrentPackageVersionId": "04t670000015YcsAAE",
32 | "MaxPackageVersionId": "04t670000015YcsAAE",
33 | "MaxPackageVersion": {
34 | "attributes": {
35 | "type": "SubscriberPackageVersion",
36 | "url": "/services/data/v62.0/tooling/sobjects/SubscriberPackageVersion/04t4P0000000004AAA"
37 | },
38 | "MajorVersion": 1,
39 | "MinorVersion": 2,
40 | "PatchVersion": 4
41 | },
42 | "SubjectId": "00h0X00000XRV9HQAX",
43 | "SubjectKeyPrefix": "00h",
44 | "SubscriberPackageId": "0330X0000000000AAA"
45 | }
46 | ],
47 | "totalSize": 2,
48 | "done": true
49 | }
50 |
--------------------------------------------------------------------------------
/test/data/apex-schedule-service/job-is-already-aborted.json:
--------------------------------------------------------------------------------
1 | {
2 | "compiled": true,
3 | "success": false,
4 | "logs": "62.0 APEX_CODE,DEBUG;APEX_PROFILING,INFO\nExecute Anonymous: System.abortJob('08e9b00000Kz6hmAAB');\n00:06:13.40 (40270026)|USER_INFO|[EXTERNAL]|0059b00000EyPaY|test-hm5zkhjbc6hb@example.com|(GMT+01:00) Central European Standard Time (Europe/Berlin)|GMT+01:00\n00:06:13.40 (40296947)|EXECUTION_STARTED\n00:06:13.40 (40310265)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex\n00:06:13.40 (44366873)|EXCEPTION_THROWN|[1]|System.StringException: Job does not exist or is already aborted.\n00:06:13.40 (44552752)|FATAL_ERROR|System.StringException: Job does not exist or is already aborted.\n\nAnonymousBlock: line 1, column 1\n00:06:13.44 (44719892)|CUMULATIVE_LIMIT_USAGE\n00:06:13.44 (44719892)|LIMIT_USAGE_FOR_NS|(default)|\n Number of SOQL queries: 0 out of 100\n Number of query rows: 0 out of 50000\n Number of SOSL queries: 0 out of 20\n Number of DML statements: 1 out of 150\n Number of Publish Immediate DML: 0 out of 150\n Number of DML rows: 1 out of 10000\n Maximum CPU time: 0 out of 10000\n Maximum heap size: 0 out of 6000000\n Number of callouts: 0 out of 100\n Number of Email Invocations: 0 out of 10\n Number of future calls: 0 out of 50\n Number of queueable jobs added to the queue: 0 out of 50\n Number of Mobile Apex push calls: 0 out of 10\n\n00:06:13.44 (44719892)|CUMULATIVE_LIMIT_USAGE_END\n\n00:06:13.40 (44780053)|CODE_UNIT_FINISHED|execute_anonymous_apex\n00:06:13.40 (44792421)|EXECUTION_FINISHED\n",
5 | "diagnostic": [
6 | {
7 | "lineNumber": "1",
8 | "columnNumber": "1",
9 | "compileProblem": "",
10 | "exceptionMessage": "System.StringException: Job does not exist or is already aborted.",
11 | "exceptionStackTrace": "AnonymousBlock: line 1, column 1"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/src/garbage-collection/entity-handlers/fullNameSingleRecord.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { Connection } from '@salesforce/core';
3 | import { FullNameSingleRecordEntity, Package2Member } from '../../types/sfToolingApiTypes.js';
4 | import { EntityDefinitionHandler } from '../entityDefinitionHandler.js';
5 | import { PackageGarbage, PackageGarbageContainer } from '../packageGarbageTypes.js';
6 | import QueryRunner from '../../common/utils/queryRunner.js';
7 |
8 | export class FullNameSingleRecord implements EntityDefinitionHandler {
9 | private readonly queryRunner: QueryRunner;
10 |
11 | public constructor(
12 | private readonly queryConnection: Connection['tooling'],
13 | private readonly entityName: string,
14 | private readonly metadataTypeName?: string
15 | ) {
16 | this.queryRunner = new QueryRunner(this.queryConnection);
17 | }
18 |
19 | public async resolve(packageMembers: Package2Member[]): Promise {
20 | const garbageList: PackageGarbage[] = [];
21 | for (const packageMember of packageMembers) {
22 | // queries with "FullName" throw exception, if more than one record is queried
23 | // so yes, we do "SOQL in a loop" here. "For performance reasons" (as stated in the docs). LOL.
24 | // eslint-disable-next-line no-await-in-loop
25 | const entityDef = await this.queryRunner.fetchRecords(
26 | `SELECT Id,FullName FROM ${this.entityName} WHERE Id = '${packageMember.SubjectId}' LIMIT 1`
27 | );
28 | if (entityDef.length > 0) {
29 | garbageList.push(new PackageGarbage(packageMember, entityDef[0].FullName));
30 | }
31 | }
32 | return {
33 | metadataType: this.metadataTypeName ?? this.entityName,
34 | componentCount: garbageList.length,
35 | components: garbageList,
36 | };
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/messages/jsc.apex.schedule.manage.md:
--------------------------------------------------------------------------------
1 | # summary
2 |
3 | Manages all cron jobs on a target org from config file.
4 |
5 | # description
6 |
7 | Provide the path to a config file that contains a definition of scheduled jobs. The manage command will try to start non-existing jobs and, depending on options, also stop obsolete running jobs. All options for this command are read from the config file.
8 |
9 | # flags.target-org.summary
10 |
11 | Target org where the job will be scheduled.
12 |
13 | # flags.config-file.summary
14 |
15 | Path to a valid config file that configures scheduled jobs.
16 |
17 | # flags.dry-run.summary
18 |
19 | Simulate a run and get information on how many jos would be started, stopped, and left untouched.
20 |
21 | # flags.config-file.description
22 |
23 | Specify the jobs to start, together with options how obsolete or redundant jobs should be treated. Jobs are identified by their unique job name. You can specify the same apex class multiple times, as long as job names are different. For convenience, you can also use the apex class as job name and omit class property - in that case, you can only schedule an apex class once for obvious reasons.
24 |
25 | # flags.dry-run.description
26 |
27 | Queries existing jobs on the target org and evaluates, which jobs would be changed. However, dry-run cannot compile the apex class and cron expression. This means, that invalid cron expressions or apex classes will not be caught and the command may still fail, when run without this flag.
28 |
29 | # examples
30 |
31 | - Sync all jobs on target org with config file
32 |
33 | <%= config.bin %> <%= command.id %> -o MyTargetOrg -f scheduled-job-definitions.yaml
34 |
35 | # infos.dry-run-mode
36 |
37 | Running in simulation mode. No jobs will be changed.
38 |
39 | # infos.dry-run-cannot-compile
40 |
41 | Dry-run cannot validate apex classes and cron expressions.
42 |
--------------------------------------------------------------------------------
/src/types/platformTypes.ts:
--------------------------------------------------------------------------------
1 | import { DescribeSObjectResult, Optional } from '@jsforce/jsforce-node';
2 |
3 | export type DescribeRecordTypeResult = {
4 | id: string;
5 | active: boolean;
6 | label: string;
7 | businessProcess: Optional;
8 | compactLayoutAssignment: Optional;
9 | description: Optional;
10 | picklistValues: RecordTypePicklist[];
11 | };
12 |
13 | export type RecordTypePicklist = {
14 | picklist: string;
15 | values: Array>;
16 | };
17 |
18 | export type RecordTypeInfo = {
19 | available: boolean;
20 | active: boolean;
21 | defaultRecordTypeMapping: boolean;
22 | master: boolean;
23 | name: string;
24 | developerName: string;
25 | recordTypeId: string;
26 | urls: {
27 | [key: string]: string;
28 | };
29 | };
30 |
31 | export type ChildRelationship = {
32 | cascadeDelete: boolean;
33 | childSObject: string;
34 | deprecatedAndHidden: boolean;
35 | field: string;
36 | junctionIdListNames: string[];
37 | junctionReferenceTo: string[];
38 | relationshipName: Optional;
39 | restrictedDelete: boolean;
40 | };
41 |
42 | export type DescribeHistoryResult = {
43 | relationship: ChildRelationship;
44 | describe: DescribeSObjectResult;
45 | };
46 |
47 | type RecordTypePicklistValue = {
48 | valueName: string;
49 | allowEmail: Optional;
50 | color: Optional;
51 | controllingFieldValues: Optional;
52 | converted: Optional;
53 | cssExposed: Optional;
54 | description: Optional;
55 | forecastCategory: Optional;
56 | highPriority: Optional;
57 | isActive: Optional;
58 | probability: Optional;
59 | reverseRole: Optional;
60 | urls: Optional;
61 | default: false;
62 | closed: Optional;
63 | reviewed: Optional;
64 | won: Optional;
65 | };
66 |
--------------------------------------------------------------------------------
/messages/garbagecollection.md:
--------------------------------------------------------------------------------
1 | # infos.excluded-from-result-not-in-filter
2 |
3 | Excluded from result because metadata type filter was active and this type was not filtered.
4 |
5 | # infos.not-fully-supported-by-tooling-api
6 |
7 | Entity %s (%s) is not fully supported by Tooling API: %s. %s members are ignored.
8 |
9 | # infos.not-yet-implemented
10 |
11 | Entity %s (%s) is not yet implemented. %s members are ignored.
12 |
13 | # infos.unknown-keyprefix
14 |
15 | Failed to resolve keyprefix %s to a valid entity definition. %s members are ignored.
16 |
17 | # infos.not-a-sfdx-project
18 |
19 | Could not initialise project (%s).
20 |
21 | # infos.unknown-error-initialising-project
22 |
23 | Failed to initialise project with an unknown error.
24 |
25 | # infos.package-aliases-not-supported
26 |
27 | To use package aliases, run this command within a valid project. You can still use package ids (0Ho).
28 |
29 | # infos.packages-filter-active
30 |
31 | Packages filter active. Only include members from these packages: %s
32 |
33 | # infos.metadata-filter-active
34 |
35 | Metadata types filter active. Only include members of these types: %s
36 |
37 | # infos.resolved-package-id
38 |
39 | Resolved %s (Package2) to %s (SubscriberPackage)
40 |
41 | # warnings.failed-to-resolved-package-id
42 |
43 | Could not resolve %s (Package2) to its corresponding SubscriberPackageId (033). The Id does not exist on DevHub (%s). Id will be ignored for filtering package members. Did you use the correct DevHub?
44 |
45 | # packages-filter-no-devhub
46 |
47 | Packages filter specified, but no Devhub was provided.
48 |
49 | # provide-valid-devhub-with-filter
50 |
51 | Provide a valid DevHub, when you specify a packages filter.
52 |
53 | # record-types-cannot-be-deleted
54 |
55 | Record types cannot be deleted via metadata API.
56 |
57 | # deprecated-list-views-not-accessible
58 |
59 | List views are not accessible via APIs, when member is deprecated.
60 |
--------------------------------------------------------------------------------
/src/garbage-collection/queries.ts:
--------------------------------------------------------------------------------
1 | import QueryBuilder from '../common/utils/queryBuilder.js';
2 |
3 | export const PACKAGE_MEMBER_BASE = `SELECT
4 | Id,
5 | CurrentPackageVersionId,
6 | MaxPackageVersionId,
7 | SubscriberPackageId,
8 | MaxPackageVersion.MajorVersion,
9 | MaxPackageVersion.MinorVersion,
10 | MaxPackageVersion.PatchVersion,
11 | SubjectId,
12 | SubjectKeyPrefix,
13 | SubjectManageableState
14 | FROM
15 | Package2Member`;
16 |
17 | // flow definitions (prefix 300) are processed separately
18 | export const ALL_DEPRECATED_PACKAGE_MEMBERS = QueryBuilder.sanitise(`${PACKAGE_MEMBER_BASE}
19 | WHERE
20 | SubjectManageableState IN ('deprecatedEditable', 'deprecated')
21 | AND SubjectKeyPrefix NOT IN ('300')
22 | ORDER BY
23 | SubjectKeyPrefix`);
24 |
25 | export const ENTITY_DEFINITION_QUERY = QueryBuilder.sanitise(`SELECT
26 | Id,
27 | DurableId,
28 | QualifiedApiName,
29 | DeveloperName,
30 | MasterLabel,
31 | KeyPrefix,
32 | IsRetrieveable
33 | FROM
34 | EntityDefinition`);
35 |
36 | export const OBSOLETE_FLOWS: string = QueryBuilder.sanitise(`SELECT
37 | Id,
38 | VersionNumber,
39 | Definition.DeveloperName,
40 | DefinitionId,
41 | Status
42 | FROM
43 | Flow
44 | WHERE
45 | Status = 'Obsolete'
46 | ORDER BY
47 | Definition.DeveloperName,
48 | VersionNumber ASC
49 | LIMIT 2000`);
50 |
51 | export const ALL_CUSTOM_OBJECTS: string = QueryBuilder.sanitise(`SELECT
52 | Id,
53 | DurableId,
54 | QualifiedApiName,
55 | DeveloperName,
56 | MasterLabel,
57 | KeyPrefix,
58 | IsRetrieveable
59 | FROM
60 | EntityDefinition
61 | WHERE
62 | KeyPrefix LIKE 'a%'
63 | OR KeyPrefix LIKE 'm%'
64 | OR KeyPrefix LIKE 'e%'`);
65 |
66 | export const PACKAGE_2: string = QueryBuilder.sanitise(`SELECT
67 | Id,
68 | SubscriberPackageId
69 | FROM
70 | Package2`);
71 |
72 | export const SUBSCRIBER_PACKAGE_FIELDS = ['Id', 'Name', 'Description', 'IsPackageValid', 'NamespacePrefix'];
73 |
--------------------------------------------------------------------------------
/test/mock-utils/sfQueryApiMocks.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs';
2 | import path from 'node:path';
3 | import { type AnyJson } from '@salesforce/ts-types';
4 | import { QueryResult, Record } from '@jsforce/jsforce-node';
5 | import { EmptyQueryResult } from '../data/api/queryResults.js';
6 | import { MockAnyObjectResult } from '../data/describes/mockDescribeResults.js';
7 |
8 | export function mockQueryResponseWithQueryMore(request: AnyJson): Promise {
9 | const url = (request as { url: string }).url;
10 | if (url.includes('/query?')) {
11 | const orderBatch1 = JSON.parse(fs.readFileSync('./test/data/api/orders-1.json', 'utf-8')) as QueryResult;
12 | return Promise.resolve(orderBatch1);
13 | }
14 | if (url.includes('/query/0r8xx3d3FsAvwqKAIR-20')) {
15 | const orderBatch2 = JSON.parse(fs.readFileSync('./test/data/api/orders-2.json', 'utf-8')) as QueryResult;
16 | return Promise.resolve(orderBatch2);
17 | }
18 | return Promise.reject({ data: { message: 'Unexpected query was executed' } });
19 | }
20 |
21 | export function mockAnySObjectDescribe(request: AnyJson): Promise {
22 | if (request?.toString().endsWith('/describe')) {
23 | const requestUrl = String(request).split('/');
24 | const sobjectName = requestUrl[requestUrl.length - 2];
25 | const describe = { ...MockAnyObjectResult, name: sobjectName };
26 | return Promise.resolve(describe as AnyJson);
27 | }
28 | const url = (request as { url: string }).url;
29 | if (url.includes('/query?')) {
30 | return Promise.resolve(EmptyQueryResult);
31 | }
32 | return Promise.resolve({});
33 | }
34 |
35 | export function parseFileAsQueryResult(filePath: string[]) {
36 | return JSON.parse(fs.readFileSync(path.join(...filePath), 'utf8')) as QueryResult;
37 | }
38 |
39 | export function parseFile(filePath: string[]) {
40 | return JSON.parse(fs.readFileSync(path.join(...filePath), 'utf8')) as T;
41 | }
42 |
--------------------------------------------------------------------------------
/test/data/garbage-collection/cmd-m00-records.json:
--------------------------------------------------------------------------------
1 | {
2 | "records": [
3 | {
4 | "attributes": {
5 | "type": "CompanyData__mdt",
6 | "url": "/services/data/v62.0/sobjects/CompanyData__mdt/m004P0000009UQsQAM"
7 | },
8 | "Id": "m004P0000009UQsQAM",
9 | "DeveloperName": "CITY"
10 | },
11 | {
12 | "attributes": {
13 | "type": "CompanyData__mdt",
14 | "url": "/services/data/v62.0/sobjects/CompanyData__mdt/m004P0000009UQtQAM"
15 | },
16 | "Id": "m004P0000009UQtQAM",
17 | "DeveloperName": "COUNTRY"
18 | },
19 | {
20 | "attributes": {
21 | "type": "CompanyData__mdt",
22 | "url": "/services/data/v62.0/sobjects/CompanyData__mdt/m004P0000009UQuQAM"
23 | },
24 | "Id": "m004P0000009UQuQAM",
25 | "DeveloperName": "EMAIL"
26 | },
27 | {
28 | "attributes": {
29 | "type": "CompanyData__mdt",
30 | "url": "/services/data/v62.0/sobjects/CompanyData__mdt/m004P0000009UQvQAM"
31 | },
32 | "Id": "m004P0000009UQvQAM",
33 | "DeveloperName": "NAME"
34 | },
35 | {
36 | "attributes": {
37 | "type": "CompanyData__mdt",
38 | "url": "/services/data/v62.0/sobjects/CompanyData__mdt/m004P0000009UQwQAM"
39 | },
40 | "Id": "m004P0000009UQwQAM",
41 | "DeveloperName": "PHONE"
42 | },
43 | {
44 | "attributes": {
45 | "type": "CompanyData__mdt",
46 | "url": "/services/data/v62.0/sobjects/CompanyData__mdt/m004P0000009UQxQAM"
47 | },
48 | "Id": "m004P0000009UQxQAM",
49 | "DeveloperName": "POSTAL_CODE"
50 | },
51 | {
52 | "attributes": {
53 | "type": "CompanyData__mdt",
54 | "url": "/services/data/v62.0/sobjects/CompanyData__mdt/m004P0000009UQyQAM"
55 | },
56 | "Id": "m004P0000009UQyQAM",
57 | "DeveloperName": "STREET"
58 | }
59 | ],
60 | "totalSize": 7,
61 | "done": true
62 | }
63 |
--------------------------------------------------------------------------------
/test/mock-utils/apexSchedulerMocks.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs';
2 | import path from 'node:path';
3 | import { AnyJson } from '@salesforce/ts-types';
4 | import { ExecuteAnonymousResponse } from '@salesforce/apex-node';
5 | import { QueryResult } from '@jsforce/jsforce-node';
6 | import { AsyncApexJob } from '../../src/types/scheduledApexTypes.js';
7 |
8 | const testDataPath = path.join('test', 'data', 'apex-schedule-service');
9 |
10 | export default class ApexSchedulerMocks {
11 | public SCHEDULE_START_SUCCESS = parseMockResult('schedule-start-success.json');
12 | public SCHEDULE_STOP_SUCCESS = parseMockResult('schedule-stop-success.json');
13 | public ALREADY_SCHEDULED_ERROR = parseMockResult('is-already-scheduled-error.json');
14 | public INVALID_CRON_EXPRESSION_ERROR = parseMockResult('invalid-cron-error.json');
15 | public JOB_ALREADY_ABORTED = parseMockResult('job-is-already-aborted.json');
16 | public JOB_DETAILS = parseMockResult>('cron-trigger-details.json');
17 | public ALL_JOBS = parseMockResult>('all-jobs.json');
18 |
19 | public mockQueryResults(request: AnyJson): Promise {
20 | const url = (request as { url: string }).url;
21 | if (url.includes(encodeURIComponent("FROM AsyncApexJob WHERE JobType = 'ScheduledApex' AND Status = 'Queued'"))) {
22 | return Promise.resolve(this.ALL_JOBS);
23 | }
24 | if (url.includes(encodeURIComponent("FROM AsyncApexJob WHERE CronTriggerId = '08e9b00000KiFENAA3' LIMIT 1"))) {
25 | return Promise.resolve(this.JOB_DETAILS);
26 | }
27 | return Promise.reject(new Error(`No mock was defined for: ${decodeURIComponent(url)}`));
28 | }
29 | }
30 |
31 | function parseMockResult(filePath: string) {
32 | return JSON.parse(fs.readFileSync(`${path.join(testDataPath, filePath)}`, 'utf8')) as T;
33 | }
34 |
--------------------------------------------------------------------------------
/src/garbage-collection/entity-handlers/nameEntity.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { Connection } from '@salesforce/core';
3 | import { NamedRecord, Package2Member } from '../../types/sfToolingApiTypes.js';
4 | import { buildSubjectIdFilter, EntityDefinitionHandler } from '../entityDefinitionHandler.js';
5 | import { PackageGarbage, PackageGarbageContainer } from '../packageGarbageTypes.js';
6 | import QueryRunner from '../../common/utils/queryRunner.js';
7 |
8 | export class NameEntity implements EntityDefinitionHandler {
9 | private readonly queryRunner: QueryRunner;
10 |
11 | public constructor(
12 | private readonly queryConnection: Connection | Connection['tooling'],
13 | private readonly entityName: string,
14 | private readonly metadataTypeName?: string
15 | ) {
16 | this.queryRunner = new QueryRunner(this.queryConnection);
17 | }
18 |
19 | public async resolve(packageMembers: Package2Member[]): Promise {
20 | const garbageList: PackageGarbage[] = [];
21 | const entityDefs = await this.fetchEntities(packageMembers);
22 | packageMembers.forEach((packageMember) => {
23 | if (entityDefs.has(packageMember.SubjectId)) {
24 | const def = entityDefs.get(packageMember.SubjectId)!;
25 | garbageList.push(new PackageGarbage(packageMember, def.Name));
26 | }
27 | });
28 | return {
29 | metadataType: this.metadataTypeName ?? this.entityName,
30 | componentCount: garbageList.length,
31 | components: garbageList,
32 | };
33 | }
34 |
35 | private async fetchEntities(packageMembers: Package2Member[]): Promise