From 17f3ef33b68ae237e2dea7b54c6c631db64ad3f7 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 13 Jun 2024 11:14:29 -0600 Subject: [PATCH 1/6] feat: add --hard-delete flag --- src/BulkBaseCommand.ts | 8 +++++--- src/bulkOperationCommand.ts | 7 +++---- src/commands/data/delete/bulk.ts | 6 +++--- test/soqlQuery.test.ts | 6 ------ 4 files changed, 11 insertions(+), 16 deletions(-) delete mode 100644 test/soqlQuery.test.ts diff --git a/src/BulkBaseCommand.ts b/src/BulkBaseCommand.ts index c3502ad2..4e39bf48 100644 --- a/src/BulkBaseCommand.ts +++ b/src/BulkBaseCommand.ts @@ -70,9 +70,11 @@ export const displayBulkV2Result = ({ cmd: SfCommand; username?: string; }): void => { + // if we just read from jobInfo.operation it may suggest running the nonexistent `sf data hardDelete resume` command + const operation = jobInfo.operation === 'hardDelete' || jobInfo.operation === 'delete' ? 'delete' : jobInfo.operation; if (isAsync && jobInfo.state !== 'JobComplete' && jobInfo.state !== 'Failed') { - cmd.logSuccess(messages.getMessage('success', [jobInfo.operation, jobInfo.id])); - cmd.info(messages.getMessage('checkStatus', [jobInfo.operation, jobInfo.id, username])); + cmd.logSuccess(messages.getMessage('success', [operation, jobInfo.id])); + cmd.info(messages.getMessage('checkStatus', [operation, jobInfo.id, username])); } else { cmd.log(); cmd.info(getResultMessage(jobInfo)); @@ -81,7 +83,7 @@ export const displayBulkV2Result = ({ process.exitCode = 1; } if (jobInfo.state === 'InProgress' || jobInfo.state === 'Open') { - cmd.info(messages.getMessage('checkStatus', [jobInfo.operation, jobInfo.id, username])); + cmd.info(messages.getMessage('checkStatus', [operation, jobInfo.id, username])); } if (jobInfo.state === 'Failed') { throw messages.createError('bulkJobFailed', [jobInfo.id]).setData(jobInfo); diff --git a/src/bulkOperationCommand.ts b/src/bulkOperationCommand.ts index 7a6e8e0c..ab8012d6 100644 --- a/src/bulkOperationCommand.ts +++ b/src/bulkOperationCommand.ts @@ -7,7 +7,6 @@ import fs from 'node:fs'; import { ReadStream } from 'node:fs'; import os from 'node:os'; - import { Flags, SfCommand } from '@salesforce/sf-plugins-core'; import { Duration } from '@salesforce/kit'; import { Connection, Messages } from '@salesforce/core'; @@ -77,7 +76,7 @@ export const baseFlags = { }), }; -type SupportedOperations = Extract; +type SupportedOperations = Extract; export const runBulkOperation = async ({ sobject, @@ -156,6 +155,7 @@ export const runBulkOperation = async ({ }; const getCache = async (operation: SupportedOperations): Promise => { switch (operation) { + case 'hardDelete': case 'delete': return BulkDeleteRequestCache.create(); case 'upsert': @@ -168,8 +168,7 @@ const getCache = async (operation: SupportedOperations): Promise( job: IngestJobV2, diff --git a/src/commands/data/delete/bulk.ts b/src/commands/data/delete/bulk.ts index 73236cde..93cf7211 100644 --- a/src/commands/data/delete/bulk.ts +++ b/src/commands/data/delete/bulk.ts @@ -7,7 +7,7 @@ import { Messages } from '@salesforce/core'; import { Duration } from '@salesforce/kit'; -import { SfCommand } from '@salesforce/sf-plugins-core'; +import { Flags, SfCommand } from '@salesforce/sf-plugins-core'; import { baseFlags, runBulkOperation } from '../../../bulkOperationCommand.js'; import { BulkResultV2 } from '../../../types.js'; @@ -19,7 +19,7 @@ export default class Delete extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); - public static readonly flags = baseFlags; + public static readonly flags = { ...baseFlags, 'hard-delete': Flags.boolean({ summary: 'abc', default: false }) }; public async run(): Promise { const { flags } = await this.parse(Delete); @@ -30,7 +30,7 @@ export default class Delete extends SfCommand { connection: flags['target-org'].getConnection(flags['api-version']), wait: flags.async ? Duration.minutes(0) : flags.wait, verbose: flags.verbose, - operation: 'delete', + operation: flags['hard-delete'] ? 'hardDelete' : 'delete', }); } } diff --git a/test/soqlQuery.test.ts b/test/soqlQuery.test.ts deleted file mode 100644 index da986873..00000000 --- a/test/soqlQuery.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Copyright (c) 2023, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ From fdd6218090f89d24ac4697dfbfeb91f062195e7d Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 13 Jun 2024 11:26:51 -0600 Subject: [PATCH 2/6] docs: update messages, ready for Juliet --- messages/bulkv2.delete.md | 10 +++++++++- src/commands/data/delete/bulk.ts | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/messages/bulkv2.delete.md b/messages/bulkv2.delete.md index 03ef9619..ed929fea 100644 --- a/messages/bulkv2.delete.md +++ b/messages/bulkv2.delete.md @@ -16,4 +16,12 @@ When you execute this command, it starts a job, displays the ID, and then immedi - Bulk delete records from a custom object in an org with alias my-scratch and wait 5 minutes for the command to complete: - <%= config.bin %> <%= command.id %> --sobject MyObject__c --file files/delete.csv --wait 5 --target-org my-scratch + <%= config.bin %> <%= command.id %> --sobject MyObject\_\_c --file files/delete.csv --wait 5 --target-org my-scratch + +# flags.hard-delete.summary + +summary for Juliet here :) + +# flags.hard-delete.description + +description here :) diff --git a/src/commands/data/delete/bulk.ts b/src/commands/data/delete/bulk.ts index 93cf7211..139224cd 100644 --- a/src/commands/data/delete/bulk.ts +++ b/src/commands/data/delete/bulk.ts @@ -19,7 +19,14 @@ export default class Delete extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); - public static readonly flags = { ...baseFlags, 'hard-delete': Flags.boolean({ summary: 'abc', default: false }) }; + public static readonly flags = { + ...baseFlags, + 'hard-delete': Flags.boolean({ + summary: messages.getMessage('flags.hard-delete.summary'), + description: messages.getMessage('flags.hard-delete.description'), + default: false, + }), + }; public async run(): Promise { const { flags } = await this.parse(Delete); From edb052dfab7d4889b59118a7d4f0fc233b21ca65 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 13 Jun 2024 11:56:13 -0600 Subject: [PATCH 3/6] chore: add types to imports --- src/BulkBaseCommand.ts | 4 ++-- src/api/data/tree/exportApi.ts | 2 +- src/api/file/fileToContentVersion.ts | 2 +- src/batcher.ts | 4 ++-- src/bulkOperationCommand.ts | 2 +- src/bulkUtils.ts | 2 +- src/commands/data/create/record.ts | 2 +- src/commands/data/delete/record.ts | 2 +- src/commands/data/get/record.ts | 2 +- src/commands/data/query.ts | 2 +- src/commands/data/query/resume.ts | 2 +- src/commands/data/update/record.ts | 2 +- src/dataCommand.ts | 2 +- src/dataSoqlQueryTypes.ts | 2 +- src/queryUtils.ts | 2 +- src/reporters/csvReporter.ts | 2 +- src/reporters/humanReporter.ts | 2 +- test/api/data/tree/export.test.ts | 2 +- test/commands/data/create/file.nut.ts | 2 +- test/commands/data/create/record.test.ts | 2 +- test/commands/data/dataBulk.nut.ts | 2 +- test/commands/data/delete/record.test.ts | 2 +- test/commands/data/query/resume.nut.ts | 2 +- test/commands/data/record/dataRecord.nut.ts | 2 +- test/commands/data/update/record.test.ts | 2 +- test/testUtil.ts | 2 +- 26 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/BulkBaseCommand.ts b/src/BulkBaseCommand.ts index 4e39bf48..31f821c8 100644 --- a/src/BulkBaseCommand.ts +++ b/src/BulkBaseCommand.ts @@ -6,11 +6,11 @@ */ import { SfCommand, Spinner } from '@salesforce/sf-plugins-core'; -import { IngestJobV2, JobInfoV2 } from '@jsforce/jsforce-node/lib/api/bulk2.js'; +import type { IngestJobV2, JobInfoV2 } from '@jsforce/jsforce-node/lib/api/bulk2.js'; import { Duration } from '@salesforce/kit'; import { capitalCase } from 'change-case'; import { Messages } from '@salesforce/core'; -import { Schema } from '@jsforce/jsforce-node'; +import type { Schema } from '@jsforce/jsforce-node'; import { getResultMessage } from './reporters/reporters.js'; import { BulkDataRequestCache } from './bulkDataRequestCache.js'; diff --git a/src/api/data/tree/exportApi.ts b/src/api/data/tree/exportApi.ts index 08ff6948..b145dc15 100644 --- a/src/api/data/tree/exportApi.ts +++ b/src/api/data/tree/exportApi.ts @@ -9,7 +9,7 @@ import path from 'node:path'; import fs from 'node:fs'; import { Logger, Messages, Org, SfError, Lifecycle } from '@salesforce/core'; -import { DescribeSObjectResult, QueryResult } from '@jsforce/jsforce-node'; +import type { DescribeSObjectResult, QueryResult } from '@jsforce/jsforce-node'; import { Ux } from '@salesforce/sf-plugins-core'; import { BasicRecord, diff --git a/src/api/file/fileToContentVersion.ts b/src/api/file/fileToContentVersion.ts index 87d83618..2b403f80 100644 --- a/src/api/file/fileToContentVersion.ts +++ b/src/api/file/fileToContentVersion.ts @@ -8,7 +8,7 @@ import { readFile } from 'node:fs/promises'; import { basename } from 'node:path'; import { Connection } from '@salesforce/core'; -import { Record, SaveResult } from '@jsforce/jsforce-node'; +import type { Record, SaveResult } from '@jsforce/jsforce-node'; import FormData from 'form-data'; export type ContentVersion = { diff --git a/src/batcher.ts b/src/batcher.ts index d25c9494..6c561248 100644 --- a/src/batcher.ts +++ b/src/batcher.ts @@ -8,10 +8,10 @@ import { ReadStream } from 'node:fs'; import { Connection, Messages, SfError } from '@salesforce/core'; import { Ux } from '@salesforce/sf-plugins-core'; -import { Schema } from '@jsforce/jsforce-node'; +import type { Schema } from '@jsforce/jsforce-node'; import { stringify } from 'csv-stringify/sync'; import { parse } from 'csv-parse'; -import { +import type { Batch, BatchInfo, BulkIngestBatchResult, diff --git a/src/bulkOperationCommand.ts b/src/bulkOperationCommand.ts index ab8012d6..3cf1476f 100644 --- a/src/bulkOperationCommand.ts +++ b/src/bulkOperationCommand.ts @@ -11,7 +11,7 @@ import { Flags, SfCommand } from '@salesforce/sf-plugins-core'; import { Duration } from '@salesforce/kit'; import { Connection, Messages } from '@salesforce/core'; import { Ux } from '@salesforce/sf-plugins-core/Ux'; -import { Schema } from '@jsforce/jsforce-node'; +import type { Schema } from '@jsforce/jsforce-node'; import { BulkV2, IngestJobV2, diff --git a/src/bulkUtils.ts b/src/bulkUtils.ts index 068e7fab..792572bf 100644 --- a/src/bulkUtils.ts +++ b/src/bulkUtils.ts @@ -13,7 +13,7 @@ import type { IngestJobV2UnprocessedRecords, } from '@jsforce/jsforce-node/lib/api/bulk2.js'; -import { Schema } from '@jsforce/jsforce-node'; +import type { Schema } from '@jsforce/jsforce-node'; import { Connection, Messages } from '@salesforce/core'; import { BulkProcessedRecordV2, BulkRecordsV2 } from './types.js'; diff --git a/src/commands/data/create/record.ts b/src/commands/data/create/record.ts index 9a51df52..e205bc84 100644 --- a/src/commands/data/create/record.ts +++ b/src/commands/data/create/record.ts @@ -6,7 +6,7 @@ */ import { Messages } from '@salesforce/core'; -import { SaveResult } from '@jsforce/jsforce-node'; +import type { SaveResult } from '@jsforce/jsforce-node'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { orgFlags, perflogFlag } from '../../../flags.js'; import { stringToDictionary, collectErrorMessages } from '../../../dataCommand.js'; diff --git a/src/commands/data/delete/record.ts b/src/commands/data/delete/record.ts index e9610d15..cd4b131a 100644 --- a/src/commands/data/delete/record.ts +++ b/src/commands/data/delete/record.ts @@ -6,7 +6,7 @@ */ import { Messages, SfError } from '@salesforce/core'; -import { SaveResult } from '@jsforce/jsforce-node'; +import type { SaveResult } from '@jsforce/jsforce-node'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { orgFlags, perflogFlag } from '../../../flags.js'; import { collectErrorMessages, query } from '../../../dataCommand.js'; diff --git a/src/commands/data/get/record.ts b/src/commands/data/get/record.ts index 5f806170..0ed6157c 100644 --- a/src/commands/data/get/record.ts +++ b/src/commands/data/get/record.ts @@ -6,7 +6,7 @@ */ import { Messages, SfError } from '@salesforce/core'; -import { Record } from '@jsforce/jsforce-node'; +import type { Record } from '@jsforce/jsforce-node'; import { toAnyJson } from '@salesforce/ts-types'; import { SfCommand, Flags, Ux } from '@salesforce/sf-plugins-core'; import { orgFlags, perflogFlag } from '../../../flags.js'; diff --git a/src/commands/data/query.ts b/src/commands/data/query.ts index 9bb45679..ed096474 100644 --- a/src/commands/data/query.ts +++ b/src/commands/data/query.ts @@ -8,7 +8,7 @@ import fs from 'node:fs'; import { Connection, Logger, Messages, SfError } from '@salesforce/core'; -import { Record as jsforceRecord } from '@jsforce/jsforce-node'; +import type { Record as jsforceRecord } from '@jsforce/jsforce-node'; import { AnyJson, ensureJsonArray, diff --git a/src/commands/data/query/resume.ts b/src/commands/data/query/resume.ts index b1eb4bb7..2d0bbcfe 100644 --- a/src/commands/data/query/resume.ts +++ b/src/commands/data/query/resume.ts @@ -7,7 +7,7 @@ import { Messages } from '@salesforce/core'; import { QueryJobV2 } from '@jsforce/jsforce-node/lib/api/bulk2.js'; -import { Record as jsforceRecord } from '@jsforce/jsforce-node'; +import type { Record as jsforceRecord } from '@jsforce/jsforce-node'; import { Flags, loglevel, diff --git a/src/commands/data/update/record.ts b/src/commands/data/update/record.ts index 41d0637a..c78d041a 100644 --- a/src/commands/data/update/record.ts +++ b/src/commands/data/update/record.ts @@ -6,7 +6,7 @@ */ import { Messages, SfError } from '@salesforce/core'; -import { SaveError, SaveResult } from '@jsforce/jsforce-node'; +import type { SaveError, SaveResult } from '@jsforce/jsforce-node'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { orgFlags } from '../../../flags.js'; import { collectErrorMessages, query, stringToDictionary } from '../../../dataCommand.js'; diff --git a/src/dataCommand.ts b/src/dataCommand.ts index 26b4faa7..45a9356f 100644 --- a/src/dataCommand.ts +++ b/src/dataCommand.ts @@ -6,7 +6,7 @@ */ import { Connection, Messages, SfError } from '@salesforce/core'; -import { Record as jsforceRecord, SaveResult } from '@jsforce/jsforce-node'; +import type { Record as jsforceRecord, SaveResult } from '@jsforce/jsforce-node'; import { Ux } from '@salesforce/sf-plugins-core'; import { GenericObject } from './types.js'; diff --git a/src/dataSoqlQueryTypes.ts b/src/dataSoqlQueryTypes.ts index eedb02e3..70dc9fc7 100644 --- a/src/dataSoqlQueryTypes.ts +++ b/src/dataSoqlQueryTypes.ts @@ -5,7 +5,7 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { QueryResult, Record } from '@jsforce/jsforce-node'; +import type { QueryResult, Record } from '@jsforce/jsforce-node'; import { Optional } from '@salesforce/ts-types'; import { GenericEntry } from './types.js'; diff --git a/src/queryUtils.ts b/src/queryUtils.ts index acce8e6a..c9683318 100644 --- a/src/queryUtils.ts +++ b/src/queryUtils.ts @@ -4,7 +4,7 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { Record } from '@jsforce/jsforce-node'; +import type { Record } from '@jsforce/jsforce-node'; import { Field, FieldType, SoqlQueryResult } from './dataSoqlQueryTypes.js'; import { FormatTypes, JsonReporter } from './reporters/reporters.js'; import { CsvReporter } from './reporters/csvReporter.js'; diff --git a/src/reporters/csvReporter.ts b/src/reporters/csvReporter.ts index ecfa5044..a6d50622 100644 --- a/src/reporters/csvReporter.ts +++ b/src/reporters/csvReporter.ts @@ -7,7 +7,7 @@ import { EOL } from 'node:os'; import { Ux } from '@salesforce/sf-plugins-core'; import { get, getNumber, isString } from '@salesforce/ts-types'; -import { Record as jsforceRecord } from '@jsforce/jsforce-node'; +import type { Record as jsforceRecord } from '@jsforce/jsforce-node'; import { Field, SoqlQueryResult } from '../dataSoqlQueryTypes.js'; import { getAggregateAliasOrName, maybeMassageAggregates } from './reporters.js'; import { QueryReporter, logFields, isSubquery, isAggregate } from './reporters.js'; diff --git a/src/reporters/humanReporter.ts b/src/reporters/humanReporter.ts index f0f46db2..4ca20b50 100644 --- a/src/reporters/humanReporter.ts +++ b/src/reporters/humanReporter.ts @@ -8,7 +8,7 @@ import { Ux } from '@salesforce/sf-plugins-core'; import ansis from 'ansis'; import { get, getArray, isPlainObject, isString, Optional } from '@salesforce/ts-types'; import { Messages } from '@salesforce/core'; -import { Record as jsforceRecord } from '@jsforce/jsforce-node'; +import type { Record as jsforceRecord } from '@jsforce/jsforce-node'; import { GenericEntry, GenericObject } from '../types.js'; import { Field, FieldType, SoqlQueryResult } from '../dataSoqlQueryTypes.js'; import { QueryReporter, logFields, isSubquery, isAggregate, getAggregateAliasOrName } from './reporters.js'; diff --git a/test/api/data/tree/export.test.ts b/test/api/data/tree/export.test.ts index 41e237ee..890055d0 100644 --- a/test/api/data/tree/export.test.ts +++ b/test/api/data/tree/export.test.ts @@ -6,7 +6,7 @@ */ import { expect, config } from 'chai'; -import { DescribeSObjectResult } from '@jsforce/jsforce-node'; +import type { DescribeSObjectResult } from '@jsforce/jsforce-node'; import { RefFromIdByType, buildRefMap, diff --git a/test/commands/data/create/file.nut.ts b/test/commands/data/create/file.nut.ts index 2dedb268..a30a5f5b 100644 --- a/test/commands/data/create/file.nut.ts +++ b/test/commands/data/create/file.nut.ts @@ -8,7 +8,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; import { expect } from 'chai'; -import { SaveResult } from '@jsforce/jsforce-node'; +import type { SaveResult } from '@jsforce/jsforce-node'; import { SoqlQueryResult } from '../../../../src/dataSoqlQueryTypes.js'; import { ContentVersion } from '../../../../src/api/file/fileToContentVersion.js'; diff --git a/test/commands/data/create/record.test.ts b/test/commands/data/create/record.test.ts index 983fd4a9..5970b738 100644 --- a/test/commands/data/create/record.test.ts +++ b/test/commands/data/create/record.test.ts @@ -11,7 +11,7 @@ import { Config } from '@oclif/core/config'; import { expect } from 'chai'; import { TestContext, MockTestOrgData } from '@salesforce/core/testSetup'; import { AnyJson, ensureJsonMap, ensureString } from '@salesforce/ts-types'; -import { SaveResult } from '@jsforce/jsforce-node'; +import type { SaveResult } from '@jsforce/jsforce-node'; import Create from '../../../../src/commands/data/create/record.js'; const sObjectId = '0011100001zhhyUAAQ'; diff --git a/test/commands/data/dataBulk.nut.ts b/test/commands/data/dataBulk.nut.ts index ba9c87e1..72711a8e 100644 --- a/test/commands/data/dataBulk.nut.ts +++ b/test/commands/data/dataBulk.nut.ts @@ -12,7 +12,7 @@ import { expect, config as chaiConfig } from 'chai'; import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; import { sleep } from '@salesforce/kit'; import { ensurePlainObject } from '@salesforce/ts-types'; -import { SaveResult } from '@jsforce/jsforce-node'; +import type { SaveResult } from '@jsforce/jsforce-node'; import { BulkResultV2 } from '../../../src/types.js'; import { QueryResult } from './dataSoqlQuery.nut.js'; diff --git a/test/commands/data/delete/record.test.ts b/test/commands/data/delete/record.test.ts index be4c8b2c..608f6389 100644 --- a/test/commands/data/delete/record.test.ts +++ b/test/commands/data/delete/record.test.ts @@ -13,7 +13,7 @@ import { ensureJsonMap, ensureString, AnyJson } from '@salesforce/ts-types'; import { expect } from 'chai'; import { Config } from '@oclif/core/config'; -import { SaveResult } from '@jsforce/jsforce-node'; +import type { SaveResult } from '@jsforce/jsforce-node'; import Delete from '../../../../src/commands/data/delete/record.js'; const sObjectId = '0011100001zhhyUAAQ'; diff --git a/test/commands/data/query/resume.nut.ts b/test/commands/data/query/resume.nut.ts index 0bc4a0b5..c57e86c5 100644 --- a/test/commands/data/query/resume.nut.ts +++ b/test/commands/data/query/resume.nut.ts @@ -7,7 +7,7 @@ import path from 'node:path'; import { expect, config } from 'chai'; import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; -import { QueryResult, Record } from '@jsforce/jsforce-node'; +import type { QueryResult, Record } from '@jsforce/jsforce-node'; import { sleep } from '@salesforce/kit'; import { JobInfoV2 } from '@jsforce/jsforce-node/lib/api/bulk2.js'; config.truncateThreshold = 0; diff --git a/test/commands/data/record/dataRecord.nut.ts b/test/commands/data/record/dataRecord.nut.ts index 0f0ed868..84b5e41a 100644 --- a/test/commands/data/record/dataRecord.nut.ts +++ b/test/commands/data/record/dataRecord.nut.ts @@ -9,7 +9,7 @@ import { strict as assert } from 'node:assert/strict'; import { expect } from 'chai'; import { execCmd, genUniqueString, TestSession } from '@salesforce/cli-plugins-testkit'; import { Dictionary } from '@salesforce/ts-types'; -import { SaveResult } from '@jsforce/jsforce-node'; +import type { SaveResult } from '@jsforce/jsforce-node'; type AccountRecord = { Id: string; diff --git a/test/commands/data/update/record.test.ts b/test/commands/data/update/record.test.ts index 28068287..af6d7caf 100644 --- a/test/commands/data/update/record.test.ts +++ b/test/commands/data/update/record.test.ts @@ -13,7 +13,7 @@ import { expect } from 'chai'; import { TestContext, MockTestOrgData, shouldThrow } from '@salesforce/core/testSetup'; import { Config } from '@oclif/core/config'; import { SfError } from '@salesforce/core/sfError'; -import { SaveResult } from '@jsforce/jsforce-node'; +import type { SaveResult } from '@jsforce/jsforce-node'; import Update from '../../../../src/commands/data/update/record.js'; const sObjectId = '0011100001zhhyUAAQ'; diff --git a/test/testUtil.ts b/test/testUtil.ts index e00feee2..32f1b23c 100644 --- a/test/testUtil.ts +++ b/test/testUtil.ts @@ -6,7 +6,7 @@ */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { QueryResult, SaveResult, UpsertResult, UserInfo } from '@jsforce/jsforce-node'; +import type { QueryResult, SaveResult, UpsertResult, UserInfo } from '@jsforce/jsforce-node'; import { Connection } from '@salesforce/core'; import EventEmitter = NodeJS.EventEmitter; From 1f608eeffc188621e5a37ac6d310b7069ea3676a Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 13 Jun 2024 14:24:27 -0600 Subject: [PATCH 4/6] test: add UTs, update messages --- messages/bulk.operation.command.md | 4 + src/bulkOperationCommand.ts | 4 + test/commands/data/delete/bulk.test.ts | 176 +++++++++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 test/commands/data/delete/bulk.test.ts diff --git a/messages/bulk.operation.command.md b/messages/bulk.operation.command.md index f25b06c0..95553561 100644 --- a/messages/bulk.operation.command.md +++ b/messages/bulk.operation.command.md @@ -17,3 +17,7 @@ Run the command asynchronously. # flags.verbose.summary Print verbose output of failed records if result is available. + +# hard-delete-permission-error + +You must have the "Bulk API Hard Delete" system permission to use the --hard-delete flag. This permission is disabled by default and can be enabled only by a system administrator. diff --git a/src/bulkOperationCommand.ts b/src/bulkOperationCommand.ts index 3cf1476f..cc7bd4e8 100644 --- a/src/bulkOperationCommand.ts +++ b/src/bulkOperationCommand.ts @@ -146,6 +146,10 @@ export const runBulkOperation = async ({ } return result; } catch (err) { + if (err instanceof Error && err.name === 'FEATURENOTENABLED' && operation === 'hardDelete') { + // add info specific to hardDelete permission + err.message = messages.getMessage('hard-delete-permission-error'); + } cmd.spinner.stop(); throw err; } diff --git a/test/commands/data/delete/bulk.test.ts b/test/commands/data/delete/bulk.test.ts new file mode 100644 index 00000000..e2c9f0e5 --- /dev/null +++ b/test/commands/data/delete/bulk.test.ts @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import fs from 'node:fs'; +import { TestContext, MockTestOrgData, shouldThrow } from '@salesforce/core/testSetup'; +import { Config } from '@oclif/core/config'; +import { assert, expect } from 'chai'; +import { IngestJobV2, JobInfoV2 } from '@jsforce/jsforce-node/lib/api/bulk2.js'; +import Bulk from '../../../../src/commands/data/delete/bulk.js'; + +describe('data:delete:bulk', () => { + const $$ = new TestContext(); + const testOrg = new MockTestOrgData(); + let config: Config; + + before(async () => { + config = new Config({ root: resolve(dirname(fileURLToPath(import.meta.url)), '../../../..') }); + await config.load(); + }); + + beforeEach(async () => { + await $$.stubAuths(testOrg); + $$.SANDBOX.stub(fs, 'existsSync').returns(true); + $$.SANDBOX.stub(fs, 'createReadStream').resolves(); + // @ts-expect-error only stubbing a very small part + $$.SANDBOX.stub(fs.promises, 'stat').resolves({ isFile: () => true, isDirectory: () => true }); + $$.SANDBOX.stub(IngestJobV2.prototype, 'open').resolves(); + $$.SANDBOX.stub(IngestJobV2.prototype, 'uploadData').resolves(); + $$.SANDBOX.stub(IngestJobV2.prototype, 'close').resolves(); + $$.SANDBOX.stub(IngestJobV2.prototype, 'poll').resolves(); + }); + + afterEach(async () => { + $$.SANDBOX.restore(); + }); + + it('should pass the hardDelete option to the api', async () => { + const options: JobInfoV2 = { + operation: 'hardDelete', + id: '123', + object: 'Account', + apiActiveProcessingTime: 0, + assignmentRuleId: '90', + contentUrl: '389', + errorMessage: undefined, + externalIdFieldName: '123', + jobType: 'V2Ingest', + state: 'JobComplete', + apiVersion: 44.0, + concurrencyMode: 'Parallel', + retries: 0, + totalProcessingTime: 1, + apexProcessingTime: 1, + columnDelimiter: 'BACKQUOTE', + numberRecordsProcessed: 10, + contentType: 'CSV', + numberRecordsFailed: 0, + createdById: '234', + createdDate: '', + systemModstamp: '', + lineEnding: 'LF', + }; + + // we can't spy on ESM modules... :( + $$.SANDBOX.stub(IngestJobV2.prototype, 'check').resolves(options); + + const result = await Bulk.run([ + '--target-org', + 'test@org.com', + '--hard-delete', + '--file', + '../../oss/plugin-data/test/test-files/data-project/data/bulkUpsertLarge.csv', + '--sobject', + 'Account', + ]); + expect(result).to.deep.equal({ + jobInfo: options, + }); + }); + + it('should handle user without permission error', async () => { + const e = new Error('FEATURENOTENABLED'); + e.name = 'FEATURENOTENABLED'; + $$.SANDBOX.stub(IngestJobV2.prototype, 'check').throws(e); + + const bulk = new Bulk( + [ + '--target-org', + 'test@org.com', + '--hard-delete', + '--file', + '../../oss/plugin-data/test/test-files/data-project/data/bulkUpsertLarge.csv', + '--sobject', + 'Account', + ], + config + ); + try { + await shouldThrow(bulk.run()); + } catch (err) { + assert(err instanceof Error); + expect(err.message).to.equal( + 'You must have the "Bulk API Hard Delete" system permission to use the --hard-delete flag. This permission is disabled by default and can be enabled only by a system administrator.' + ); + } + }); + it('should not change error when not using --hard-delete', async () => { + const e = new Error('some other server-side error, but not permissions'); + $$.SANDBOX.stub(IngestJobV2.prototype, 'check').throws(e); + + const bulk = new Bulk( + [ + '--target-org', + 'test@org.com', + '--file', + '../../oss/plugin-data/test/test-files/data-project/data/bulkUpsertLarge.csv', + '--sobject', + 'Account', + ], + config + ); + try { + await shouldThrow(bulk.run()); + } catch (err) { + assert(err instanceof Error); + expect(err.message).to.equal('some other server-side error, but not permissions'); + } + }); + + it('should succeed', async () => { + const options: JobInfoV2 = { + operation: 'delete', + id: '123', + object: 'Account', + apiActiveProcessingTime: 0, + assignmentRuleId: '90', + contentUrl: '389', + errorMessage: undefined, + externalIdFieldName: '123', + jobType: 'V2Ingest', + state: 'JobComplete', + apiVersion: 44.0, + concurrencyMode: 'Parallel', + retries: 0, + totalProcessingTime: 1, + apexProcessingTime: 1, + columnDelimiter: 'BACKQUOTE', + numberRecordsProcessed: 10, + contentType: 'CSV', + numberRecordsFailed: 0, + createdById: '234', + createdDate: '', + systemModstamp: '', + lineEnding: 'LF', + }; + $$.SANDBOX.stub(IngestJobV2.prototype, 'check').resolves(options); + + const result = await Bulk.run([ + '--target-org', + 'test@org.com', + '--file', + '../../oss/plugin-data/test/test-files/data-project/data/bulkUpsertLarge.csv', + '--sobject', + 'Account', + ]); + expect(result).to.deep.equal({ + jobInfo: options, + }); + }); +}); From 5672666f3d3bd0346f7a2c229fb5e4b33f817038 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 13 Jun 2024 14:26:30 -0600 Subject: [PATCH 5/6] chore: update schema --- command-snapshot.json | 1 + 1 file changed, 1 insertion(+) diff --git a/command-snapshot.json b/command-snapshot.json index e99d7f20..e1776817 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -35,6 +35,7 @@ "async", "file", "flags-dir", + "hard-delete", "json", "loglevel", "sobject", From 9ab315dbf5a7a0f793c68c8923c10b64f0fb6793 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 13 Jun 2024 15:16:42 -0600 Subject: [PATCH 6/6] docs: update messages --- messages/bulkv2.delete.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/messages/bulkv2.delete.md b/messages/bulkv2.delete.md index ed929fea..3c6c0326 100644 --- a/messages/bulkv2.delete.md +++ b/messages/bulkv2.delete.md @@ -16,12 +16,12 @@ When you execute this command, it starts a job, displays the ID, and then immedi - Bulk delete records from a custom object in an org with alias my-scratch and wait 5 minutes for the command to complete: - <%= config.bin %> <%= command.id %> --sobject MyObject\_\_c --file files/delete.csv --wait 5 --target-org my-scratch + <%= config.bin %> <%= command.id %> --sobject MyObject__c --file files/delete.csv --wait 5 --target-org my-scratch # flags.hard-delete.summary -summary for Juliet here :) +Mark the records as immediately eligible for deletion by your org. If you don't specify this flag, the deleted records go into the Recycle Bin. # flags.hard-delete.description -description here :) +You must have the "Bulk API Hard Delete" system permission to use this flag. The permission is disabled by default and can be enabled only by a system administrator.